Imutabilidade
O Javascript possui dois grupos de tipos de dados: os tipos primitivos e os tipos não primitivos, compostos por: Boolean, Null, Undefined, BigInt, String, Number e Symbol.
Os tipos primitivos são imutáveis por natureza. Ao realizar qualquer alteração em um tipo primitivo, o próprio interpretador do Javascript, em tempo de execução, se encarregará de alocar um novo endereço de memória para o resultado transformado:
let num1 = 0
let num2 = num1
num2++
console.log(num1) // 0
console.log(num2) // 1
Quando a variável num1 foi criada, o interpretador do Javascript criou um identificador único para ela. Alocou um endereço na memória (por exemplo, EJ0001), e armazenou o valor "1" no endereço alocado.
Quando definimos que num2 é igual a num1, o que o Javascript fez por debaixo dos panos foi:
- Definiu um identificador único para a variável: num2.
- Apontou o identificador para o mesmo endereço de memória da variável num1 (ex: EJ0001).
Mas na linha 3, no momento em que incrementamos o valor da variável num2, o Javascript alocou uma nova unidade de memória (por exemplo: XJ0501) , armazenou o valor da expressão: "num2++" e apontou o identificador da variável num2 para este novo endereço de memória. Portanto, teremos duas variáveis apontando para dois endereços de memória distintos.
Este comportamento é diferente se for realizado com objetos e arrays, que são considerados tipos não primitivos. Estas estruturas de dados são armazenadas em outra região dentro da arquitetura da linguagem. Esta região chama-se Heap, e ela é capaz de armazenar dados não ordenados que podem crescer e diminuir dinamicamente como objetos e arrays.
Quando declaramos uma variável "person", e atribuímos a ela um tipo não primitivo como um objeto vazio:
const person = {}
É isso que acontece por debaixo dos panos:
- Um identificador é criado com o nome "person".
- Um endereço de memória é alocado em tempo de execução na Stack (ex: CM9323).
- É armazenado neste endereço criado na Stack, uma referência para um endereço de memória alocado na Heap.
- O endereço de memória na Heap armazenará o valor que foi atribuído a "person", neste caso um objeto vazio.
A partir deste ponto, qualquer alteração que fizermos no objeto person acontecerá na Heap, e todas as variáveis que apontam para o mesmo endereço de memória de person, serão afetadas pela mudança.
const person = {}
const clonedPerson = person
person.name = 'John'
console.log(person.name) // 'John'
console.log(clonedPerson) // 'John'
Este é o comportamento nativo do Javascript na hora de lidar com a memória. Mas por que ele se comporta desse jeito? Justamente para economizar memória e ganharmos performance.
Imagine um cenário hipotético onde temos um simples array com 1 milhão de itens. Se copiarmos este array 10 vezes, teremos 10 arrays distintamente desconectados com 1 milhão de itens cada, totalizando em 10 milhões de itens. Sendo que 9 milhões destes, são cópias idênticas do primeiro array.
Mas, em certos casos, principalmente quando trabalhamos seguindo o princípio da imutabilidade, um dos pilares do paradigma funcional, queremos ter um comportamento diferente. Gostaríamos de copiar todos os valores que estão contidos dentro de endereços de memória, para novos endereços de memória. Esta prática nos livra do efeito colateral presente na concorrência de acesso a dados: Se dois locais distintos da aplicação concorrem para acessarem e subscreverem um mesmo recurso, no mesmo instante de tempo, um dos locais que espera receber uma "bola", pode obter um "quadrado" devido a atualização que o outro local fez anteriormente.