Neste artigo, tentarei analisar em detalhes o mecanismo para implementar fechamentos em JavaScript. Para isso, usarei o navegador Chrome.
Vamos começar com a definição:
Encerramentos são funções que referenciam variáveis independentes (livres). Em outras palavras, a função definida no fechamento 'lembra' o ambiente em que foi criada.
MDNSe algo não estiver claro para você nesta definição, não será assustador. Apenas continue a ler.
Estou profundamente convencido de que entender algo é mais fácil e rápido com exemplos específicos.
Portanto, sugiro pegar um pedaço de código e acompanhá-lo com o intérprete do começo ao fim em etapas e resolver o que está acontecendo.
Então, vamos começar:
Figura 1Estamos no contexto global da chamada, é Global (também conhecida como Janela no navegador) e vemos que a função principal já está no contexto atual e está pronta para funcionar.
Figura 2Isso acontece porque todas as Declarações de Função (doravante denominadas DF) sempre são exibidas em qualquer contexto, são imediatamente inicializadas e prontas para o trabalho. O mesmo acontece com as variáveis declaradas via var, apenas seus valores são inicializados como indefinidos.
Também é importante entender que o JavaScript também "aumenta" as variáveis declaradas via let e const. A única diferença é que não os inicializa como var ou como FD. Portanto, quando tentamos acessá-los antes da inicialização, obtemos um erro de referência.
Além disso, em geral, vemos uma propriedade oculta internamente
[[Scopes]] - esta é uma lista de contextos externos aos quais a principal tem acesso. No nosso caso, Global está lá, pois main é lançado em um contexto global.
O fato de que em JavaScript a inicialização de referências ao ambiente externo ocorre no momento em que a função foi criada, e não no momento da execução, sugere que JS é uma linguagem com escopo estático. E isso é importante.
Vá em frente:
Figura 3Entramos na função principal e a primeira coisa que chama sua atenção é o objeto Local (na especificação - localEnv). Aí vemos
a ,
uma vez que essa variável é declarada via
var e 'apareceu', bem, e por tradição vemos todos os 3 FDs (foo, bar, baz). Agora vamos descobrir de onde tudo veio.
Quando qualquer contexto é iniciado, é iniciada a operação abstrata
NewDeclarativeEnvironment , que permite inicializar o
LexicalEnvironment (daqui em diante LE) e o
VariableEnvironment . Além disso,
NewDeclarativeEnvironment usa 1 argumento - o LE externo, a fim de criar os [[Scopes]] dos quais falamos acima. LE é uma API que permite definir o relacionamento entre identificadores e variáveis individuais, funções. LE consiste em 2 componentes:
- Record Environment - um registro de ambiente que permite determinar o relacionamento entre identificadores e o que está disponível para nós no contexto de chamada atual
- Link para LE externo. Cada função possui uma propriedade [[Scopes]] interna quando é criada .
Ambiente variável - na maioria das vezes é o mesmo que LE. A diferença entre os dois é que o valor de VariableEnvironment nunca muda e o LE pode mudar durante a execução do código. Para simplificar o entendimento, proponho combinar esses componentes em um - LE.
Também no local atual, isso ocorre devido ao fato de que
ThisBinding foi chamado - este também é um método abstrato que inicializa isso no contexto atual.
Obviamente, cada DF recebeu imediatamente [[Scopes]]:
Figura 4Vemos que todos os DFs receberam em [[Scopes]] uma matriz de [Closure main, Global], o que é lógico.
Também na figura, vemos a
pilha de chamadas - essa é uma estrutura de dados que funciona com o princípio do LIFO - a última a entrar. Como o JavaScript é de thread único, apenas um contexto pode ser executado por vez. No nosso caso, este é o contexto da função principal. Cada nova chamada de função cria um novo contexto, que é empilhado.
No topo da pilha está sempre o contexto de execução atual. Depois que a função conclui sua execução e o intérprete sai dela, o contexto de chamada é removido da pilha. É tudo o que precisamos saber sobre a Pilha de chamadas neste artigo :)
Resumimos o que aconteceu no contexto atual:
- No momento da criação, o principal recebeu [[Scopes]] com links para o ambiente externo
- O intérprete entrou no corpo da função principal
- A pilha de chamadas obteve o contexto de execução principal
- Isso inicializou
- LE inicializado
De fato, a parte mais difícil acabou. Prosseguimos para a próxima etapa no código:
Agora precisamos chamar baz para obter o resultado.
Figura 5Um novo contexto de chamada baz foi adicionado à pilha de chamadas. Vemos que um novo objeto Closure apareceu. Aqui temos o que está disponível para nós em [[Scopes]]. Então chegamos ao ponto. Este é o encerramento. Como você vê na
Figura 4, o Closure (principal) vai primeiro na lista de contextos de 'backup' no baz. Mais uma vez não há mágica.
Vamos chamar foo:
Figura 6É importante saber que, não importa onde chamemos foo, ele sempre seguirá os identificadores indefinidos em sua cadeia [[Scopes]]. Ou seja, em main e depois em Global, se não for encontrado em main.
Depois de executar foo, ela retornou o valor e seu contexto saltou da pilha de chamadas.
Passamos para a chamada para a função bar. No contexto da execução de barras, há uma variável com o mesmo nome que a variável no LE foo -
a . Mas, como você já adivinhou, isso não afeta nada. foo ainda terá o valor de seus [[Scopes]].
O local da chamada não afeta o escopo, apenas o local de criação
logachyova
Figura 7Como resultado, o baz retornará 300 e será jogado para fora da pilha de chamadas. Então o mesmo acontecerá com o contexto principal, nosso fragmento de código terminará de executar.
Resumimos:
- Durante a criação da função, [[Scopes]] é definido . Isso é muito importante para entender os fechamentos, pois o intérprete segue imediatamente esses links ao procurar valores
- Em seguida, quando essa função é chamada, um contexto de execução ativo é criado, colocado na pilha de chamadas
- ThisBinding é executado e está definido para o contexto atual
- O LE é inicializado e todos os argumentos das funções, variáveis declaradas por meio de var e FD, ficam disponíveis. Além disso, se houver variáveis declaradas via let ou const, elas também serão adicionadas ao LE
- Se o intérprete não encontrar nenhum identificador no contexto atual, então [[Scopes]] será usado para pesquisas adicionais, que serão ordenadas sucessivamente. Se o valor for encontrado, o link para ele cai no objeto Closure especial. Ao mesmo tempo, para cada contexto em que o atual é fechado, um encerramento separado é criado com as variáveis necessárias
- Se o valor não for encontrado em nenhum escopo, incluindo Global, um ReferenceError será retornado.
Isso é tudo!
Espero que este artigo tenha sido útil para você e agora você entenda como o mecanismo de bloqueio no JavaScript funciona.
Tchau :) E até breve. Curta e assine o meu canal :)