O que você é, fechamentos em JavaScript?

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.
MDN

Se 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 1

Estamos 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 2

Isso 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 3

Entramos 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:

  1. 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
  2. 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 4

Vemos 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 5

Um 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 7

Como 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 :)

Source: https://habr.com/ru/post/pt459166/


All Articles