Objetos são a espinha dorsal do JavaScript moderno. Saber manipulá-los corretamente é essencial para escrever código limpo e previsível. Nesta aula, exploraremos quatro tópicos que vão desde iteração e cópia até controle de mutabilidade. Prepare-se para dominar técnicas que você usará diariamente.

Vamos começar com métodos estáticos que transformam objetos em arrays, passando pelo operador spread que simplifica cópias e mesclagens, depois veremos como congelar objetos para evitar alterações acidentais e, por fim, entenderemos a diferença crucial entre cópia rasa e profunda. Cada seção inclui exemplos práticos para fixar o conteúdo.

Object.keys/values/entries

Esses três métodos estáticos do objeto Object permitem extrair as chaves, valores ou pares chave-valor de um objeto como arrays. Eles são especialmente úteis para iterar sobre objetos, já que objetos não são diretamente iteráveis com for...of (a menos que implementem o protocolo de iteração).

Object.keys(obj) retorna um array com as chaves enumeráveis próprias do objeto. Object.values(obj) retorna os valores correspondentes. Object.entries(obj) retorna um array de arrays, cada subarray com [chave, valor]. Esses métodos ignoram propriedades da cadeia de protótipos e são amplamente usados em combinação com forEach, map, filter, etc.

const pessoa = { nome: 'Ana', idade: 30, cidade: 'São Paulo' };

console.log(Object.keys(pessoa));   // ['nome', 'idade', 'cidade']
console.log(Object.values(pessoa)); // ['Ana', 30, 'São Paulo']
console.log(Object.entries(pessoa));
// [ ['nome', 'Ana'], ['idade', 30], ['cidade', 'São Paulo'] ]

// Iterando com forEach:
Object.entries(pessoa).forEach(([chave, valor]) => {
  console.log(`${chave}: ${valor}`);
});

Spread em objetos

O operador spread (...) foi introduzido no ES2018 para objetos, permitindo copiar propriedades de um objeto para outro de forma concisa. É uma alternativa moderna ao Object.assign. A sintaxe é: { ...objetoFonte }.

O spread cria uma cópia rasa (shallow copy) das propriedades enumeráveis próprias. Também é usado para mesclar objetos: { ...obj1, ...obj2 }. Se houver conflito de chaves, o último objeto sobrescreve o anterior. O spread é muito útil para criar novos objetos sem modificar os originais, seguindo o princípio da imutabilidade.

const dados = { a: 1, b: 2 };
const copia = { ...dados };
console.log(copia); // { a: 1, b: 2 }
console.log(copia === dados); // false (objetos diferentes)

const extras = { b: 3, c: 4 };
const mesclado = { ...dados, ...extras };
console.log(mesclado); // { a: 1, b: 3, c: 4 }

// Cuidado: spread é raso!
const aninhado = { x: { y: 10 } };
const copiaAninhada = { ...aninhado };
copiaAninhada.x.y = 20;
console.log(aninhado.x.y); // 20 (referência compartilhada)

Freeze

Object.freeze(obj) torna um objeto imutável: não é possível adicionar, remover ou modificar propriedades existentes. Além disso, as propriedades tornam-se não configuráveis e não graváveis (writable: false). É uma forma de garantir que um objeto não seja alterado acidentalmente, útil para constantes ou configurações.

Importante: Object.freeze é raso. Se o objeto contiver outros objetos (aninhados), estes ainda podem ser modificados, a menos que também sejam congelados. Para imutabilidade profunda, é necessário congelar recursivamente ou usar bibliotecas como Immutable.js. O método retorna o próprio objeto congelado.

const config = { api: 'https://api.exemplo.com', timeout: 5000 };
Object.freeze(config);
config.timeout = 3000; // Ignorado (modo estrito: TypeError)
console.log(config.timeout); // 5000

// Raso: objetos aninhados não são congelados
const obj = { interno: { valor: 1 } };
Object.freeze(obj);
obj.interno.valor = 2; // Funciona!
console.log(obj.interno.valor); // 2

Cópia rasa vs profunda

Entender a diferença entre cópia rasa (shallow copy) e cópia profunda (deep copy) é crucial para evitar bugs sutis. Uma cópia rasa duplica apenas o primeiro nível do objeto; propriedades que são objetos (ou arrays) permanecem como referências compartilhadas entre o original e a cópia. Já a cópia profunda cria uma nova instância independente para cada nível, incluindo objetos aninhados.

Para cópias rasas, podemos usar spread ({...obj}), Object.assign({}, obj) ou Array.from() para arrays. Para cópias profundas, existem várias abordagens: JSON.parse(JSON.stringify(obj)) (limitado: não suporta funções, undefined, Symbol, datas, etc.), ou funções recursivas manuais, ou bibliotecas como Lodash (_.cloneDeep). Em ambientes modernos, também temos structuredClone() (navegadores e Node.js 17+).

const original = { a: 1, b: { c: 2 } };

// Cópia rasa
const rasa = { ...original };
rasa.b.c = 999;
console.log(original.b.c); // 999 (afetado)

// Cópia profunda com JSON
const profunda = JSON.parse(JSON.stringify(original));
profunda.b.c = 777;
console.log(original.b.c); // 999 (não afetado)

// structuredClone (moderno)
const profunda2 = structuredClone(original);
profunda2.b.c = 555;
console.log(original.b.c); // 999

Boas práticas e observações finais

Sempre prefira imutabilidade quando possível: ao invés de modificar um objeto existente, crie um novo com as alterações (spread, Object.assign). Use Object.freeze para constantes e configurações. Lembre-se de que a cópia rasa é suficiente para objetos planos; para objetos aninhados, avalie se a cópia profunda é necessária. Evite JSON.parse(JSON.stringify) para dados que contenham funções, undefined ou datas; prefira structuredClone ou bibliotecas especializadas.

Referências

Exercícios

  1. Dado o objeto const carro = { marca: 'Toyota', modelo: 'Corolla', ano: 2020 }, use Object.entries para exibir no console cada chave e valor no formato "chave: valor".

    ✓ Resposta:
    const carro = { marca: 'Toyota', modelo: 'Corolla', ano: 2020 };
    Object.entries(carro).forEach(([chave, valor]) => {
      console.log(`${chave}: ${valor}`);
    });
  2. Crie uma cópia rasa do objeto const original = { a: 1, b: { c: 2 } } usando spread. Modifique a propriedade c do objeto aninhado na cópia para 99 e verifique se o original foi alterado. Explique por quê.

    ✓ Resposta:
    const original = { a: 1, b: { c: 2 } };
    const copia = { ...original };
    copia.b.c = 99;
    console.log(original.b.c); // 99
    // O original foi alterado porque o spread é raso: a propriedade b é uma referência para o mesmo objeto.
  3. Use Object.freeze para congelar o objeto const config = { host: 'localhost', port: 8080 }. Tente alterar a propriedade port para 9090. O que acontece? Verifique se o objeto está congelado com Object.isFrozen.

    ✓ Resposta:
    const config = { host: 'localhost', port: 8080 };
    Object.freeze(config);
    config.port = 9090; // Em modo estrito: TypeError; em modo normal: ignorado
    console.log(config.port); // 8080
    console.log(Object.isFrozen(config)); // true
  4. Implemente uma função deepFreeze(obj) que congela recursivamente todas as propriedades de um objeto (incluindo objetos aninhados). Teste com um objeto que contenha outro objeto.

    ✓ Resposta:
    function deepFreeze(obj) {
      Object.freeze(obj);
      for (let chave of Object.keys(obj)) {
        const valor = obj[chave];
        if (valor !== null && (typeof valor === 'object' || typeof valor === 'function')) {
          deepFreeze(valor);
        }
      }
      return obj;
    }
    
    const obj = { a: 1, b: { c: 2 } };
    deepFreeze(obj);
    obj.b.c = 99; // Ignorado
    console.log(obj.b.c); // 2
  5. Dado o objeto const usuario = { nome: 'João', endereco: { cidade: 'Rio', uf: 'RJ' } }, crie uma cópia profunda usando structuredClone e altere a cidade na cópia para 'Niterói'. Verifique se o original não foi alterado.

    ✓ Resposta:
    const usuario = { nome: 'João', endereco: { cidade: 'Rio', uf: 'RJ' } };
    const copia = structuredClone(usuario);
    copia.endereco.cidade = 'Niterói';
    console.log(usuario.endereco.cidade); // 'Rio' (não alterado)
    console.log(copia.endereco.cidade);   // 'Niterói'