jonathanjuliani
Spread e Rest em JavaScript: arrays, objetos e os gotchas de cópia rasa
Spread operator e rest em JavaScript: como funcionam em arrays, objetos e funções, e os gotchas de cópia rasa que todo dev encontra.
Você faz const copia = [...original], modifica a cópia, e o array original muda junto. Ou clona um objeto com spread e descobre que uma propriedade aninhada ainda é referência compartilhada. O spread parece mais simples do que é — e os erros que ele esconde são os mais difíceis de debugar porque não jogam exceção nenhuma.
Spread e rest usam a mesma sintaxe (...) com propósitos opostos: spread expande um iterável nos seus elementos, rest coleta elementos separados num array. Entender quando cada um entra em ação — e onde a cópia rasa te pega — é o que este post cobre.
O que você vai aprender:
- Spread em arrays: clonar, combinar e espalhar como argumento
- Spread em objetos: substituindo
Object.assignde vez - Rest parameters: coletando argumentos variádicos com elegância
- Os gotchas de cópia rasa e como fazer deep copy quando precisar
Pré-requisitos: arrays, objetos e funções básicas em JavaScript. Se quiser ver spread em objetos no contexto das features do ES2018 — onde ele foi introduzido — o post ECMAScript ES7 ao ES10 tem a visão geral de onde cada feature veio.
Spread em arrays: clonar, combinar e espalhar argumentos
A sintaxe ... antes de um array o expande nos seus elementos individuais. O caso mais comum é criar uma cópia superficial:
const original = [1, 2, 3];
const copia = [...original];
copia.push(4);
console.log(original); // [1, 2, 3] — não foi afetado
console.log(copia); // [1, 2, 3, 4]Para combinar arrays, spread é mais legível que concat:
const frutas = ['maçã', 'banana'];
const verduras = ['cenoura', 'alface'];
const mercado = [...frutas, ...verduras];
// ['maçã', 'banana', 'cenoura', 'alface']
// você também pode inserir elementos avulsos no meio
const tudo = ['pão', ...frutas, 'leite', ...verduras];
// ['pão', 'maçã', 'banana', 'leite', 'cenoura', 'alface']Outra aplicação direta: espalhar um array como argumentos de função. Antes do spread, você usava .apply():
const numeros = [3, 1, 4, 1, 5, 9];
// antes
Math.max.apply(null, numeros); // 9
// com spread
Math.max(...numeros); // 9 — muito mais limpoFunciona com qualquer função que aceite múltiplos argumentos:
function somar(a, b, c) {
return a + b + c;
}
const valores = [10, 20, 30];
somar(...valores); // 60Spread em objetos: chega de Object.assign
Spread em objetos foi introduzido no ES2018 e resolve o que Object.assign resolvia com menos verbosidade.
Clonar um objeto:
const usuario = { nome: 'Ana', idade: 28 };
const copia = { ...usuario };
copia.nome = 'Bia';
console.log(usuario.nome); // 'Ana' — não foi afetado
console.log(copia.nome); // 'Bia'Mesclar objetos — quando há chave duplicada, o último vence:
const padrao = { tema: 'claro', idioma: 'pt-BR', notificacoes: true };
const preferencias = { tema: 'escuro', tamanhoFonte: 16 };
const config = { ...padrao, ...preferencias };
// { tema: 'escuro', idioma: 'pt-BR', notificacoes: true, tamanhoFonte: 16 }Sobrescrever propriedades específicas sem mutar o original — padrão muito comum em Redux e gerenciamento de estado imutável:
function atualizarUsuario(usuario, alteracoes) {
return { ...usuario, ...alteracoes }; // retorna novo objeto, não muta o original
}
const novo = atualizarUsuario(usuario, { idade: 29 });"Tá Jon, e qual a diferença pro
Object.assignmesmo?"
Object.assign muta o primeiro argumento:
const resultado = Object.assign(padrao, preferencias);
// padrao foi mutado — resultado e padrao são o mesmo objetoCom spread, você sempre recebe um objeto novo. A mutação acidental de Object.assign é a razão pela qual o spread em objetos virou padrão. Se quiser usar Object.assign sem mutar, você passa um objeto vazio como primeiro argumento: Object.assign({}, a, b) — mas nesse caso o spread é mais legível.
Rest parameters: o spread ao contrário
Rest parameters coletam os argumentos restantes de uma chamada de função num único array. É o ... no lado receptor, não no chamador:
function somar(...numeros) {
return numeros.reduce((total, n) => total + n, 0);
}
somar(1, 2, 3); // 6
somar(10, 20, 30, 40); // 100Você pode combinar parâmetros normais com rest — mas rest tem que ser o último:
function log(nivel, ...mensagens) {
// nivel é o primeiro argumento
// mensagens é um array com o resto
mensagens.forEach(msg => console.log(`[${nivel}]`, msg));
}
log('INFO', 'servidor iniciado', 'porta 3000');
// [INFO] servidor iniciado
// [INFO] porta 3000A diferença entre rest e o objeto arguments:
function comArguments() {
console.log(arguments); // object Arguments — não é array de verdade
console.log([...arguments]); // array, mas gambiarra
}
function comRest(...args) {
console.log(args); // array de verdade — tem .map, .filter, tudo
}arguments não existe em arrow functions, não é um array real e não funciona com desestruturação. Rest parameters resolve todos esses problemas.
"Tá Jon, quando uso rest e quando uso spread?"
Fácil de lembrar: rest está na definição da função (coletando o que chega), spread está na chamada (expandindo o que você manda). São operações inversas:
// rest — na assinatura
function juntar(...partes) { return partes.join('-'); }
// spread — na chamada
const palavras = ['bora', 'ver', 'como'];
juntar(...palavras); // 'bora-ver-como'Gotchas: cópia rasa, objetos aninhados e spread em strings
Cópia rasa (shallow copy)
Spread copia apenas o primeiro nível. Se o array ou objeto contém referências — outros objetos, arrays aninhados — essas referências são compartilhadas, não clonadas:
[IMAGEM: diagrama mostrando dois objetos com spread — propriedades primitivas são copiadas por valor, mas propriedades objeto apontam para a mesma referência na memória | alt: "Diagrama de cópia rasa com spread em JavaScript: valores primitivos copiados, objetos aninhados compartilham referência"]
const original = {
nome: 'Ana',
endereco: { cidade: 'SP', rua: 'Av. Paulista' }, // objeto aninhado
};
const copia = { ...original };
copia.nome = 'Bia'; // ok — primitivo, cópia independente
copia.endereco.cidade = 'RJ'; // PERIGO — mutou o objeto original também
console.log(original.nome); // 'Ana' — ok
console.log(original.endereco.cidade); // 'RJ' — ops[IMAGEM: screenshot do console do navegador mostrando original.endereco.cidade como 'RJ' após mutação via copia | alt: "Console do navegador mostrando que o spread de objeto aninhado compartilha referência — original é mutado ao modificar a cópia"]
O mesmo acontece com arrays de objetos:
const usuarios = [{ nome: 'Ana' }, { nome: 'Bia' }];
const copia = [...usuarios];
copia[0].nome = 'Carlos';
console.log(usuarios[0].nome); // 'Carlos' — o objeto dentro ainda é referência compartilhada"Tá Jon, mas então o spread é inútil pra cópia de verdade?"
Não — spread é perfeito pra estruturas rasas, que é a maioria dos casos. O problema é usar spread esperando deep copy quando a estrutura tem aninhamento.
Quando você precisa de uma cópia profunda, tem duas opções principais:
structuredClone (moderno, Node.js 17+ e browsers modernos) — é a forma correta:
const copia = structuredClone(original); // clone profundo, sem gambiarraJSON.parse(JSON.stringify(...)) (funciona em mais ambientes, mas tem limitações):
const copia = JSON.parse(JSON.stringify(original));
// perda de: undefined, functions, Date vira string, Set/Map viram {}Use structuredClone se o ambiente suportar. Só caia no JSON.parse/stringify se precisar de compatibilidade ampla e souber as limitações.
Spread em strings
Uma curiosidade útil: spread funciona em qualquer iterável, incluindo strings:
const letras = [...'hello']; // ['h', 'e', 'l', 'l', 'o']
// útil pra converter string em array de caracteres
const chars = [...'JavaScript'];
// ['J', 'a', 'v', 'a', 'S', 'c', 'r', 'i', 'p', 't']Diferente de split(''), o spread lida corretamente com caracteres Unicode de múltiplos bytes (emojis, caracteres de línguas como árabe e japonês):
'😀🎉'.split(''); // ['', '', '', ''] — quebra os bytes, não os emojis
[...'😀🎉']; // ['😀', '🎉'] — corretoTL;DR
| Uso | Sintaxe | Resultado |
|---|---|---|
| Clonar array | [...arr] | Novo array, primeiro nível copiado |
| Combinar arrays | [...a, ...b] | Novo array com elementos de ambos |
| Espalhar em função | fn(...arr) | Passa cada elemento como argumento |
| Clonar objeto | {...obj} | Novo objeto, primeiro nível copiado |
| Mesclar objetos | {...a, ...b} | Último vence em chave duplicada |
| Rest parameters | function f(...args) | args é array com os argumentos |
| Deep copy | structuredClone(obj) | Clone profundo real |
| Spread em string | [...'texto'] | Array de caracteres (Unicode correto) |
| Primitivos | sempre copiados por valor | Alteração na cópia não afeta original |
| Objetos aninhados | copiados por referência | Alteração afeta o original — use structuredClone |
Próximos passos
Spread em objetos entrou no ES2018 — se quiser ver o contexto completo de onde ele veio junto com Object.fromEntries, flat() e o for await...of, o ECMAScript ES7 ao ES10: as features que você usa sem saber de onde vieram cobre tudo isso com exemplos práticos.
E se você usa spread pra montar payloads de requisições assíncronas, entender Promises de verdade ajuda a fechar o ciclo: Promises em JavaScript: do zero ao async/await.
[PREENCHER: adicionar CTA personalizado aqui — pode ser algo como "Qual gotcha de spread te pegou em produção? Aquele de cópia rasa pega todo mundo pelo menos uma vez. Me fala nos comentários — e se viu alguma coisa errada aqui, corrige sem cerimônia :D"]
Inscreva-se no Ledger da Engenharia
Receba notas de arquitetura e performance na sua caixa de entrada. Mesma lista do convite—inscreva-se aqui quando quiser.
Artigos relacionados
Promises em JavaScript: do zero ao async/await
Promises e async/await em JavaScript do zero: estados, encadeamento, tratamento de erros e os gotchas que a documentação oficial passa batido.
Ler publicação
Implementando Busca Binária com NodeJS e Javascript
Busca binária em JavaScript: versões iterativa e recursiva, array ordenado e complexidade O(log n). Fechamento da série com código.
Ler publicação