Funções em Diferentes Escopos

Se você programa em JavaScript orientado a objeto (1, 2, 3), provavelmente já teve alguns problemas com funções que deveriam ser executadas em um determinado escopo. Nesse post vou explicar como executar e vincular funções ao escopo de algum objeto.

Executando

Para chamar um método no escopo de outro objeto, utilizamos o método Function.apply. O objeto Function pode ser qualquer função que será chamada no escopo que deve ser passado como primeiro parâmetro. Vamos a um exemplo:

var Class = function() {

 this.text = "Escopo da Classe";

}

Class = new Class();

var showText = function() {

 alert(this.text);

}

// Escopo Global

showText(); // undefined

// Escopo de Class

showText.apply(Class); // "Escopo da Classe"

O resultado da primeira chamada a função é obvio: não há this.text definido. Quando aplicamos a mesma função no escopo de Class, o retorno sai como esperado.

O método Function.apply também pode receber um segundo parâmetro, contendo os parãmetros que devem ser passados à função. Esse parâmetro deve ser obrigatoriamente um array, e deve ser definido na ordem em que os parâmetros precisam ser recebidos.

var Class = function() {

 this.text = "Também podemos passar parâmetros:";

}

var Class = new Class();

var showText = function(text) {

 alert(this.text + " " + text);

}

showText(); // undefined undefined

showText.apply(Class, ["Viva!"]); // "Também podemos passar parâmetros: Viva!"

Também há o método Function.call, que tem a mesma função de Function.aplly. A diferença está em como os parâmetros para a função são definidos. Em Function.apply, eles são passados como um array, e em Function.call são passados como vários argumentos, sem a necessidade de estarem dentro de algum array.

var Class = function() {

 this.text = "Também podemos passar parâmetros:";

}

var Class = new Class();

var showText = function(text) {

 alert(this.text + " " + text);

}

showText(); // undefined undefined

showText.call(Class, "Eba!"); // "Também podemos passar parâmetros: Eba!"

Vinculando

O Function.apply resolve os problemas de muita gente, mas não todos os problemas. Imagine a seguinte situação (pela qual eu também já passei), onde precisamos recuperar uma variável da classe de dentro do objeto XMLHttpRequest:

function() {

 this.msgToShow = "Completo!";

 XMLHttpRequest.onreadystatechange = show;

}

var show = function() {

 alert(this.msgToShow);

}

E agora? Precisamos executar a função show no escopo da classe, e não no escopo do XMLHttpRequest. Poderíamos usar o Function.apply, mas ele executaria a função assim que fosse chamado. A solução é vincular a função ao escopo do objeto XMLHttpRequest. A solução mais comum é o uso de closures:

function() {

 var self = this;

 this.msgToShow = "Completo!";

 XMLHttpRequest.onreadystatechange = function() {

 	show.apply(self);

 }

}

var show = function() {

 alert(this.msgToShow);

}

Nos aproveitamos da capacidade do JavaScript de compartilhar variáveis entre funções, e armazenamos o escopo correto na variável self, que utilizamos para aplicar a função.

Reutilização

Tudo funcionando dentro do esperado, mas não estamos pensando na parte mais bela da programação orientada a objetos: a reutilização. Teríamos que criar closures toda vez em que uma execução em outro escopo fosse necessária. Pra isso existe o método Function.bind, criado para vincular a função a qualquer escopo. A função é a seguinte:

Function.prototype.bind = function() {

 var self = this, args = $A(arguments), scope = args.shift();

 return function() {

 	return self.apply(scope, args.concat($A(arguments)));

 }

}

var $A = function(object) {

 var array = [];

 for(var i = 0; i < object.length; i++)

 	array[i] = object[i];

 return array;

}

Esse método cria o closure e retorna uma função que executa o que for necessário. A função $A só serve para transformar os argumentos em um array. Modificando nosso script anterior:

function() {

 this.msgToShow = "Completo!";

 XMLHttpRequest.onreadystatechange = show.bind(this);

}

var show = function() {

 alert(this.msgToShow);

}

Quem não trabalha com POO, eventos ou timeouts talvez não tenha se deparado com nenhum problema deste tipo, mas duvido que alguém desse ramo nunca tenha recebido um “undefined” sem querer…

Posts Relacionados

Postado em dezembro 19, 2007 às 20:13

Comentários

  1. Ramon

    Esse bind eu desconhecia, muito boa dica. XD

    Valeu Julio,
    Bom retorno


  2. Diego Carrion

    Excelente Julio!


  3. JulioGreff

    Obrigado, pessoal. Em breve posto mais sobre escopo (eu acho), caso interesse.


  4. Bernardo Rufino

    Muito bom Júlio, vale lembrar que existe o método call, em que os parâmetros são passados como parâmetros mesmo e não em um Array.


  5. Rodrigo Fante

    Quem é vivo sempre aparece.. haha excelente post, muito bem explicado, detalhado e objetivo :D

    Julio, estou escrevendo também para pedir uma opiniao sua como profissional, pode responder por email se quiser, sobre o seguinte site que estou desenvolvendo com um amigo:
    http://www.yoomp.com
    Uma frase ou 2 me bastam.. heheh soh um feedback mesmo :D
    Se nao for incomodar claro.

    Abraço e Feliz Natal


  6. JulioGreff

    @Bernardo: sim, já estou atualizando o artigo, me passou despercebido. Valeu pelo lembrete.
    @Rodrigo: tá certo, te respondo via e-mail.


Trackbacks

  1. links for 2007-12-21 Dezembro 20, 2007 @ 22:24

Deixe seu comentário