O fechamento é um dos conceitos fundamentais do JavaScript, causando dificuldades para muitos iniciantes, que todo programador de JS deve conhecer e entender. Para entender bem os fechamentos, é possível escrever um código melhor, mais eficiente e mais limpo. E isso, por sua vez, contribuirá para o seu crescimento profissional.
O material, cuja tradução publicamos hoje, é dedicado a uma história sobre os mecanismos internos de fechamento e como eles funcionam nos programas JavaScript.
O que é um fechamento?
Um fechamento é uma função que tem acesso a um escopo formado por uma função externa em relação a ele, mesmo após a conclusão dessa função externa. Isso significa que um fechamento pode armazenar variáveis declaradas em uma função externa e argumentos passados para ela. Antes de prosseguirmos, de fato, para os fechamentos, trataremos do conceito de "ambiente lexical".
O que é um ambiente lexical?
O termo "ambiente lexical" ou "ambiente estático" em JavaScript refere-se à capacidade de acessar variáveis, funções e objetos com base em sua localização física no código-fonte. Considere um exemplo:
let a = 'global'; function outer() { let b = 'outer'; function inner() { let c = 'inner' console.log(c); // 'inner' console.log(b); // 'outer' console.log(a); // 'global' } console.log(a); // 'global' console.log(b); // 'outer' inner(); } outer(); console.log(a); // 'global'
Aqui, a função
inner()
tem acesso a variáveis declaradas em seu próprio escopo, no escopo da função
outer()
e no escopo global. A função
outer()
tem acesso a variáveis declaradas em seu próprio escopo e no escopo global.
A cadeia de escopo do código acima ficará assim:
Global { outer { inner } }
Observe que a função
inner()
é cercada pelo ambiente lexical da função
outer()
, que por sua vez é cercada por um escopo global. É por isso que a função
inner()
pode acessar variáveis declaradas na função
outer()
e no escopo global.
Exemplos práticos de fechamentos
Considere, antes de desmontar os meandros do circuito interno, alguns exemplos práticos.
▍ Exemplo No. 1
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
Aqui chamamos a função
person()
, que retorna a função interna
displayName()
, e armazena essa função na variável
peter
. Quando, depois disso, chamamos a função
peter()
(a variável correspondente realmente armazena uma referência à função
displayName()
), o nome
Peter
é exibido no console.
Ao mesmo tempo, a função
displayName()
não possui uma variável chamada
name
, portanto, podemos concluir que essa função pode acessar a variável declarada na função externa a ela,
person()
, mesmo depois disso. como essa função funcionou. Talvez seja porque a função
displayName()
é realmente um fechamento.
▍ Exemplo No. 2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
Aqui, como no exemplo anterior, armazenamos o link para a função interna anônima retornada pela função
getCounter()
na
count
variáveis. Como a função
count()
é um fechamento, ela pode acessar a variável de
counter
da função
getCount()
mesmo depois que a função
getCounter()
concluiu seu trabalho.
Observe que o valor da variável do
counter
não é redefinido para 0 cada vez que a função
count()
é chamada. Pode parecer que seja redefinido para 0, como seria ao chamar uma função regular, mas isso não acontece.
Isso funciona assim porque toda vez que a função
count()
é chamada, um novo escopo é criado para ela, mas existe apenas um escopo para a função
getCounter()
. Como a variável do
counter
é declarada no escopo da função
getCounter()
, seu valor entre as chamadas para a função
count()
é salvo sem redefinir para 0.
Como funcionam os curtos-circuitos?
Até agora, falamos sobre o que são fechamentos e examinamos exemplos práticos. Agora vamos falar sobre os mecanismos internos do JavaScript que os fazem funcionar.
Para entender os fechamentos, precisamos lidar com dois conceitos cruciais do JavaScript. Este é o contexto de execução e o ambiente lexical.
Context Contexto de execução
O contexto de execução é um ambiente abstrato no qual o código JavaScript é calculado e executado. Quando o código global é executado, isso acontece dentro do contexto de execução global. O código da função é executado dentro do contexto da execução da função.
Em algum momento, o código pode ser executado em apenas um contexto de execução (JavaScript é uma linguagem de programação de thread único). Esses processos são gerenciados usando a chamada pilha de chamadas.
A pilha de chamadas é uma estrutura de dados organizada de acordo com o princípio LIFO (Last In, First Out - Last In, First Out). Novos elementos podem ser colocados apenas no topo da pilha e apenas elementos podem ser removidos.
O contexto de execução atual sempre estará no topo da pilha e, quando a função atual sair, seu contexto de execução será extraído da pilha e o controle será transferido para o contexto de execução, localizado abaixo do contexto dessa função na pilha de chamadas.
Considere o exemplo a seguir para entender melhor o que são o contexto de execução e a pilha de chamadas:
Exemplo de contexto de execuçãoQuando esse código é executado, o mecanismo JavaScript cria um contexto de execução global para a execução do código global e, quando encontra uma chamada para a
first()
função
first()
, cria um novo contexto de execução para essa função e o coloca no topo da pilha.
A pilha de chamadas desse código é assim:
Pilha de chamadasQuando a execução da
first()
função
first()
é concluída, seu contexto de execução é recuperado da pilha de chamadas e o controle é transferido para o contexto de execução abaixo, ou seja, para o contexto global. Depois disso, o código restante no escopo global será executado.
Environment ambiente lexical
Cada vez que o mecanismo JS cria um contexto de execução para executar uma função ou código global, também cria um novo ambiente lexical para armazenar as variáveis declaradas nessa função durante sua execução.
O ambiente lexical é uma estrutura de dados que armazena informações sobre a correspondência de identificadores e variáveis. Aqui, "identificador" é o nome de uma variável ou função e "variável" é uma referência a um objeto (isso inclui funções) ou um valor de um tipo primitivo.
O ambiente lexical contém dois componentes:
- Um registro de ambiente é o local em que as declarações de variáveis e funções são armazenadas.
- Referência ao ambiente externo - um link que permite acessar o ambiente lexical externo (principal). Esse é o componente mais importante que precisa ser tratado para entender os fechamentos.
Conceitualmente, o ambiente lexical é assim:
lexicalEnvironment = { environmentRecord: { <identifier> : <value>, <identifier> : <value> } outer: < Reference to the parent lexical environment> }
Dê uma olhada no seguinte trecho de código:
let a = 'Hello World!'; function first() { let b = 25; console.log('Inside first function'); } first(); console.log('Inside global execution context');
Quando o mecanismo JS cria um contexto de execução global para a execução de código global, ele também cria um novo ambiente lexical para armazenar variáveis e funções declaradas no escopo global. Como resultado, o ambiente lexical do escopo global ficará assim:
globalLexicalEnvironment = { environmentRecord: { a : 'Hello World!', first : < reference to function object > } outer: null }
Observe que a referência ao ambiente lexical externo (
outer
) está definida como
null
, pois o escopo global não possui um ambiente lexical externo.
Quando o mecanismo cria um contexto de execução para a
first()
função
first()
, ele também cria um ambiente lexical para armazenar as variáveis declaradas nessa função durante sua execução. Como resultado, o ambiente lexical da função ficará assim:
functionLexicalEnvironment = { environmentRecord: { b : 25, } outer: <globalLexicalEnvironment> }
O link para o ambiente lexical externo da função é definido como
<globalLexicalEnvironment>
, pois no código-fonte o código da função está no escopo global.
Observe que, quando a função termina seu trabalho, seu contexto de execução é recuperado da pilha de chamadas, mas seu ambiente lexical pode ser excluído da memória ou pode permanecer lá. Depende se em outros ambientes lexicais existem referências a esse ambiente lexical na forma de links para um ambiente lexical externo.
Análise detalhada de exemplos de trabalho com fechamentos
Agora que nos preparamos para o conhecimento do contexto de execução e do ambiente lexical, retornaremos ao encerramento e analisaremos com mais profundidade os mesmos fragmentos de código que já examinamos.
▍ Exemplo No. 1
Dê uma olhada neste snippet de código:
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
Quando a função
person()
é executada, o mecanismo JS cria um novo contexto de execução e um novo ambiente lexical para essa função. Terminando o trabalho, a função retorna a função
displayName()
, uma referência a esta função é gravada na variável
peter
.
Seu ambiente lexical ficará assim:
personLexicalEnvironment = { environmentRecord: { name : 'Peter', displayName: < displayName function reference> } outer: <globalLexicalEnvironment> }
Quando a função
person()
sai, seu contexto de execução é exibido na pilha. Mas seu ambiente lexical permanece na memória, pois existe um link para ele no ambiente lexical de sua função interna
displayName()
. Como resultado, as variáveis declaradas nesse ambiente lexical permanecem disponíveis.
Quando a função
peter()
é chamada (a variável correspondente armazena uma referência à função
displayName()
), o mecanismo JS cria um novo contexto de execução e um novo ambiente lexical para esta função. Esse ambiente lexical terá a seguinte aparência:
displayNameLexicalEnvironment = { environmentRecord: { } outer: <personLexicalEnvironment> }
Não há variáveis na função
displayName()
, portanto, seu registro de ambiente estará vazio. Durante a execução desta função, o mecanismo JS tentará encontrar a variável
name
no ambiente lexical da função.
Como a pesquisa não pode ser encontrada no ambiente lexical da função
displayName()
, a pesquisa continuará no ambiente lexical externo, ou seja, no ambiente lexical da função
person()
, que ainda está na memória. Lá, o mecanismo encontra a variável desejada e exibe seu valor no console.
▍ Exemplo No. 2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
O ambiente lexical da função
getCounter()
terá a seguinte aparência:
getCounterLexicalEnvironment = { environmentRecord: { counter: 0, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
Essa função retorna uma função anônima atribuída à variável
count
.
Quando a função
count()
é executada, seu ambiente lexical fica assim:
countLexicalEnvironment = { environmentRecord: { } outer: <getCountLexicalEnvironment> }
Ao executar esta função, o sistema procurará a variável do
counter
em seu ambiente lexical. Nesse caso, novamente, o registro do ambiente da função está vazio; portanto, a pesquisa pela variável continua no ambiente lexical externo da função.
O mecanismo localiza a variável, a exibe no console e incrementa a variável do
counter
, que é armazenada no ambiente lexical da função
getCounter()
.
Como resultado, o ambiente lexical da função
getCounter()
após a primeira chamada para a função
count()
terá a seguinte aparência:
getCounterLexicalEnvironment = { environmentRecord: { counter: 1, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
Cada vez que a função
count()
é chamada, o mecanismo JavaScript cria um novo ambiente lexical para essa função e incrementa a variável do
counter
, o que leva a alterações no ambiente lexical da função
getCounter()
.
Sumário
Neste artigo, falamos sobre o que são fechamentos e resolvemos os mecanismos JavaScript subjacentes a eles. Os fechamentos são um dos conceitos fundamentais mais importantes sobre JavaScript, e todo desenvolvedor de JS deve entendê-los. Entender os fechamentos é uma das etapas para criar aplicativos eficazes e de alta qualidade.
Caros leitores! Se você tem experiência no desenvolvimento de JS, compartilhe exemplos práticos de uso de fechamentos com iniciantes.
