Imutabilidade na prática
Esta aula explora o conceito de imutabilidade em JavaScript, mostrando por que ela é importante, como o const não garante imutabilidade, e técnicas práticas para trabalhar com dados imutáveis, incluindo o uso de structuredClone.
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
constpara 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 depush,splice, etc. - Para objetos, use spread operator ou
Object.assignpara atualizações simples. - Para objetos complexos, considere
structuredCloneou 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
- MDN: const
- MDN: Spread syntax
- MDN: Object.assign
- MDN: structuredClone
- MDN: Array.prototype.toSorted
- Immer.js
- Immutable.js
- MDN: Trabalhando com objetos
- MDN: Array
- MDN: JSON.stringify
Exercícios
Explique a diferença entre
conste imutabilidade. Dê um exemplo de código ondeconstnão impede a mutação.✓ Resposta:constimpede a reatribuição da variável, mas não torna o valor imutável. Por exemplo:
Imutabilidade significa que o objeto não pode ser alterado após sua criação; para isso, é necessário criar novos objetos.const obj = { a: 1 }; obj.a = 2; // permitido, pois o objeto pode ser modificado.Escreva uma função
atualizarNomeque 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 }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]Explique a diferença entre cópia rasa e cópia profunda. Use
structuredClonepara 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.structuredClonefaz 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); // 2Escreva uma função
adicionarItemque recebe um array e um item, e retorna um novo array com o item adicionado, sem modificar o original. Useconcatou 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]