O arroz e feijão do JavaScript por baixo dos panos.

Gerenciamento de memória global, contexto de execução e callstack

Angél
7 min readSep 4, 2020

Neste artigo, apresentarei um breve resumo do último meetup que ofereci junto com a {reprograma}: “O arroz e feijão do JavaScript por baixo dos panos”, onde falei dos conceitos básicos, porém essenciais, de JavaScript (JS).

Ao falar sobre os princípios do JS podemos resumi-los em duas características.

A primeira característica do JS é que no momento da execução, o interpretar lê o código linha por linha (e chamamos isso de thread de execução). Já a segunda característica, é que a interpretação dessa leitura é guardada na memória.

Javascript não funciona magicamente. Entender a execução de dessa linguagem permite que as habilidades do engenheiro se desenvolvam e que ele consiga resolver problemas de alto nível, tornando seu dia a dia mais eficiente.

Dito isso, podemos começar observando um exemplo.

Esse é um pequeno código que retorna como resultado a multiplicação de dois números. Iremos a destrinchar a execução do código exatamente como o JavaScript faz nos bastidores.

Quando um código em JavaScript é executado pela primeira vez, um contexto de execução global é criado, que pode ser acessado de qualquer outro contexto em seu programa. Neste exemplo, ele está sendo representado pela borda verde.

Contexto de execução global de borda verde e novo contexto de execução de borda laranja.

No contexto global, à medida que a execução do código avança de acordo com a linha de execução do código, variáveis ​​como strings, números, arrays e objetos vão sendo identificados e devidamente guardados na memória do computador.

Quando o compilador (intérprete) de JavaScript encontra o chamado de uma função “function ()”, ele cria um novo contexto de execução e avalia ali dentro o código dessa função. Esse contexto está sendo representado na imagem de cima pela borda laranja, criando um escopo privado, o que quer dizer que nada declarado fora poderá acessar essa função atual.

Este será o novo ambiente ou escopo no qual o código da função atual será avaliado.

“Thread de execução global” no detalhe:

Linha 1 do código executado.

Linha 1: Declara uma constante chamada “numBase” e atribuída o valor 10;

Linha 2 do código executado.

Linha 2: declara uma constante chamada “num” e atribui o valor 5;

Linha 4 do código executado.

Linha 4: Declare a função denominada “function vezesPorBase (numInput) {}”, que possui um parâmetro (numInput). Não vamos executar nenhum código para essa função agora. O código para esta função é empacotado e armazenado na memória do computador como uma sequência de caracteres. E esta função é representada na memória global com uma pequena caixa contendo um f, “-> | f | ->”.

Linha 9: Declara uma constante denominada “vezes” e atribui o valor “vezesPorBase (num)”, que o intérprete JavaScript identifica e reconhece como uma função que já existe na memória, porque reconheceu os parênteses “()”, identificador de que é invocada uma função. Nesta invocação da função “vezesPorBase (num)”, o valor da constante “num” declarada anteriormente na linha 2 será enviada como argumento dessa função.

A constante “vezes” é declarada na memória com valor undefined, pois está aguardando o valor a ser atribuído que será o resultado da execução da função “vezesPorBase (num)”. Continuando com a thread de execução da linha 9, vemos que acaba de ser entrelaçado com a função (vezesPorBase) (numInput) {}, declarada na linha 4 anterior, ao fazer esta invocação é criado um novo “Contexto de Execução”. Lembre-se de que na função original da linha 4, possui um parâmetro chamado (numInput), este é um placeholder que está esperando por qualquer valor conhecido como argumento. Durante a invocação, este parâmetro (numInput) recebe como argumento o valor da constante (sum), guardado em memória.

Novo contexto de execução gerado pela invocação da função vezesPorBase (num)

Depois que esse contexto de execução é criado, o interpretador JavaScript entra em um primeiro estágio, antes de qualquer código ser executado. Ele cria variáveis, funções e argumentos, e também determina o escopo da “Cadeia de escopo” e o valor “this”, onde se refere ao objeto de onde a função foi chamada.

Já o segundo estágio é a execução do código, onde você atribui valores e interpreta as referências às funções.

Linha 4: Declara uma variável chamada (result) e atribui a ela o resultado da multiplicação entre o valor da constante “numBase” e a constante (numInput).

Linha 5: Declara o retorno em que retorna o valor atribuído à variável “resultado” para o contexto pai onde foi invocada a função (no caso, o contexto global).

Valor retorno atribuído à variável vezes guardado à memória global.

O valor de retorno da função “vezesPorBase ()” ficará guardado na memória global com a respectiva etiqueta “vezes”.

De volta ao ambiente global, o interpretador JavaScript retoma a leitura do código na linha 8, isso porque ele funciona com apenas uma thread por vez. Ou seja, isso significa que apenas uma coisa pode acontecer por vez no mundo do Javascript, com outras ações ou eventos sendo enfileirados. Num seguinte post, vamos ver em detalhe como isso funciona.

Linha 10: Declare uma constante denominada “outraVezes” e atribua a ela a função “vezesPorBase (10)” envie como argumento o valor 10.

Linha 10 do código executado.

Com esta nova declaração vemos a sua semelhança com o processo que foi executado na linha 9.

Novamente invocamos a função “vezesPorBase (numInput)” declarada na linha 4, e o parâmetro “numInput” recebe como argumento o valor 10.

Novo contexto de execução gerado pela invocação da função vezesPorBase (10)

Criamos um novo “Contexto de Execução”, onde resolveremos o valor da constante “outraVezes”. É importante destacar que este novo contexto de execução que se abre a partir da declaração da variável “outraVezes” nada tem a ver com o contexto de execução anterior, que resolveu o valor da constante “vezes”.

Valor retorno atribuído à variável outraVezes guardado à memória global.

Uma vez que a operação armazenada na constante “resultado” tenha sido executada, ela será retornada e guardada na memória global com a respectiva etiqueta “outraVezes”.

Resultado da memória global ao finalizar a execução do código.

Voltando ao contexto global, o interpretador de JavaScript percebe que não tem mais linhas de código para ler e termina sua execução.

CallStack

Seguindo o propósito deste artigo e para facilitar a compreensão deste material de estudo, passamos ao que é chamado de pilha de execução (callstack).

Como vimos anteriormente, o interpretador de JavaScript é implementado como um único thread, isso significa que o JavaScript mantém o controle unicamente da função que está se executando atualmente.

O diagrama a seguir é uma visão abstrata da execução síncrona da pilha durante o exemplo anterior.

Considere que quando todo o nosso código é executado pela primeira vez, ele é criado dentro de uma função com o rótulo global, localizada na posição atual da pilha. Lembre-se de que existe apenas um contexto global durante a interpretação de todo o código.

Seguindo o fluxo de execução do código, entra-se na função interna “vezesPorBase (5)”, que cria um novo contexto de execução que é colocado no topo da pilha existente, no topo do contexto global e uma vez que a função completa a execução do contexto de execução atual, ela será puxada do topo da pilha, retornando o controle para o contexto inferior “Global ()” na pilha atual.

Neste segundo diagrama está representado quando se invoca “vezesPorBase (10)”. Aqui, repete-se a sequência anterior, deixando claro que o processo do diagrama anterior nada tem a ver com o atual. Mesmo que esteja a ser invocada a mesma função cada uma tem sua identidade e seu momento diferente.

Isso deixa claro que os contextos de execução são infinitos, e uma vez finalizada a execução do código, este contexto é eliminado e retorna ao contexto global.

Lembremos que o interpretador de javascript reconhece escopos, criando uma cadeia entre as funções, deixando-as estaticamente unidas, isso quer dizer que cada invocação de uma nova função cria um novo contexto de execução que contém os valores das variáveis ​​no contexto atual.

Para finalizar, gostaria de reforçar que entender esses dois fundamentos básicos do JavaScript, o contexto de execução e o callstack, vão ajudar você na sua carreira como pessoa programadora.

--

--