terça-feira, 7 de junho de 2011

JavaScript Orientado a objeto: Passando métodos de objetos como parâmetros para setInterval e setTimeout

É freqüente a necessidade do programador javascript escrever funções ou rotinas utilizando um contador para executar determinada ação em um intervalo de tempo, principalmente em ambientes utilizando ajax onde você necessita executar uma função em determinado espaço de tempo.

setInterval e setTimeout

A função setTimeout do javascript difere-se da setInterval por um detalhe: setInterval executa a função passada por parâmetro infinitamente em intervalos de milisegundos passados como segundo parâmetro à função. Ex: setInterval('funcao()', 1000).

Obs: 1000 milisegundos equivalem à 1 segundo.

A função setTimeout executa a função passada por parâmetro apenas uma vez, depois de passados a espera, também em milisegundos, ao segundo parâmetro da função. Ex: setTimeout('funcao()', 1000)

Como passar métodos por parâmetro
É possível programar em javascript orientado a objetos. Nesse post não vou entrar nos detalhes de orientação à objetos. Mas se você desejar passar um método por parâmetro dentro de seu objetos para uma das funções acima?

Primeiro, há outra forma de passar a função por parâmetro: (Iremos referenciar sempre setInterval, mas tudo que se aplica à essa, se aplica a setTimeout. A diferença entre as duas está no tópico anterior)

setInterval(funcao, 1000). Ou seja, passa uma função apenas pelo nome, nesse caso é passado a referência da função.

Dado a seguinte classe:
<script type="text/javascript">
function Classe() {
    var intervalo = 1; // Intervalo em segundos
    this.metodo = function() {
        alert("Estou dentro de um setInterval!");
    }
    // Aqui vai o método que chama o setInterval e passa o this.metodo como parâmetro
    this.set = function() {
        setInterval(this.metodo, intervalo*1000); // Transforma segundos em milisegundos multiplicando por 1000
    }
}
var c = new Classe();
c.set();
</script>

Esse código funciona?

Funciona!

MAS


Se quiséssemos chamar um outro método dentro do Classe.metodo e ainda passar parâmetros para o método passado na função, NÃO FUNCIONA!

Mudando a classe anterior:

<script type="text/javascript">

function Classe() {
    var intervalo = 1; // Intervalo em segundos
    this.metodo = function(msg) {
        this.metodo2(msg); // Chamando um outro método para exibir o alert
    }
    this.metodo2 = function(msg) {
        alert(msg);
    }

    // Aqui vai o método que chama o setInterval e passa o this.metodo como parâmetro
    this.set = function(msg) {
        setInterval(this.metodo(msg), intervalo*1000); // Transforma segundos em milisegundos multiplicando por 1000
    }
}
var c = new Classe();
c.set('Estou dentro de um setInterval');
</script>

Primeiro, esse código gerará um erro, você não está passando uma referência da função para setInterval e sim o valor resultante da função, nesse caso undefined. Esse problema é fácil de resolver:

setInterval(function() {this.metodo(msg);}, intervalo*1000);

Ótimo, agora estou passando a referência de uma função que executa o método em seu corpo. Mas ainda causará um erro. A função this.metodo2() não está definida!

Isso acontece porquê o this.metodo está sendo chamado repetidamente dentro do contexto do objeto window, ao qual pertence a função setInterval e setTimeout (window.setInterval, window.setTimeout) e não dentro do contexto do objeto. Dessa forma, os métodos chamados dentro do método passado por parâmetro estarão dentro do contexto de window.

A SOLUÇÃO


Devemos passar uma cópia do objeto para a função chamada em setInterval ou setTimeout dessa forma:

<script type="text/javascript">

function Classe() {
    var self = this; // Essa é a cópia do objeto

    var intervalo = 1; // Intervalo em segundos

    this.metodo = function(msg) {

        this.metodo2(msg);

    }

    this.metodo2 = function(msg) {

        alert(msg);

    }

    // Aqui vai o método que chama o setInterval e passa o this.metodo como parâmetro

    this.set = function(msg) {

        setInterval(function() {self.metodo(msg);}, intervalo*1000); // E aqui enviamos o método no contexto desse objeto
    }

}

var c = new Classe();

c.set('Estou dentro de um setInterval');
</script>

Atente às linhas: 

var self = this;

Faz a cópia do objeto para uma variável privada do objeto.

setInterval(function() {self.metodo(msg);}, intervalo*1000);

Aqui é feita a "mágica", passo uma função por referência que executa o método pelo objeto passado por cópia, e não this, que estariam referenciando o objeto window.

Qualquer dúvida, só postar nos comentários. Até a próxima!