Uma das diferenças fundamentais entre objetos e primitivos é que os objetos são armazenados e copiados “por referência”, enquanto os valores primitivos: strings, números, booleanos, etc – são sempre copiados “como um valor inteiro”.
Isso é fácil de entender se olharmos um pouco mais detalhadamente o que acontece quando copiamos um valor.
Vamos começar com um primitivo, como uma string.
Aqui colocamos uma cópia da message
na phrase
:
deixe mensagem = "Olá!"; deixe frase = mensagem;
Como resultado temos duas variáveis independentes, cada uma armazenando a string "Hello!"
.
Um resultado bastante óbvio, certo?
Os objetos não são assim.
Uma variável atribuída a um objeto armazena não o objeto em si, mas seu “endereço na memória” – em outras palavras, “uma referência” a ele.
Vejamos um exemplo de tal variável:
deixe usuário = { nome: "João" };
E aqui está como ele é realmente armazenado na memória:
O objeto é armazenado em algum lugar da memória (à direita da imagem), enquanto a variável user
(à esquerda) possui uma “referência” a ele.
Podemos pensar em uma variável de objeto, como user
, como uma folha de papel com o endereço do objeto nela.
Quando realizamos ações com o objeto, por exemplo, pegamos uma propriedade user.name
, o mecanismo JavaScript olha o que está naquele endereço e executa a operação no objeto real.
Agora, aqui está por que isso é importante.
Quando uma variável de objeto é copiada, a referência é copiada, mas o objeto em si não é duplicado.
Por exemplo:
deixe usuário = {nome: "John" }; deixe admin = usuário; //copia a referência
Agora temos duas variáveis, cada uma armazenando uma referência ao mesmo objeto:
Como você pode ver, ainda existe um objeto, mas agora com duas variáveis que o referenciam.
Podemos usar qualquer uma das variáveis para acessar o objeto e modificar seu conteúdo:
deixe usuário = {nome: 'John' }; deixe admin = usuário; admin.name = 'Pete'; // alterado pela referência "admin" alerta(usuário.nome); // 'Pete', as alterações são vistas a partir da referência "usuário"
É como se tivéssemos um armário com duas chaves e usássemos uma delas ( admin
) para entrar nele e fazer alterações. Então, se posteriormente usarmos outra chave ( user
), ainda abriremos o mesmo gabinete e poderemos acessar o conteúdo alterado.
Dois objetos são iguais apenas se forem o mesmo objeto.
Por exemplo, aqui a
e b
fazem referência ao mesmo objeto, portanto são iguais:
deixe a = {}; seja b = uma; //copia a referência alerta(a == b); // verdadeiro, ambas as variáveis fazem referência ao mesmo objeto alerta(a === b); // verdadeiro
E aqui dois objetos independentes não são iguais, embora sejam parecidos (ambos estão vazios):
deixe a = {}; seja b = {}; // dois objetos independentes alerta(a == b); // falso
Para comparações como obj1 > obj2
ou para uma comparação com um primitivo obj == 5
, os objetos são convertidos em primitivos. Estudaremos como funcionam as conversões de objetos em breve, mas para dizer a verdade, tais comparações são necessárias muito raramente – geralmente elas aparecem como resultado de um erro de programação.
Objetos Const podem ser modificados
Um efeito colateral importante do armazenamento de objetos como referências é que um objeto declarado como const
pode ser modificado.
Por exemplo:
const usuário={ nome: "João" }; usuário.nome = "Pete"; // (*) alerta(usuário.nome); // Pete
Pode parecer que a linha (*)
causaria um erro, mas isso não acontece. O valor de user
é constante, deve sempre fazer referência ao mesmo objeto, mas as propriedades desse objeto podem ser alteradas.
Em outras palavras, o const user
gera um erro apenas se tentarmos definir user=...
como um todo.
Dito isto, se realmente precisarmos criar propriedades de objetos constantes, também é possível, mas usando métodos totalmente diferentes. Mencionaremos isso no capítulo Sinalizadores e descritores de propriedade.
Portanto, copiar uma variável de objeto cria mais uma referência ao mesmo objeto.
Mas e se precisarmos duplicar um objeto?
Podemos criar um novo objeto e replicar a estrutura do existente, iterando suas propriedades e copiando-as no nível primitivo.
Assim:
deixe usuário = { nome: "João", idade: 30 }; deixe clonar = {}; //o novo objeto vazio // vamos copiar todas as propriedades do usuário nele for (deixe digitar o usuário) { clone[chave] = usuário[chave]; } // agora o clone é um objeto totalmente independente com o mesmo conteúdo clone.name = "Pete"; // alterou os dados nele alerta(usuário.nome); // ainda John no objeto original
Também podemos usar o método Object.assign.
A sintaxe é:
Object.assign(destino, ...fontes)
O primeiro argumento dest
é um objeto de destino.
Outros argumentos são uma lista de objetos de origem.
Ele copia as propriedades de todos os objetos de origem no destino dest
e depois os retorna como resultado.
Por exemplo, temos um objeto user
, vamos adicionar algumas permissões a ele:
deixe usuário = {nome: "John" }; deixe permissões1 = {canView: true}; deixe permissões2 = {canEdit: true}; // copia todas as propriedades de permissões1 e permissões2 para o usuário Object.assign(usuário, permissões1, permissões2); // agora usuário = {nome: "John", canView: true, canEdit: true } alerta(usuário.nome); // John alerta(usuário.canView); // verdadeiro alerta(user.canEdit); // verdadeiro
Se o nome da propriedade copiada já existir, ele será substituído:
deixe usuário = {nome: "John" }; Object.assign(usuário, {nome: "Pete" }); alerta(usuário.nome); // agora usuário = { nome: "Pete" }
Também podemos usar Object.assign
para realizar uma simples clonagem de objeto:
deixe usuário = { nome: "João", idade: 30 }; deixe clonar = Object.assign({}, usuário); alerta(clone.nome); // John alerta(clone.idade); //30
Aqui ele copia todas as propriedades do user
para o objeto vazio e o retorna.
Existem também outros métodos de clonagem de um objeto, por exemplo, usando a sintaxe de propagação clone = {...user}
, abordada posteriormente neste tutorial.
Até agora assumimos que todas as propriedades do user
são primitivas. Mas as propriedades podem ser referências a outros objetos.
Assim:
deixe usuário = { nome: "João", tamanhos: { altura: 182, largura: 50 } }; alerta(usuário.tamanhos.altura); //182
Agora não basta copiar clone.sizes = user.sizes
, pois user.sizes
é um objeto, e será copiado por referência, então clone
e user
compartilharão os mesmos tamanhos:
deixe usuário = { nome: "João", tamanhos: { altura: 182, largura: 50 } }; deixe clonar = Object.assign({}, usuário); alerta(user.sizes === clone.sizes); // verdadeiro, mesmo objeto // tamanhos de compartilhamento de usuário e clone usuário.tamanhos.largura = 60; // altera uma propriedade de um lugar alerta(clone.tamanhos.largura); // 60, pega o resultado do outro
Para corrigir isso e fazer com que user
e clone
sejam objetos verdadeiramente separados, devemos usar um loop de clonagem que examine cada valor de user[key]
e, se for um objeto, então replique sua estrutura também. Isso é chamado de “clonagem profunda” ou “clonagem estruturada”. Existe um método StructureClone que implementa clonagem profunda.
A chamada structuredClone(object)
clona o object
com todas as propriedades aninhadas.
Veja como podemos usá-lo em nosso exemplo:
deixe usuário = { nome: "João", tamanhos: { altura: 182, largura: 50 } }; deixe clone = estruturadoClone(usuário); alerta(user.sizes === clone.sizes); // falso, objetos diferentes // usuário e clone não têm nenhuma relação agora usuário.tamanhos.largura = 60; // altera uma propriedade de um lugar alerta(clone.tamanhos.largura); // 50, não relacionado
O método structuredClone
pode clonar a maioria dos tipos de dados, como objetos, matrizes, valores primitivos.
Ele também oferece suporte a referências circulares, quando uma propriedade de objeto faz referência ao próprio objeto (diretamente ou por meio de uma cadeia ou referências).
Por exemplo:
deixe usuário = {}; // vamos criar uma referência circular: //user.me faz referência ao próprio usuário usuário.me = usuário; deixe clone = estruturadoClone(usuário); alerta(clone.me === clone); // verdadeiro
Como você pode ver, clone.me
faz referência ao clone
, não ao user
! Portanto, a referência circular também foi clonada corretamente.
Embora haja casos em que structuredClone
falha.
Por exemplo, quando um objeto possui uma propriedade de função:
//erro estruturadoClone({ f: função() {} });
As propriedades de função não são suportadas.
Para lidar com casos tão complexos, podemos precisar usar uma combinação de métodos de clonagem, escrever código personalizado ou, para não reinventar a roda, usar uma implementação existente, por exemplo _.cloneDeep(obj) da biblioteca JavaScript lodash.
Os objetos são atribuídos e copiados por referência. Em outras palavras, uma variável armazena não o “valor do objeto”, mas uma “referência” (endereço na memória) para o valor. Portanto, copiar tal variável ou passá-la como argumento de função copia essa referência, não o objeto em si.
Todas as operações por meio de referências copiadas (como adicionar/remover propriedades) são executadas no mesmo objeto único.
Para fazer uma “cópia real” (um clone) podemos usar Object.assign
para a chamada “cópia superficial” (objetos aninhados são copiados por referência) ou uma função de “clonagem profunda” structuredClone
ou usar uma implementação de clonagem personalizada, como como _.cloneDeep(obj).