É importante lembrarmos que Javascript é uma linguagem single thread, ou seja, é capaz de executar apenas uma instrução por vez. Logo, existe apenas um call stack e um heap.
Heap
Heap é um componente da engine Javascript, uma estrutura em formato de árvore que armazena os valores não primitivos. A ideia é que qualquer dado seja obtido na menor quantidade de tempo.
Exemplo do heap da aplicação Google Chat
Para entender o gerenciamento de memória do Javascript vejamos o exemplo:
let num1 = 1 let num2 = num1 num1 += 1 console.log(num2)
A pergunta é: qual será o valor exibido no console?
A resposta certa é: 1. Por que?
- Quando a variável num1 é iniciada, o JS cria um identificador único e aloca em um endereço na memória (ex. A001), o valor da variável também é armazenado nesse endereço de memória e fica no call stack.
- Quando especificamos que num2 é igual a num1, significa que num2 será atribuída com o valor de num1, e não seu endereço de memória.
- Ao alterar o valor de num1 para 2, o JS aloca um novo endereço de memória, e guarda o valor 2 nesse novo endereço (ex: A002). Isso acontece pois tipos primitivos são imutáveis.
Sabemos que não podemos alterar variáveis
const
, e isso não acontece pelo valor, e sim por não podermos alterar seu endereço na memória. Porém, podemos por exemplo adicionar propriedades ou incluir elementos em arrays e objetos declarados com const
.Como o heap armazena os tipos não primitivos, quando criamos uma variável como array ou objeto, o JS cria um identificador único, aloca o endereço na memória e guarda o valor com essa referência no heap. Quando alteramos esse array ou objeto, o endereço na memória não é alterado, e sim o valor armazenado no heap.
Execution Context
Um execution context é um conceito abstrato de onde o código é computado e executado. Sempre que um código é executado em Javascript, é feito dentro de um execution context. Podemos entender como o ambiente (environment) que o código será executado, e ambiente seria o valor do
this
, as variáveis, objetos e funções que o engine tem acesso em um período do tempo específico. Existem basicamente 3 tipos de execution context.- Global Execution Context: é o contexto padrão ou base, o código que não está dentro de qualquer função está nesse contexto. Duas coisas acontecem aqui: a criação do objeto global window (nos browsers); atribui esse objeto global à variável
this
; aloca memória para as funções e variáveis. Apenas um contexto global pode existir em uma aplicação.
- Functional Execution Context: sempre que uma função é executada, é criado um contexto para essa função
- Eval Function Execution Context: um código executado dentro da função
eval()
também tem seu próprio contexto. Porém o uso deeval
é desestimulado: eval() - JavaScript | MDN (mozilla.org)
Um execution context possui alguns componentes de estado que descrevem e rastreiam a execução de determinado código, tendo uma estrutura similar a ilustrada abaixo:
ExecutionContext = { // Estados obrigatórios: CodeEvaluationState: `qualquer estado necessário para executar, suspender e reproduzir o código associado ao contexto atual`, Function: `se o contexto atual estiver computando o código de uma função, então o valor deste componente é a própria função, caso contrário é null` Realm: `agrupa o objeto global, todo o código que está associado ao escopo global e outras informações adicionais`, ScriptOrModule: `referência ao objeto do módulo ou script que originou a execução do código atual, caso não exista referência o valor é null`, // Estados adicionais: LexicalEnvironment: ``, VariableEnvironment: ``, PrivateEnvironment: `objeto com os membros privados das classes, null caso não existam classes`, }
O execution context atual pode ser suspenso para dar lugar a um outro execution context, como por exemplo acontece quando usamos
async/await
.Vamos dar mais ênfase ao LexicalEnvironment e VariableEnvironment para podermos explicar mais sobre escopo. Ambos são compostos de um EnvironmentRecord, então antes de seguirmos, vamos entender do que se trata esse tal referido.
EnvironmentRecord
Um Environment Record é uma estrutura que mapeia identificadores com seus valores e funções, baseando-se na estrutura léxica encadeada do código JS.
Todo Environment Record possui um campo OuterEnv, que pode ser null, caso seja o escopo top-level ou uma referência para o Environment Record pai. Dessa forma conseguimos ter acesso a variáveis que estão no escopo pai caso não existam no escopo atual.
LexicalEnvironment
O Lexical Environment é um Environment Record usado para armazenar as variáveis declaradas com `
let
` e `const
` dentro do escopo em questão a ser executado. Caso seja uma função, o campo OuterEnv irá referenciar o escopo superior.VariableEnvironment
Também é um Lexical Environment porém a diferença é que este é usado para armazenar variáveis criadas com
var
.Creation & Execution Phase
Durante a fase de criação, a engine cria o Environment Record e atribui um valor default (undefined para var e uninitialized para let/const), e as declarações de funções também são colocadas em memória.
Já na fase de execução, a engine executa linha por linha e atribui os valores reais para as variáveis que já estão na memória.
Call Stack
Um call stack é uma estrutura de dados implementada pela engine Javascript, em formato de fila LIFO (last in first out) que gerencia a execução das funções em um script, quais outras funções foram chamadas a partir desta, qual função está em execução no momento e etc...
Quando uma função é invocada, a função, seu parâmetros e variáveis são inseridos no call stack a partir da engine, para formar um stack frame, que é um endereço de memória no call stack. Os dados de tipo primitivo são armazenados no call stack.
function greeting() { // [1] Some code here sayHi(); // [2] Some code here } function sayHi() { return "Hi!"; } // Invoke the `greeting` function greeting(); // [3] Some code here
- Quando o código acima é carregado no browser, a engine Javascript cria um global execution context.
- Todas as funções são ignoradas até a invocação da função
greeting
- Cria um contexto de execução e adiciona a função
greeting
no call stack
Call Stack: greeting
- Executa todo o código dentro da função
greeting
até a invocação da funçãosayHi
- Cria um contexto de execução e adiciona a função
sayHi
no call stack
Call Stack: sayHi - greeting
- Executa todo o código dentro da função
sayHi
- Retorna ao ponto que chamou a função
sayHi
e continua executando a funçãogreeting
- Exclui a função
sayHi
do call stack
Call Stack: greeting
- Ao término da execução da função
greeting
, retorna para quem a invocou para continuar executando o código JS
- Exclui a função
greeting
do call stack
Call Stack: vazia
Stack Overflow
Cada engine pode estipular o tamanho máximo do call stack, e ao atingirmos esse limite, a engine interrompe a execução do código e dispara uma exceção.