O Problema das Cópias

Imagine que você está trabalhando em um sistema de cadastro de usuários. Você tem um usuário registrado e precisa criar uma cópia dele para fazer algumas alterações de teste. Você faz o seguinte:

let usuarioOriginal = {
  nome: "Maria Silva",
  email: "maria@email.com",
  idade: 28
};

let usuarioTeste = usuarioOriginal;
usuarioTeste.nome = "Maria Santos"; // Modificando o teste

console.log(usuarioOriginal.nome); // O que você espera ver aqui?

Surpresa! O resultado é "Maria Santos". Mas por quê? Você modificou apenas o usuarioTeste, certo?

Este é um dos conceitos mais importantes e, ao mesmo tempo, mais confusos do JavaScript: referências de objetos. Vamos desvendar esse mistério!

Conceito Fundamental: Referências vs Valores

Tipos Primitivos: Cópia por Valor

Primeiro, vamos entender como funcionam os tipos primitivos (numbers, strings, booleans):

let a = 10;
let b = a; // Copia o VALOR de a para b

a = 20;
console.log(a); // 20
console.log(b); // 10 (não foi afetado!)

Quando copiamos um valor primitivo, criamos uma cópia independente. Modificar uma variável não afeta a outra.

Por quê? Primitivos são armazenados diretamente na memória. Cada variável tem sua própria cópia do valor.

Objetos: Cópia por Referência

Agora, veja o que acontece com objetos:

let objetoA = { valor: 10 };
let objetoB = objetoA; // Copia a REFERÊNCIA, não o objeto!

objetoA.valor = 20;
console.log(objetoA.valor); // 20
console.log(objetoB.valor); // 20 (foi afetado!)

Por quê essa diferença?

Objetos em JavaScript são armazenados na memória de forma diferente. Quando você cria um objeto, o JavaScript armazena o objeto em um local da memória e a variável guarda apenas um "endereço" (referência) para esse local.

Analogia do Mundo Real:

Pense em um objeto como uma casa e a variável como um endereço:

  • Primitivos: Quando você copia um número, é como tirar uma fotografia. Você tem duas fotos independentes.
  • Objetos: Quando você copia um objeto, é como anotar o endereço de uma casa. Você tem dois papéis com o mesmo endereço, mas ainda é a mesma casa. Se você pintar a casa usando um endereço, ela estará pintada quando você for lá usando o outro endereço também!

Entendendo Referências na Prática

Exemplo 1: Múltiplas Variáveis, Mesmo Objeto

let pessoa = {
  nome: "João",
  idade: 30
};

let usuario = pessoa;
let admin = pessoa;

// Todas as três variáveis apontam para o MESMO objeto
admin.nome = "Carlos";

console.log(pessoa.nome);  // "Carlos"
console.log(usuario.nome); // "Carlos"
console.log(admin.nome);   // "Carlos"

Exemplo 2: Comparação de Objetos

let a = { nome: "Ana" };
let b = { nome: "Ana" };

console.log(a === b); // false

let c = a;
console.log(a === c); // true

Por que a === b é false?

Mesmo que os objetos tenham exatamente as mesmas propriedades e valores, eles são objetos diferentes na memória. O operador === compara as referências, não o conteúdo.

É como ter duas casas idênticas em endereços diferentes. São casas diferentes, mesmo que sejam iguais por dentro!

Por que a === c é true?

Porque c recebeu a referência de a. Eles apontam para o mesmo objeto na memória.

Clonagem de Objetos: Criando Cópias Independentes

Agora que entendemos o problema, como criamos uma cópia verdadeiramente independente de um objeto?

Método 1: Clonagem Manual

A forma mais básica é copiar propriedade por propriedade:

let usuario = {
  nome: "João",
  idade: 30,
  ativo: true
};

let clone = {}; // Novo objeto vazio

// Copiando todas as propriedades
for (let chave in usuario) {
  clone[chave] = usuario[chave];
}

clone.nome = "Pedro";
console.log(usuario.nome); // "João" (não foi afetado!)
console.log(clone.nome);   // "Pedro"

Vantagem: Você tem controle total sobre o que está copiando. Desvantagem: Trabalhoso e propenso a erros.

Método 2: Object.assign()

O JavaScript oferece um método mais elegante:

let usuario = {
  nome: "João",
  idade: 30
};

let clone = Object.assign({}, usuario);

clone.nome = "Pedro";
console.log(usuario.nome); // "João"
console.log(clone.nome);   // "Pedro"

Como funciona:

  • Object.assign(destino, fonte1, fonte2, ...)
  • Copia todas as propriedades das fontes para o destino
  • Retorna o objeto destino

Mesclando objetos:

let usuario = { nome: "João" };
let permissoes1 = { podeVer: true };
let permissoes2 = { podeEditar: true };

let usuarioCompleto = Object.assign({}, usuario, permissoes1, permissoes2);

console.log(usuarioCompleto);
// { nome: "João", podeVer: true, podeEditar: true }

Método 3: Spread Operator (...)

A forma mais moderna e concisa:

let usuario = {
  nome: "João",
  idade: 30
};

let clone = { ...usuario };

clone.nome = "Pedro";
console.log(usuario.nome); // "João"
console.log(clone.nome);   // "Pedro"

O operador spread ... "espalha" todas as propriedades do objeto original no novo objeto.

Vantagem: Sintaxe limpa e moderna.

O Problema da Clonagem Superficial

Todos os métodos acima realizam clonagem superficial (shallow cloning). Isso significa que copiam apenas o primeiro nível de propriedades.

E qual é o problema?

let usuario = {
  nome: "João",
  endereco: {
    rua: "Rua Principal",
    numero: 123
  }
};

let clone = { ...usuario };

clone.endereco.rua = "Rua Secundária";

console.log(usuario.endereco.rua); // "Rua Secundária" (foi afetado!)

O que aconteceu?

A propriedade endereco é um objeto. Ao copiar superficialmente, copiamos apenas a referência ao objeto endereco, não o objeto em si!

Visualizando:

usuario ──┐
          ├─→ nome: "João"
          ├─→ endereco ──┐
                         ├─→ rua: "Rua Principal"
                         └─→ numero: 123

clone ────┤              │
          ├─→ nome: "João"
          └─→ endereco ──┘

Ambos os objetos (usuario e clone) compartilham o mesmo objeto endereco!

Clonagem Profunda: A Solução Completa

Para copiar objetos aninhados, precisamos de clonagem profunda (deep cloning).

Método 1: structuredClone() (Moderno e Recomendado)

let usuario = {
  nome: "João",
  endereco: {
    rua: "Rua Principal",
    numero: 123,
    cidade: {
      nome: "São Paulo",
      estado: "SP"
    }
  }
};

let cloneProfundo = structuredClone(usuario);

cloneProfundo.endereco.rua = "Rua Secundária";
cloneProfundo.endereco.cidade.nome = "Rio de Janeiro";

console.log(usuario.endereco.rua);         // "Rua Principal" (não afetado!)
console.log(usuario.endereco.cidade.nome); // "São Paulo" (não afetado!)

Vantagens do structuredClone():

  • Faz clonagem profunda completa
  • Funciona com estruturas complexas
  • Preserva tipos especiais (Date, Map, Set, etc.)

Quando usar: Sempre que você precisar copiar objetos com propriedades aninhadas!

Método 2: JSON.parse(JSON.stringify())

Uma técnica antiga, mas ainda útil:

let usuario = {
  nome: "João",
  endereco: {
    rua: "Rua Principal"
  }
};

let clone = JSON.parse(JSON.stringify(usuario));

clone.endereco.rua = "Rua Secundária";
console.log(usuario.endereco.rua); // "Rua Principal"

Como funciona:

  1. JSON.stringify(usuario): Converte o objeto em texto JSON
  2. JSON.parse(...): Reconstrói um novo objeto a partir do texto

⚠️ Limitações importantes:

  • Perde funções
  • Perde undefined
  • Perde símbolos
  • Não funciona com referências circulares
  • Perde tipos especiais (Date vira string)
let objeto = {
  data: new Date(),
  funcao: function() { return "oi"; },
  indefinido: undefined
};

let clone = JSON.parse(JSON.stringify(objeto));

console.log(clone.data);       // String, não Date!
console.log(clone.funcao);     // undefined (perdido!)
console.log(clone.indefinido); // undefined (perdido!)

Quando usar: Apenas para objetos simples com dados primitivos.

Mutabilidade e Objetos const

Um conceito importante relacionado a referências é a mutabilidade com const:

const usuario = {
  nome: "João",
  idade: 30
};

// Isso funciona! 
usuario.nome = "Pedro";
usuario.idade = 35;
console.log(usuario); // { nome: "Pedro", idade: 35 }

// Isso NÃO funciona!
usuario = { nome: "Carlos" }; // TypeError: Assignment to constant variable

Por quê?

O const protege a referência, não o conteúdo do objeto.

Analogia: É como ter um endereço escrito com caneta permanente. Você não pode mudar o endereço escrito, mas pode reformar a casa naquele endereço!

const endereco = "Rua A, 123"; // Não pode mudar o endereço
// Mas a casa no endereço pode ser pintada, reformada, etc.

Quando Usar Cada Técnica

Use Spread Operator ou Object.assign() quando:

  • O objeto tem apenas um nível de propriedades
  • As propriedades são valores primitivos
  • Você precisa mesclar objetos
let config = { tema: "escuro", idioma: "pt" };
let novaSessao = { ...config, usuario: "João" };

Use structuredClone() quando:

  • O objeto tem propriedades aninhadas
  • Você quer uma cópia completamente independente
  • O objeto contém tipos especiais (Date, Map, Set)
let perfil = {
  dados: { nome: "Ana" },
  configuracoes: { tema: { cor: "azul" } }
};
let copia = structuredClone(perfil);

Use JSON.parse/stringify quando:

  • Você tem certeza que o objeto contém apenas dados serializáveis
  • Você está trabalhando com APIs que usam JSON
  • Você precisa compatibilidade com navegadores antigos
let dadosAPI = { nome: "João", idade: 30, cidade: "SP" };
let backup = JSON.parse(JSON.stringify(dadosAPI));

Exemplos Práticos

Exemplo 1: Sistema de Carrinho de Compras

// Estado inicial do carrinho
let carrinhoOriginal = {
  itens: [
    { produto: "Notebook", preco: 3000, quantidade: 1 },
    { produto: "Mouse", preco: 50, quantidade: 2 }
  ],
  total: 3100
};

// Usuário quer simular adicionar um item sem modificar o carrinho real
let carrinhoSimulacao = structuredClone(carrinhoOriginal);

carrinhoSimulacao.itens.push({ 
  produto: "Teclado", 
  preco: 200, 
  quantidade: 1 
});
carrinhoSimulacao.total += 200;

console.log(carrinhoOriginal.itens.length);  // 2 (não foi afetado)
console.log(carrinhoSimulacao.itens.length); // 3

Exemplo 2: Histórico de Alterações

let documento = {
  titulo: "Meu Documento",
  conteudo: "Texto inicial",
  versao: 1
};

// Salvando versão anterior antes de modificar
let historico = [structuredClone(documento)];

documento.conteudo = "Texto modificado";
documento.versao = 2;

historico.push(structuredClone(documento));

// Posso acessar versões anteriores
console.log(historico[0].conteudo); // "Texto inicial"
console.log(historico[1].conteudo); // "Texto modificado"

Exemplo 3: Configurações Padrão

const configPadrao = {
  tema: "claro",
  notificacoes: true,
  idioma: "pt-BR"
};

function criarPerfilUsuario(personalizacoes) {
  // Mescla configurações padrão com personalizações
  return {
    ...configPadrao,
    ...personalizacoes
  };
}

let perfil1 = criarPerfilUsuario({ tema: "escuro" });
let perfil2 = criarPerfilUsuario({ idioma: "en-US" });

console.log(perfil1); // { tema: "escuro", notificacoes: true, idioma: "pt-BR" }
console.log(perfil2); // { tema: "claro", notificacoes: true, idioma: "en-US" }

Armadilhas Comuns e Como Evitá-las

Armadilha 1: Modificação Acidental

// ❌ ERRADO
function adicionarDesconto(produto) {
  produto.preco = produto.preco * 0.9;
  return produto;
}

let produtoOriginal = { nome: "Notebook", preco: 3000 };
let produtoComDesconto = adicionarDesconto(produtoOriginal);

console.log(produtoOriginal.preco); // 2700 (foi modificado!)
// ✅ CORRETO
function adicionarDesconto(produto) {
  let produtoNovo = { ...produto };
  produtoNovo.preco = produtoNovo.preco * 0.9;
  return produtoNovo;
}

let produtoOriginal = { nome: "Notebook", preco: 3000 };
let produtoComDesconto = adicionarDesconto(produtoOriginal);

console.log(produtoOriginal.preco); // 3000 (não foi modificado!)

Armadilha 2: Clonagem Superficial Inadequada

// ❌ ERRADO para objetos aninhados
let configuracao = {
  usuario: { nome: "Ana" },
  preferencias: { tema: "escuro" }
};

let copia = { ...configuracao };
copia.preferencias.tema = "claro";

console.log(configuracao.preferencias.tema); // "claro" (foi modificado!)
// ✅ CORRETO
let copia = structuredClone(configuracao);
copia.preferencias.tema = "claro";

console.log(configuracao.preferencias.tema); // "escuro" (não foi modificado!)

Por Que Isso É Importante?

Entender referências e cópias é crucial porque:

  1. Previne Bugs Difíceis de Detectar: Modificações inesperadas são uma fonte comum de bugs
  2. Gerenciamento de Estado: Essencial para frameworks modernos (React, Vue)
  3. Performance: Saber quando copiar vs quando compartilhar referências
  4. Código Previsível: Funções que não modificam seus parâmetros são mais confiáveis
  5. Programação Funcional: Base para imutabilidade e programação funcional

Próximos passos

Na próxima aula começaremos nosso projeto final! Nele iremos:

  • Fazer um projeto completo de gestão de livros de uma biblioteca
  • Aplicar todos os conceitos aprendidos em um projeto real
  • Ver como é organiado um projeto real

Lembre-se: na dúvida sobre se uma modificação vai afetar o original, faça um teste rápido no console ou use structuredClone() para garantir uma cópia independente!