Contexto de execução JavaScript e pilha de chamadas

Se você é um desenvolvedor JavaScript ou deseja se tornar um, isso significa que você precisa entender os mecanismos internos para executar o código JS. Em particular, é absolutamente necessário entender o contexto de execução e a pilha de chamadas para dominar outros conceitos de JavaScript, como aumentar variáveis, escopo e fechamento. O material, cuja tradução publicamos hoje, é dedicado ao contexto de execução e à pilha de chamadas em JavaScript.



Contexto de execução


O contexto de execução é, em termos simplificados, um conceito que descreve o ambiente em que o código JavaScript é executado. O código é sempre executado dentro de um contexto.

▍ Executar tipos de contexto


JavaScript tem três tipos de contextos de execução:

  • Contexto de execução global. Este é o contexto básico de execução padrão. Se algum código não estiver em nenhuma função, esse código pertencerá ao contexto global. O contexto global é caracterizado pela presença de um objeto global, que, no caso do navegador, é o objeto de window e o fato de que this aponta para esse objeto global. Um programa pode ter apenas um contexto global.
  • Contexto de execução da função. Cada vez que uma função é chamada, um novo contexto é criado para ela. Cada função tem seu próprio contexto de execução. Um programa pode simultaneamente ter muitos contextos para executar funções. Ao criar um novo contexto para a execução de uma função, ela passa por uma certa sequência de etapas, que discutiremos abaixo.
  • O contexto de execução da função eval . O código executado dentro da função eval também possui seu próprio contexto de execução. No entanto, a função eval é usada muito raramente, portanto, aqui não falaremos sobre esse contexto de execução.

Pilha de execução


A pilha de execução, também chamada pilha de chamadas, é a pilha LIFO usada para armazenar contextos de execução criados durante a execução do código.

Quando o mecanismo JS começa a processar o script, ele cria um contexto de execução global e o coloca na pilha atual. Quando um comando para chamar uma função é detectado, o mecanismo cria um novo contexto de execução para essa função e o coloca no topo da pilha.

O mecanismo executa uma função cujo contexto de execução está no topo da pilha. Quando a função é concluída, seu contexto é removido da pilha e o controle é transferido para o contexto que está no elemento anterior da pilha.

Vamos explorar essa idéia com o seguinte exemplo:

 let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); console.log('Inside Global Execution Context'); 

Veja como a pilha de chamadas mudará quando esse código for executado.


Status da pilha de chamadas

Quando o código acima é carregado no navegador, o mecanismo JavaScript cria um contexto de execução global e o coloca na pilha de chamadas atual. Ao fazer uma chamada para a first() função first() , o mecanismo cria um novo contexto para essa função e a coloca no topo da pilha.

Quando a second() função second() é chamada a partir da first() , um novo contexto de execução é criado para essa função e também é empurrado para a pilha. Após a second() função second() concluir seu trabalho, seu contexto é removido da pilha e o controle é transferido para o contexto de execução localizado na pilha abaixo dela, ou seja, para o contexto da first() função first() .

Quando a first() função first() sai, seu contexto é exibido na pilha e o controle é transferido para o contexto global. Depois que todo o código é executado, o mecanismo recupera o contexto de execução global da pilha atual.

Sobre como criar contextos e executar código


Até agora, falamos sobre como o mecanismo JS gerencia contextos de execução. Agora vamos falar sobre como os contextos de execução são criados e o que acontece com eles após a criação. Em particular, estamos falando sobre o estágio de criação do contexto de execução e o estágio de execução do código.

▍ Estágio de criação do contexto de execução


Antes de o código JavaScript ser executado, o contexto de execução é criado. No processo de sua criação, três ações são executadas:

  1. Este valor é determinado e this (esta ligação) é vinculado.
  2. O componente LexicalEnvironment é criado.
  3. O componente VariableEnvironment é criado.

Conceitualmente, o contexto de execução pode ser representado da seguinte maneira:

 ExecutionContext = { ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... }, } 

Essa ligação


No contexto de execução global, this contém uma referência ao objeto global (como já mencionado, no navegador, é um objeto de window ).

No contexto da execução da função, o valor this depende de como a função foi chamada. Se for chamado como método de um objeto, o valor this vinculado a esse objeto. Em outros casos, this vinculado a um objeto global ou definido como undefined (no modo estrito). Considere um exemplo:

 let foo = { baz: function() { console.log(this); } } foo.baz();    // 'this'    'foo',    'baz'              //    'foo' let bar = foo.baz; bar();       // 'this'     window,                 //      

Ambiente lexical


De acordo com a especificação ES6, Lexical Environment é um termo usado para definir o relacionamento entre identificadores e variáveis ​​e funções individuais com base na estrutura do aninhamento lexical do código ECMAScript. O ambiente lexical consiste em um registro de ambiente e uma referência ao ambiente lexical externo, que pode ser null .

Simplificando, um ambiente lexical é uma estrutura que armazena informações sobre a correspondência de identificadores e variáveis. Aqui, por "identificador" significa o nome de uma variável ou função e por "variável" é uma referência a um objeto específico (incluindo uma função) ou a um valor primitivo.

No ambiente lexical, existem dois componentes:

  1. Registro de um ambiente. É aqui que as declarações de variáveis ​​e funções são armazenadas.
  2. Link para o ambiente externo. A presença desse link indica que o ambiente lexical tem acesso ao ambiente lexical pai (escopo).

Existem dois tipos de ambientes lexicais:

  1. O ambiente global (ou o contexto de execução global) é um ambiente lexical que não possui um ambiente externo. A referência do ambiente global ao ambiente externo é null . No ambiente global (no registro do ambiente), estão disponíveis entidades de linguagem internas (como Object , Array etc.) associadas ao objeto global, também existem variáveis ​​globais definidas pelo usuário. O valor this neste ambiente aponta para um objeto global.
  2. O ambiente da função na qual, no registro do ambiente, as variáveis ​​declaradas pelo usuário são armazenadas. A referência ao ambiente externo pode indicar um objeto global e uma função externa à função em questão.

Existem dois tipos de registros de ambiente:

  1. Um registro de ambiente declarativo que armazena variáveis, funções e parâmetros.
  2. Um registro de objeto do ambiente usado para armazenar informações sobre variáveis ​​e funções em um contexto global.

Como resultado, em um ambiente global, um registro de ambiente é representado por um registro de ambiente de objeto e, em um ambiente de função, por um registro de ambiente declarativo.

Observe que no ambiente da função, o registro declarativo do ambiente também contém o objeto de arguments , que armazena a correspondência entre os índices e os valores dos argumentos passados ​​para a função e informações sobre o número de tais argumentos.

O ambiente lexical pode ser representado como o seguinte pseudocódigo:

 GlobalExectionContext = { LexicalEnvironment: {   EnvironmentRecord: {     Type: "Object",     //        }   outer: <null> } } FunctionExectionContext = { LexicalEnvironment: {   EnvironmentRecord: {     Type: "Declarative",     //        }   outer: <        > } } 

Variáveis ​​de ambiente


Um ambiente variável também é um ambiente lexical cujo registro de ambiente armazena as ligações criadas usando os comandos VariableStatement no contexto de execução atual.

Como o ambiente das variáveis ​​também é um ambiente lexical, ele possui todas as propriedades descritas acima do ambiente lexical.

No ES6, há uma diferença entre os componentes LexicalEnvironment e VariableEnvironment . Consiste no fato de que o primeiro é usado para armazenar declarações de funções e variáveis ​​declaradas usando as palavras-chave let e const , e o último é usado apenas para armazenar ligações de variáveis ​​declaradas usando a palavra-chave var .

Considere exemplos que ilustram o que acabamos de discutir:

 let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e * f * g; } c = multiply(20, 30); 

Uma representação esquemática do contexto de execução para este código será semelhante a este:

 GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: {   EnvironmentRecord: {     Type: "Object",     //          a: < uninitialized >,     b: < uninitialized >,     multiply: < func >   }   outer: <null> }, VariableEnvironment: {   EnvironmentRecord: {     Type: "Object",     //          c: undefined,   }   outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: {   EnvironmentRecord: {     Type: "Declarative",     //          Arguments: {0: 20, 1: 30, length: 2},   },   outer: <GlobalLexicalEnvironment> }, VariableEnvironment: {   EnvironmentRecord: {     Type: "Declarative",     //          g: undefined   },   outer: <GlobalLexicalEnvironment> } } 

Como você provavelmente notou, as variáveis ​​e constantes declaradas usando as palavras-chave let e const não possuem valores associados, e as variáveis ​​declaradas usando a palavra-chave var são definidas como undefined .

Isso ocorre porque, durante a criação do contexto, o código procura por declarações de variáveis ​​e funções, enquanto as declarações de funções são armazenadas inteiramente no ambiente. Os valores das variáveis, ao usar var , são definidos como undefined e, ao usar let ou const permanecem não inicializados.

É por isso que você pode acessar variáveis ​​declaradas com var antes de serem declaradas (embora sejam undefined ), mas quando você tenta acessar variáveis ​​ou constantes declaradas com let e const executadas antes de serem declaradas, ocorre um erro .

O que acabamos de descrever é chamado de "variáveis ​​de elevação". As declarações variáveis ​​“sobem” para o topo de seu escopo lexical antes de executar operações de atribuir-lhes quaisquer valores.

▍ Estágio de execução do código


Esta é talvez a parte mais simples deste material. Nesta fase, os valores são atribuídos às variáveis ​​e o código é executado.

Observe que, se durante a execução do código, o mecanismo JS não conseguir encontrar o valor da variável declarada usando a palavra-chave let no local da declaração, atribuirá a essa variável o valor undefined .

Sumário


Acabamos de discutir os mecanismos internos para executar o código JavaScript. Embora, para ser um desenvolvedor de JS muito bom, não seja necessário saber tudo isso, se você tiver alguma compreensão dos conceitos acima, isso ajudará você a entender melhor e mais profundamente outros mecanismos da linguagem, como aumentar variáveis, escopo, curto-circuito.

Caros leitores! O que você acha que além do contexto de execução e da pilha de chamadas é útil para os desenvolvedores de JavaScript saberem?

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


All Articles