Front End

15 abr, 2011

Guia de escopo em JavaScript

Publicidade

JavaScript é uma linguagem que tem a reputação de ser tanto flexível quanto peculiar. Um exemplo de sua flexibilidade é como as funções podem ser atribuídas, aninhadas e retornadas como qualquer outro tipo de dado. No entanto, como as funções podem executar códigos enquanto aninham vários objetos e funções profundamente, acompanhar o escopo se torna muito importante. Mas o escopo no JavaScript pode ser bem solto. Por esse motivo, manter o escopo de uma função consistente pode ser complicado. A boa notícia é que, a partir do momento em que você sabe o que procurar e entende como usar algumas das ferramentas do JavaScript, acompanhar o escopo referente ao dado que você quer é na verdade muito fácil.

Começando simples

Vamos começar com um script simples para testar o escopo de uma função usando o console Firebug.

window.name = "window";

action = function(greeting) {
console.log(greeting + " " + this.name);
}

action("hello");

// hello window

O que é Firebug? – Firebug é um plugin do Firefox que oferece ferramentas valiosas para desenvolvedores de JavaScript. A ferramenta específica que vamos usar neste tutorial é o console JavaScript, para que possamos ter feedback instantâneo sobre o que está acontecendo com as nossas funções.

No exemplo acima, nós determinamos uma variável para a janela do objeto global chamado name. Depois nós definimos a função no escopo da janela que vai logar uma string ao console e então a executamos. A linha de maior interesse é na qual usamos a variável thethis. Como nossa função está dentro do escopo global da janela, ela resolve para o objeto da janela e, como tal, podemos chegar a nossa variável name e concatenar na nossa saudação string.

Esse padrão funciona bem até que tenhamos um monte de funções e variáveis flutuando em torno do nosso programa. Quando isso acontece, nós podemos empregar objetos no JavaScript para agir como recipientes.

window.name = "window";

object = {
name: "object",

action: function(greeting) {
console.log(greeting + " " + this.name);
}
}

object.action("hello");

// hello object

Nesse exemplo, nós empacotamos, ordenadamente, nossa variável name e nossa função de ação de um objeto literal. Nós também vemos que esse valor mudou para o objeto receptor, que não é mais janela, e sim objeto. Isso é bastante útil, de modo que podemos manter um conjunto de funções e variáveis abstraídas em um namespace. Tudo está parecendo ir bem, até que nossa função fica um pouco mais complexa e o JavaScript perde nosso escopo.

Perdendo o escopo e o recuperando

Nossa função pede um pouco mais de lógica, como a maioria das funções. Para lidar com isso, nós vamos precisar aninhar uma função dentro da nossa função de ação chamada nestedAction. É aqui que as coisas se tornam estranhas.

window.name = "window";

object = {
name: "object",

action: function() {
nestedAction = function(greeting) {
console.log(greeting + " " + this.name);
}

nestedAction("hello");
}
}

object.action("hello");

// hello window

Hmm, nossa função nestedAction está realmente armazenada dentro do objeto, no entanto, “this” está apontando para janela. Isso é devido à maneira como estamos executando a função nestedAction. No JavaScript, o escopo é resolvido durante a execução de funções. Apesar da função ouraction ser definida e executada dentro do objeto, nos estamos dizendo ao JavaScript para executá-la dentro do escopo da janela. Assim, a função nestedAction está, na verdade, executando globalmente, apesar de ter sido definida localmente dentro da função de ação.

Resumindo, uma vez que executamos uma função aninhada dentro de uma função, o JavaScript perde nosso escopo e está padronizando para o melhor que pode ficar, janela. Para conseguir nosso escopo de volta, o JavaScript nos oferece duas úteis funções, call e apply. 

object = {
name: "object",

action: function() {
nestedAction = function(greeting) {
console.log(greeting + " " + this.name);
}

nestedAction.call(this, "hello");
nestedAction.apply(this, ["hello"]);
}
}

object.action("hello");

// hello object
// hello object

Agora, sim. Ao usar call e apply, nós podemos especificar qual escopo nossa nestedAction deve resolver. As funções call e apply são enviadas a uma função e carregam dois tipos de parâmetros, o escopo em que a função deve resolver e os parâmetros que devem ser utilizados na hora da execução. Você vai notar que o escopo em que estamos conectando a função nestedAction para resolver é this. Nós podemos fazer isso porque o this resolve para o objeto até que aninhamos nosso código em uma função mais profunda.

Você também vai notar que call e apply funcionam da mesma maneira, exceto por uma diferença fundamental. A função call primeiro aceita o objeto em que o escopo será resolvido, e então os parâmetros são passados para a função. Por outro lado, o apply aceita objeto em que o escopo será resolvido e uma array de parâmetros para serem passados para a função. Essa diferença é importante, já que uma interface pode ser mais fácil de implementar em nosso código do que outra.

Portanto, quão flexíveis são call e apply no gerenciamento de escopo no JavaScript? Muito flexíveis, como podemos ver no código abaixo: 

window.name = "the window";

alice = {
name: "Alice"
}

eve = {
name: "Eve",

talk: function(greeting) {
console.log(greeting + ", my name is " + this.name);
}
}

eve.talk("yo");
eve.talk.apply(alice, ["hello"]);
eve.talk.apply(window, ["hi"]);

// yo, my name is eve
// hello, my name is alice
// hi, my name is the window

Nesse exemplo, o Eve é capaz de personificar tanto Alice como objeto da janela, ao passar apply para sua função talk. Entre esse exemplo e o anterior, podemos definitivamente ver como call e apply podem ser usados para ajudar o JavaScript a segurar o escopo durante a operação. No entanto, encontramos um problema sério com essa técnica. Como ainda estamos determinando o escopo no momento da execução, devemos executar a função antes do seu escopo estar resolvido. Isso se torna problemático se queremos passar funções para serem executadas depois com seu escopo intacto.

Escopo de Ligação

Uma adição bem interessante de usar junto com a biblioteca de Prototype do JavaScript é o bind. O conceito é simples, não seria fácil conectar o escopo da função na definição ao invés de na execução? Bem, com um pouquinho de JavaScript hacker, podemos construir nossa própria função bind, e fazer exatamente isso. Dê uma olhada:

Function.prototype.bind = function(scope) {
var _function = this;

return function() {
return _function.apply(scope, arguments);
}
}

alice = {
name: "alice"
}

eve = {
talk: function(greeting) {
console.log(greeting + ", my name is " + this.name);
}.bind(alice) // <- bound to "alice"
}

eve.talk("hello");

// hello, my name is alice

O exemplo acima usa a mesma técnica que o Prototype, embora sintaticamente diferente. O que estamos fazendo é influenciando a interpretação de funções flexíveis do JavaScript ao envolver nossa função talk em outro função que está propriamente “escopada”. A explicação é a seguinte:

  1. Primeiro definimos um novo prototype para a classe de função chamada “bind”, que toma o escopo como parâmetro.
  2. Como estamos trabalhando com uma função, isso dentro da nossa definição “bind” vai resolver para a função que está recebendo a mensagem “bind”.
  3. Então nós retornamos a função, que, quando executada, irá executar a função original enquanto aplica o escopo especificado nos argumentos passados.
  4. Finalmente, nós podemos “bind” nossa função talk ao escopo apropriado em sua definição e chamá-lo normalmente como sua execução.

Nesse exemplo, podemos ver como aplly é melhor que call. Como o JavaScript tem uma variável especial chamada argumentos que age como uma Array, podemos passá-la a apply e manter os parâmetros de nossa função intactos. Agora que temos um método para conectar o escopo definido, podemos retornar para nosso problema anterior e fazer funcionar como nós havíamos antecipado.

Function.prototype.bind = function(scope) {
var _function = this;

return function() {
return _function.apply(scope, arguments);
}
}

object = {
name: "object",

action: function() {
nestedAction = function(greeting) {
console.log(greeting + " " + this.name);
}.bind(this) // <- bound to "object"

nestedAction("hello");
}
}

object.action("hello");

// hello object

Conclusão

Nós vimos bastante coisa neste artigo. Primeiro, vimos como podemos utilizar namespace nas nossas funções e em suas respectivas variáveis. Então, mergulhamos dentro da maneira como o JavaScript lida com escopo e como ele pode ser controlado com call e apply. Depois de saber como controlar o escopo durante a execução de uma função, finalmente aprendemos como empacotar o escopo durante a definição de uma função, com o objetivo de manter nosso código limpo e consistente. Ao utilizar esse método, será mais fácil manter suas funções em JavaScript organizadas e propriamente abstratas enquanto você garante que o escopo seja resolvido onde você espera. 

*


Texto original de Robert Sosinks, disponível em http://www.robertsosinski.com/2009/04/28/binding-scope-in-javascript.