Imutabilidade é um princípio fundamental na programação funcional e uma prática recomendada em muitos estilos de desenvolvimento. Em JavaScript, entender o que é imutável e como aplicar esse conceito no dia a dia pode evitar bugs sutis, tornar o código mais previsível e facilitar a depuração. Nesta aula, vamos mergulhar na imutabilidade na prática: por que ela importa, como o const não é suficiente, quais técnicas podemos usar para manter os dados imutáveis e como a função structuredClone pode nos ajudar.

Vamos começar com uma base teórica e depois ver exemplos práticos que você pode aplicar imediatamente em seus projetos.

Por que importa

A imutabilidade significa que, uma vez criado, um objeto (ou valor) não pode ser modificado. Em vez de alterar o objeto original, criamos uma nova versão com as mudanças desejadas. Isso traz vários benefícios:

  • Previsibilidade: funções que não alteram dados externos são mais fáceis de entender e testar.
  • Menos bugs: evita efeitos colaterais indesejados, onde uma alteração em um lugar afeta o resto do programa de forma inesperada.
  • Facilidade de rastreamento: em aplicações complexas, saber que um objeto nunca muda permite otimizações e simplificações.
  • Compatibilidade com referências: em bibliotecas como React, a imutabilidade é essencial para detectar mudanças e otimizar re-renderizações.

Considere um exemplo simples: se você tem um array de itens e o passa para uma função que o modifica, qualquer outra parte do código que usa esse array será afetada. Com imutabilidade, você cria um novo array e o original permanece intacto, evitando surpresas.

Um exemplo clássico de problema:

const original = [1, 2, 3];
const copia = original;
copia.push(4);
console.log(original); // [1, 2, 3, 4] - inesperado!

Ao invés, com imutabilidade:

const original = [1, 2, 3];
const nova = [...original, 4];
console.log(original); // [1, 2, 3]
console.log(nova);     // [1, 2, 3, 4]

const não é imutável

Muitos desenvolvedores iniciantes acreditam que const torna o valor imutável, mas isso é um equívoco. const apenas impede a reatribuição da variável, ou seja, você não pode fazer variavel = novoValor. No entanto, se o valor for um objeto ou array, suas propriedades ou elementos ainda podem ser alterados.

const obj = { nome: 'João' };
obj.nome = 'Maria'; // permitido!
console.log(obj); // { nome: 'Maria' }

const arr = [1, 2, 3];
arr.push(4); // permitido!
console.log(arr); // [1, 2, 3, 4]

Isso acontece porque const controla a ligação entre o nome da variável e o objeto, não o objeto em si. O objeto permanece mutável. Para alcançar imutabilidade, precisamos de outras técnicas.

Essa distinção é crucial: const é sobre a referência, não sobre o valor. Em linguagens como Java, final tem comportamento semelhante, mas a imutabilidade real exige classes imutáveis ou uso de bibliotecas.

Técnicas

Existem várias técnicas para manter dados imutáveis em JavaScript. Vamos explorar as mais comuns:

Spread operator e Object.assign

Para objetos, podemos usar o spread operator (...) ou Object.assign para criar um novo objeto com as propriedades atualizadas:

const original = { a: 1, b: 2 };
const atualizado = { ...original, b: 3 };
console.log(atualizado); // { a: 1, b: 3 }
console.log(original);   // { a: 1, b: 2 } - intacto

// Com Object.assign
const outro = Object.assign({}, original, { b: 4 });
console.log(outro); // { a: 1, b: 4 }

Para arrays, usamos concat, slice, spread, map, filter, etc., que retornam novos arrays sem modificar o original.

const arr = [1, 2, 3];
const adicionado = [...arr, 4]; // [1, 2, 3, 4]
const removido = arr.filter(n => n !== 2); // [1, 3]
const mapeado = arr.map(n => n * 2); // [2, 4, 6]

Métodos imutáveis de array

O JavaScript moderno inclui métodos como toSorted, toReversed, toSpliced (em versões recentes) que retornam novos arrays em vez de modificar o original.

const arr = [3, 1, 2];
const ordenado = arr.toSorted();
console.log(ordenado); // [1, 2, 3]
console.log(arr);       // [3, 1, 2] - original intacto

Bibliotecas como Immutable.js ou Immer

Para projetos maiores, bibliotecas como Immutable.js fornecem estruturas de dados persistentes e imutáveis. Immer permite que você escreva código mutável, mas trabalha com cópias sob o capô, usando o conceito de "rascunho" (draft).

// Exemplo com Immer
import { produce } from 'immer';

const state = { items: [1, 2, 3] };
const nextState = produce(state, draft => {
  draft.items.push(4);
});
console.log(nextState); // { items: [1, 2, 3, 4] }
console.log(state);     // { items: [1, 2, 3] }

Cuidado com cópias rasas

O spread operator e Object.assign fazem cópias rasas (shallow copy). Isso significa que, se o objeto tiver objetos aninhados, eles ainda serão compartilhados por referência. Para imutabilidade completa, precisamos de cópias profundas ou de técnicas como imutabilidade estrutural.

const original = { a: { b: 1 } };
const copiaRasa = { ...original };
copiaRasa.a.b = 2;
console.log(original.a.b); // 2 - mudou!

Para evitar isso, precisamos de cópias profundas ou de bibliotecas especializadas.

structuredClone

O método structuredClone é uma função nativa do JavaScript (disponível em Node.js 17+ e navegadores modernos) que cria uma cópia profunda de um objeto. Ele usa o algoritmo de clonagem estruturada, que suporta muitos tipos de dados, incluindo Date, Map, Set, ArrayBuffer, etc.

const original = { a: { b: 1 }, arr: [1, 2, 3] };
const clone = structuredClone(original);
clone.a.b = 2;
console.log(original.a.b); // 1 - não mudou!

Isso é extremamente útil para criar versões imutáveis de objetos complexos sem precisar de bibliotecas externas. No entanto, há algumas limitações: não suporta funções, símbolos, elementos DOM, e pode lançar erros para objetos não clonáveis. Além disso, a clonagem é síncrona e pode ser custosa para objetos muito grandes.

Exemplo prático: em uma aplicação React, você pode usar structuredClone para atualizar o estado sem mutar o original:

const [state, setState] = useState({ user: { name: 'João' } });

function updateName(newName) {
  const newState = structuredClone(state);
  newState.user.name = newName;
  setState(newState);
}

Alternativas anteriores incluíam JSON.parse(JSON.stringify(obj)), mas essa abordagem tem limitações: não suporta Date, Map, Set, funções, etc., e pode ter problemas com tipos especiais. structuredClone é mais robusto e nativo.

Boas práticas e observações finais

Aqui estão algumas recomendações para trabalhar com imutabilidade no dia a dia:

  • Use const para variáveis que não serão reatribuídas, mas lembre-se de que isso não garante imutabilidade do valor.
  • Prefira métodos imutáveis de array (como map, filter, reduce) em vez de push, splice, etc.
  • Para objetos, use spread operator ou Object.assign para atualizações simples.
  • Para objetos complexos, considere structuredClone ou bibliotecas como Immer.
  • Em times, estabeleça convenções para evitar mutações acidentais.
  • Teste suas funções para garantir que não alterem os dados de entrada.

Imutabilidade não é uma bala de prata; às vezes, mutar dados é mais eficiente. Mas, na maioria das aplicações modernas, os benefícios superam as desvantagens. Comece aplicando essas técnicas em pequenos trechos e observe como o código fica mais claro.

Referências

Exercícios

  1. Explique a diferença entre const e imutabilidade. Dê um exemplo de código onde const não impede a mutação.

    ✓ Resposta: const impede a reatribuição da variável, mas não torna o valor imutável. Por exemplo:
    const obj = { a: 1 };
    obj.a = 2; // permitido, pois o objeto pode ser modificado.
    
    Imutabilidade significa que o objeto não pode ser alterado após sua criação; para isso, é necessário criar novos objetos.
  2. Escreva uma função atualizarNome que recebe um objeto usuário e um novo nome, e retorna um novo objeto com o nome atualizado, sem modificar o original. Use o spread operator.

    ✓ Resposta:
    function atualizarNome(usuario, novoNome) {
      return { ...usuario, nome: novoNome };
    }
    
    const usuario = { nome: 'João', idade: 30 };
    const atualizado = atualizarNome(usuario, 'Maria');
    console.log(usuario); // { nome: 'João', idade: 30 }
    console.log(atualizado); // { nome: 'Maria', idade: 30 }
    
  3. Dado o array const numeros = [1, 2, 3, 4], escreva código para adicionar o número 5 ao final sem modificar o array original, usando o spread operator.

    ✓ Resposta:
    const numeros = [1, 2, 3, 4];
    const novosNumeros = [...numeros, 5];
    console.log(numeros); // [1, 2, 3, 4]
    console.log(novosNumeros); // [1, 2, 3, 4, 5]
    
  4. Explique a diferença entre cópia rasa e cópia profunda. Use structuredClone para criar uma cópia profunda de um objeto aninhado e mostre que a cópia é independente.

    ✓ Resposta: Uma cópia rasa copia apenas o nível superior; objetos aninhados ainda são compartilhados. Uma cópia profunda copia todos os níveis. structuredClone faz uma cópia profunda. Exemplo:
    const original = { a: { b: 1 } };
    const copia = structuredClone(original);
    copia.a.b = 2;
    console.log(original.a.b); // 1
    console.log(copia.a.b);    // 2
    
  5. Escreva uma função adicionarItem que recebe um array e um item, e retorna um novo array com o item adicionado, sem modificar o original. Use concat ou spread.

    ✓ Resposta:
    function adicionarItem(arr, item) {
      return [...arr, item];
    }
    
    const original = [1, 2];
    const novo = adicionarItem(original, 3);
    console.log(original); // [1, 2]
    console.log(novo);    // [1, 2, 3]