Por que não conseguimos comparar arrays e objetos com ===?
Olá pessoal, decidi escrever esse artigo para tirar uma dúvida que sempre tive, que era o motivo pelo qual não conseguimos comparar arrays e objetos com ===. Então estudei um pouco e isso foi o que encontrei sobre como o Javascript funciona por debaixo dos panos.
Javascript tem 5 tipos de dados que são passados por valor: boolean, null, undefined, String e Number. São chamados de tipos primitivos
Javascript tem 3 tipos de dados que são passados por referência: Array, Function e Object. Tecnicamente, todos os 3 são objetos, então podemos nos referir aos 3 como objetos. São chamados de tipos não primitivos
Tipos Primitivos (Valor)
Quando você declara uma variável de um tipo primitivo, ela armazena o valor do tipo.
const nome = 'João'
const idade = 25
| Variável | Valor |
|---|---|
| nome | 'João' |
| idade | 25 |
Simples assim.
Tipos Não Primitivos (Referência)
Variáveis que recebem um valor não primitivo são atribuídas a uma referência para esse valor. Essa referência aponta para a localização do objeto na memória. Nesse caso, a variável vai armazenar a referência, não o valor em si.
Por exemplo:
const frutas = []
frutas.push('Banana')
A representação do que acontece nessas linhas é o seguinte:
| Variável | Valor | Endereço | Objeto |
|---|---|---|---|
| frutas | H001 | H001 | [] |
| Variável | Valor | Endereço | Objeto |
|---|---|---|---|
| frutas | H001 | H001 | ['Banana'] |
O que acontece quando declaramos uma variável pela referência?
Quando uma variável de referência é copiada para outra usando =, estamos copiando o endereço de referência. Objetos são copiados pela referência ao invés do valor.
const frutas = ['Banana']
const frutasAmarelas = frutas
| Variável | Valor | Endereço | Objeto |
|---|---|---|---|
| frutas | H001 | H001 | ['Banana'] |
| frutasAmarelas | H001 |
Então, se adicionarmos uma fruta no array de frutasAmarelas, estaremos adicionando também no array de frutas, já que eles compartilham a mesma referência.
frutasAmarelas.push('Abacaxi')
| Variável | Valor | Endereço | Objeto |
|---|---|---|---|
| frutas | H001 | H001 | ['Banana', 'Abacaxi'] |
| frutasAmarelas | H001 |
Redeclarando uma variável de referência
Quando reassinamos uma variável de referência, ela substitui a referência e não o valor.
let pessoa = { nome: 'João' }
pessoa = { nome: 'Maria' }
Em memória, acontece isso:
| Variável | Valor | Endereço | Objeto |
|---|---|---|---|
| pessoa | H002 | H001 | { nome: 'João' } |
| H002 | { nome: 'Maria' } | ||
| A referência antiga ainda é mantida no endereço, porém substituímos o valor do endereço pelo novo. |
Por que não é possível comparar arrays com ===?
Quando === é usado em um tipo de referência, isto é dados não primitivos, a condição só vai retornar verdadeiro se eles tiverem a mesma referência, porque é isso que o === checa em dados não primitivos, checa a referência, não o valor
const arr1 = ['1']
const arr2 = arr1
console.log(arr1 === arr2) // Verdadeiro
Porém, se eles forem objetos distintos, por mais que tenham o mesmo valor, as referências são diferentes, então a comparação retorna false
const arr1 = ['1']
const arr2 = ['2']
console.log(arr1 === arr2) // Falso
Parâmetros nas funções
Quando passamos valores primitivos por parâmetros, o comportamento é simples, ele assina o valor no parâmetro como se eu estivesse usando um =
const nota = 10
const media = 2
function dividir(x, y) {
return x / y
}
const notaMedia = dividir(nota, media)
Nesse caso, nota é igual a 10, media é igual a 2, x é igual a 10, e y é igual a 2. Isso porque ele copiou o valor para a variável de parâmetro.
| Variável | Valor | Endereço | Objeto |
|---|---|---|---|
| nota | 10 | ||
| media | 2 | ||
| dividir | H001 | H001 | function(x,y) |
| x | 10 | ||
| y | 2 |
Funções Puras
Funções puras são funções que não afetam nada fora do escopo dela mesma e todas as variáveis usando dentro dela são limpas assim que a função retorna o valor.
O exemplo de função acima dividir é uma função pura.
Funções Impuras
Funções impuras são funções que podem receber objetos e mudar o estado dele fora do escopo. Porque ela recebe a referência do valor, e caso essa referência seja alterada dentro da função, também será alterada fora da função. Por exemplo:
function trocarNome(pessoa) {
pessoa.nome = 'Maria'
return pessoa
}
const joao = {
nome: 'João'
}
const maria = trocarNome(joao);
console.log(joao) // Maria
console.log(maria) // Maria
Isso acontece porque dentro da função, estamos alterando o valor que está na referência, que no caso é a mesma tanto para joao tanto para maria porque o parâmetro recebe a referência e não o valor.
Ok, mas como transformamos isso em uma função pura? Da mesma forma que o Array.map e o Array.filter faz. Criamos uma cópia do objeto dentro da função, manipulamos e retornamos essa cópia, assim não alteramos a referência original.
Refatorando então, ficaria dessa forma:
function trocarNome(pessoa) {
const novaPessoa = JSON.parse(JSON.stringify(pessoa))
novaPessoa.nome = 'Maria'
return novaPessoa
}
const joao = {
nome: 'João'
}
const maria = trocarNome(joao);
console.log(joao) // Joao
console.log(maria) // Maria
Nessa função, transformamos o objeto em uma string (tipo primitivo) e depois transformamos ele em um objeto novamente, essa operação faz com que uma nova referência seja criada, porque quando o objeto se torna string, ele se torna um valor primitivo e depois o objeto é criado a partir desse valor e não da referência do original.
Conclusão
- Tipos Primitivos: Armazenam o valor diretamente na variável.
- Tipos Não Primitivos: Armazenam uma referência ao valor, não o valor em si.
- Cópia de Referências: Objetos compartilhando a mesma referência refletem mudanças mutuamente.
- Operador
===: Compara referências, não valores, em tipos não primitivos. - Funções Puras: Não alteram valores fora de seu escopo.
- Funções Impuras: Podem modificar o estado de objetos compartilhados por referência.
- Tornando funções puras: Criar cópias dos objetos evita alterações em referências originais.
E se quiser saber mais sobre como as variáveis são armazenadas no Javascript, leia este outro artigo que escrevi, explicando sobre Call Stack e Memory Heap.
Espero que tenha gostado, até breve!