JavaScript: Nomes, vinculação, escopo e closures
Nomes, vinculação e escopo
Em programas de computador, nomes representam coisas, como valores, comportamentos etc. Os nomes são vinculados às coisas que representam em diversos momentos. Por exemplo:
- O nome
if
é vinculado ao comportamento de estrutura de seleção em tempo de projeto de linguagem - Em C, quando você escreve
#define PI 3.14159
, o nomePI
é vinculado ao valor3.14159
em tempo de compilação - Em geral, variáveis são vinculadas aos seus valores em tempo de execução
Em linguagens de programação, vinculação (ou amarração, ou ligação, ou binding) se refere à vinculação de um nome a um valor ou comportamento.
Escopo de uma vinculação é o conjunto de trechos de um programa que consegue usar a vinculação. Exemplo:
O escopo da variável glob
corresponde às linhas 2, 4, 5 e 7 do programa. O escopo da variável x
corresponde à linha 5.
A variável glob
tem um escopo global, pois é acessível em todo o programa (essa é uma definição simplificada). A variável x
tem um escopo local, pois só é acessível dentro da função no qual ela é definida. A variável teste
também possui escopo global (lembre-se de que em Javascript funções também são valores!).
A maioria das linguagens de programação adota regras de escopo léxico (ou estático). Isso significa que é possível determinar o escopo de uma variável somente lendo o código-fonte do programa. Algumas linguagens de programação usam o escopo dinâmico; uma situação comum nesse caso é uma função ter acesso a variáveis definidas na função que a chamou. Pode-se argumentar que uma linguagem de programação que adota o escopo dinâmico tende a resultar em programas mais difíceis de entender.
O contexto ou ambiente de referenciamento de uma região de um programa corresponde ao conjunto de vinculações que pode-se acessar naquela região. Vejamos o exemplo anterior:
O ambiente de referenciamento da linha 7 corresponde às variáveis glob
e teste
(e, naturalmente, outras variáveis definidas globalmente pelo browser, a exemplo de window
e console
). O ambiente de referenciamento da linha 5 contém todas essas variáveis e mais a variável x
.
Regras de escopo de Javascript
Considere agora o exemplo a seguir. O que acontecerá quando a linha 8 for executada? Vai dar erro? Para responder a essa pergunta, devemos descobrir se a variável xext
faz ou não faz parte do ambiente de referenciamento da linha 8 (e da função interna
, em geral). Faça suas apostas e execute o código.
Para buscar a variável xext
, o interpretador Javascript primeiro procura nas variáveis definidas dentro da função interna
. Não encontrando, ele vai procurar variáveis definidas no contexto da função externa
. Se ainda não encontrar, procura nas variáveis globais. Ou seja, o ambiente de referenciamento inclui o escopo atual e mais todos os escopos externos.
Então o que vai acontecer no seguinte código?
Existem três vinculações diferentes do nome x
, com diferentes escopos. Surge a dúvida: se o x
da linha 1 tem escopo global, isso significa que esse escopo se estende à linha 8?
Não. A declaração de x
na linha 4 mascara a variável x
da linha 1 (esse fenômeno é também conhecido como sombreamento. O resultado disso é que, dentro da função externa
, vale a variável x
da linha 4
… exceto na linha 8, pois nesse caso a variável que vale é a variável x
definida na linha 7.
No final das contas, o escopo de x
da linha 1 é a linha 12; o escopo de x
da linha 4 é a linha 10; e o escopo de x
da linha 7 é a linha 8.
Closures
(Baseado em https://developer.mozilla.org/en/docs/Web/JavaScript/Closures)
Closures (do inglês, fechamento) são funções que:
- referenciam variáveis livres, isto é, variáveis que não foram definidas dentro da função e nem são parâmetro da função
- têm acesso a essas variáveis mesmo depois que elas saem de escopo.
Do ponto de vista operacional, uma closure é um registro que armazena uma função juntamente com um ambiente de referenciamento.
Considere o seguinte exemplo:
A função mostraNome()
é uma função interna que faz referência à variável livre nome
, que é uma variável que não está definida no escopo da função mostraNome()
, e sim no escopo da função alo()
. Isso ainda não é um exemplo de closure pois, embora mostraNome()
satisfaça à primeira condição (referencia variáveis livres), não satisfaz à segunda: quando nome
sai de escopo, a função mostraNome()
também sai de escopo.
Captura de variáveis locais
Agora considere um exemplo de closure:
O resultado é o mesmo, mas a função criaFuncaoAlo()
desta vez retorna a função interna mostraNome()
, sem executá-la. Em vez disso, a função só é executada na última linha de código.
O código não é muito intuitivo. Normalmente, uma variável local definida em uma função só existe enquanto a função é executada; quando a execução termina, a variável deveria ser destruída e deixar de ser acessível. No exemplo, no entanto, a variável nome
ainda está acessível depois que a função criaFuncaoAlo()
terminou de executar.
A solução desse mistério é que mostraNome()
tornou-se uma closure, que é um objeto especial que combina duas coisas: uma função e o ambiente na qual a função foi criada. O ambiente consiste de todas as variáveis locais disponíveis no momento em que a função foi criada. Dizemos que a função mostraNome()
captura a variável nome.
closure mostraNome =
função mostraNome +
variável nome (ambiente de referenciamento)
Captura de parâmetros
Closures também capturam parâmetros das funções externas:
Visualizando o escopo no DevTools (Google Chrome)
Copie o código abaixo e cole no console JavaScript:
function criaFuncaoAlo(nome) {
var varlocal = "testando";
function mostraNome() {
console.log("Alo, " + nome);
}
console.log(varlocal + "!!!");
return mostraNome;
}
var alo = criaFuncaoAlo("Mundo");
Agora escreva no console:
alo.prototype
Então visualize a propriedade constructor
> [[Scopes]]
> Closure
. Veja que a variável nome
foi capturada, mas a variável varlocal
não, pois ela não é usada dentro da função mostraNome()
.
Extra: inclua uma linha console.log(eval(prompt()))
dentro da função mostraNome()
e então visualize o escopo da função.
Outros exemplos
Mais um exemplo:
Qual o resultado? Três números iguais? Três números diferentes? O primeiro número será igual ao terceiro? Faça suas apostas, execute e revise seu conhecimento sobre closures.
Agora um exemplo mais útil, mostrando que parâmetros também são capturados por uma closure:
A closure também pode alterar as variáveis capturadas:
Closures e programação orientada a objetos
Uma closure permite associar alguns dados (o ambiente) com a função que opera sobre os dados. Isso soa como programação orientada a objetos, na qual objetos permitem associar alguns dados (as propriedades do objeto) a um ou mais métodos.
Exemplo:
Obviamente, esse esquema é limitado, pois não há herança e polimorfismo.
Closures na prática (programação web)
É importante entender closures para programar Javascript para web? Muito! Grande parte do código que escrevemos em Javascript para web é baseado em eventos: definimos um comportamento, então anexamos o comportamento a um evento que é disparado pelo usuário (por exemplo, um clique). O código é geralmente escrito em um callback: uma função que é executada em resposta a um evento.
Exemplo (rode o código e depois clique no botão):
Agora vamos fazer com três botões?
Funcionou como você gostaria? Qual a explicação?
Como resolver esse problema? A solução é capturar o índice i
em uma função retornada dentro de outra função:
Podemos fazer a mesma coisa sem criar uma função auxiliar (basta trocar criaFuncaoAlerta
pela sua implementação como função anônima):
Note que definimos uma função anônima e chamamos essa função imediatamente. Esse é um padrão tão comum no desenvolvimento web em JavaScript que tem até um nome: IIFE (immediately-invoked function expression, ou expressão de função invocada imediatamente).