Olá, pessoal!
Estava tentando entender com mais detalhes, como funciona os tipos de dados em Javascript e principalmente como é a alocação de memória para cada um destes tipos. O JavaScript por ser uma linguagem de alto nível, muitos dos detalhes técnicos, como o gerenciamento de memória, são abstraídos do desenvolvedor. No entanto, acredito que compreender o mínimo de como funciona a alocação e a desalocação de memória é essencial para escrevermos um código mais eficiente e evitar problemas, como vazamentos de memória.
Fui atrás de materiais para esclarecer melhor esse tema, mas tive dificuldade em encontrar em um unico lugar algo que explicasse esse processo de forma mais aprofundada. Por isso, resolvi compilar em um unico material o que consegui entender.
Espero que possa ajudar quem possa ter as mesmas dúvidas que eu!
Tipos de Dados em JavaScript: Imutáveis e Mutáveis
Para entendermos como o JavaScript lida com alocação e desalocação de variáveis, primeiro precisamos entender quais são os os dois tipos de dados disponiveis no JavaScript. Os tipos primitivos (Imutáveis) e os tipos não primitivos (Mutáveis).
Tipos Primitivos (Imutáveis)
Os tipos primitivos são valores simples e imutáveis. Isso significa que, uma vez criados, eles não podem ser alterados diretamente. Quando você modifica um valor primitivo, na verdade, você está descartando o valor original e alocando um novo valor na memória.
No exemplo abaixo, o valor original “Hello” não é modificado; em vez disso, um novo espaço de memória é alocado com o conteudo “Hello World” e o espaço de memória que tinha o “Hello” é totalmente liberado.
let str = "Hello";
str = str + " World"; // Um novo valor "Hello World" é criado, e "Hello" é descartado.
Os tipos Primitivos no JavaScript são:
- Number
- String
- Boolean
- null
- undefined
- Symbol
- BigInt
Tipos Não Primitivos (Mutáveis)
Os tipos não primitivos são os Objects, Arrays e Functions. Eles são mutáveis, o que significa que seus valores internos podem ser alterados sem criar uma nova referência.
No exemplo abaixo, a referencia do objeto na memória stack permanece a mesa, mas seu conteúdo interno é modificado.
const obj = { name: "Alice" };
obj.name = "Bob"; // Alteração do valor interno é permitida
Os tipos não primitivos no JavaScript são:
- Objects ({})
- Arrays ([])
- Functions
Alocação de Memória: Stack e Heap
Agora que já entendemos os tipos de dados no JavaScript, vamos entender como esses dados são armazenados na memória.
O motor do JavaScript utiliza duas áreas principais de memória para alocar variáveis: stack e heap.
Stack
A memória stack é usada para armazenar valores primitivos e referências a objetos no heap.
É uma estrutura de dados linear e limitada em tamanho, ideal para armazenamento temporário de variáveis locais e chamadas de funções.
Operações no stack são extremamente rápidas devido ao seu modo de funcionamento: LIFO (Last In, First Out).
No exemplo abaixo, a e b armazenam cópias independentes do valor 10.
let a = 10; // Alocado diretamente no stack
let b = a; // Nova cópia do valor é criada no stac
Heap
A memória heap é usada para armazenar Objects
, Arrays
e Functions
.
Ao contrário do stack, o heap é uma área de memória não estruturada e dinâmica, o que o torna ideal para armazenar dados mais complexos e de tamanho variável.
Os objetos armazenados no heap são acessados por referências que por sua vez são armazenadas no stack.
No exemplo abaixo, a variável obj está alocada na Heap e na stack contém uma referência do endereço de acesso ao local do objeto na heap.
const obj = { name: "Alice" }; // O objeto está no heap
Tabela de referência
Fiz uma tabelinha para facilitar o entendimento de qual é a mutabilidade e o local de alocação dos tipos de dados no JavaScript:
Tipo | Mutabilidade | Local de Alocação |
---|---|---|
Number | Imutavel | Stack |
String | Imutavel | Heap (Por ser de tamanho variável) |
Boolean | Imutavel | Stack |
null/undefined | Imutavel | Stack |
Object | Mutável | Heap (Referência na stack) |
Array | Mutável | Heap (Referência na stack) |
Function | Mutável | Heap (Referência na stack) |
Desalocação de Memória e Garbage Collector
Uma das vantagens do JavaScript é que ele gerencia a memória automaticamente. A desalocação de memória é realizada por meio de um processo conhecido como Garbage Collection (Coleta de Lixo) que por sua vez, utiliza um algoritmo chmado Mark-and-Sweep para saber quando uma alocação de memoria pode ser liberada. Sendo bem simplista, o processo é mais ou menos assim:
Marcar: O motor identifica todas as variáveis que ainda são acessíveis a partir das “raízes” (como o escopo global e a pilha de execução).
Varredura: Objetos que não estão marcados como acessíveis são considerados “inacessíveis” e têm sua memória liberada.
Problemas Comuns Relacionados à Memória
Apesar de ter todo o processo de gerenciamento de memória automatizado, ainda sim é possível encontrar problemas de memória no JavaScript. Veja abaixo um exemplo comum de referencia circular e que impede o carbage collector de liberar a memória desses objetos:
let obj1 = {};
let obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
Um outro exemplo clássico, são os listeners
não removidos que o Garbage Colletor também não consegue identificar e consequentemente não consegue liberar a memória utiliza por esses objetos:
window.addEventListener("resize", someFunction);
Conclusão
Tentei simplificar ao máximo esse tema, que pode ser um pouco complexo, mas espero que vocês tenham compreendido como o JavaScript aloca e desaloca memória. Além disso, espero que tenham percebido o quanto esse conhecimento é essencial para escrever códigos mais eficientes e evitar problemas de desempenho. Entender a diferença entre stack e heap, assim como entre valores primitivos e objetos mutáveis, é crucial para prever como as variáveis são tratadas internamente.
Embora o Garbage Collector automatize a limpeza da memória, adotar boas práticas, como evitar referências desnecessárias e liberar recursos explicitamente, é fundamental para prevenir problemas como vazamentos de memória em aplicações reais.
Se você está desenvolvendo aplicações complexas ou críticas, utilizar ferramentas como o Chrome DevTools para análise de memória pode oferecer uma vantagem significativa.
Se quiser ver um vídeo legal sobre esse assunto, eu recomendo o “Desvendando Call Stack e Memory Heap de forma Simples e Prática” do Eduardo Motta, ele explica de uma maneira bem simples e didática.
E se você também tiver alguma dúvida ou algo a acrescentar, sinta-se à vontade para deixar seu comentário.
Obrigado!
Fontes:
Conteúdo interessante e pouco abordado.
Parabéns!
Obrigado Ricardo!