Em JavaScript, copiar objetos de forma profunda (deep copy) é uma necessidade comum quando queremos criar uma cópia independente de um objeto existente, especialmente quando o objeto contém outros objetos ou arrays aninhados. Vamos explorar diferentes abordagans para implementar funções de cópia profunda.
Implementação Básica de Cópia Profunda
function copiaProfunda(obj) {
// Determina se devemos criar um novo array ou objeto
const novoObjeto = Array.isArray(obj) ? [] : {};
// Verifica se o objeto existe e é do tipo objeto
if (obj && typeof obj === 'object') {
for (const chave in obj) {
// Se o elemento for um objeto, aplica recursão
if (obj[chave] && typeof obj[chave] === 'object') {
novoObjeto[chave] = copiaProfunda(obj[chave]);
} else {
// Se não for objeto, atribui diretamente
novoObjeto[chave] = obj[chave];
}
}
}
return novoObjeto;
}
// Exemplo de uso
const objetoOriginal = {
nome: 'Item1',
valor: 42,
detalhes: {
cor: 'azul',
dimensao: { largura: 10, altura: 20 }
}
};
const objetoCopia = copiaProfunda(objetoOriginal);
objetoCopia.detalhes.cor = 'verde';
objetoCopia.detalhes.dimensao.largura = 30;
console.log(objetoOriginal);
// { nome: 'Item1', valor: 42, detalhes: { cor: 'azul', dimensao: { largura: 10, altura: 20 } } }
console.log(objetoCopia);
// { nome: 'Item1', valor: 42, detalhes: { cor: 'verde', dimensao: { largura: 30, altura: 20 } } }
Abordagem Mais Robusta
const copiaProfundaObjeto = objeto => clonar(objeto);
const verificaTipo = (obj, tipo) => {
if (typeof obj !== 'object') return false;
const tipoString = Object.prototype.toString.call(obj);
let resultado;
switch (tipo) {
case 'Array':
resultado = tipoString === '[object Array]';
break;
case 'Date':
resultado = tipoString === '[object Date]';
break;
case 'RegExp':
resultado = tipoString === '[object RegExp]';
break;
default:
resultado = false;
}
return resultado;
};
/**
* Função de clonagem profunda
* @param {Object} parent - Objeto a ser clonado
* @returns {Object} Objeto clonado
*/
const clonar = pai => {
const pais = [];
const filhos = [];
const _clonar = pai => {
if (pai === null) return null;
if (typeof pai !== 'object') return pai;
let filho, proto;
if (verificaTipo(pai, 'Array')) {
// Tratamento especial para arrays
filho = [];
} else if (verificaTipo(pai, 'RegExp')) {
// Tratamento especial para expressões regulares
filho = new RegExp(pai.source, /\w*$/.exec(pai));
if (pai.lastIndex) filho.lastIndex = pai.lastIndex;
} else if (verificaTipo(pai, 'Date')) {
// Tratamento especial para objetos Date
filho = new Date(pai.getTime());
} else {
// Tratamento para objetos comuns
proto = Object.getPrototypeOf(pai);
filho = Object.create(proto);
}
// Gerencia referências circulares
const indice = pais.indexOf(pai);
if (indice !== -1) {
return filhos[indice];
}
pais.push(pai);
filhos.push(filho);
for (const chave in pai) {
filho[chave] = _clonar(pai[chave]);
}
return filho;
};
return _clonar(pai);
};
console.log(copiaProfundaObjeto({
nome: 'Maria', idade: 30,
endereco: { rua: 'Rua A', numero: 123},
hobbies: ['leitura', 'natação']
}));
Abordagem Simplificada com Mapa de Referências
function copiar(obj, aparecido = new Map()) {
// Se não for objeto, retorna o valor primitivo
if (!(obj instanceof Object)) return obj;
// Se já foi processado, retorna a cópia existente
if (aparecido.has(obj)) return aparecido.get(obj);
const resultado = Array.isArray(obj) ? [] : {};
aparecido.set(obj, resultado);
// Copia todas as propriedade, incluindo símbolos
[...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)].forEach(chave => {
resultado[chave] = copiar(obj[chave], aparecido);
});
return resultado;
}
Implementação Avançada com Suporte a Tipos Especiais
// Constantes de flags
const FLAG_COPIA_PROFUNDA = 1;
const FLAG_COPIA_PLANA = 2;
const FLAG_SIMBOLOS = 4;
// Tags de tipos para identificação
const TAG_ARGUMENTOS = "[object Arguments]";
const TAG_ARRAY = "[object Array]";
const TAG_BOOLEANO = "[object Boolean]";
const TAG_DATA = "[object Date]";
const TAG_ERRO = "[object Error]";
const TAG_FUNCAO = "[object Function]";
const TAG_REGEXP = "[object RegExp]";
const TAG_MAPA = "[object Map]";
const TAG_NUMERO = "[object Number]";
const TAG_OBJETO = "[object Object]";
const TAG_CONJUNTO = "[object Set]";
const TAG_STRING = "[object String]";
const TAG_SIMBOLO = "[object Symbol]";
const TAG_FRACO_MAPA = "[object WeakMap]";
// Funções de inicialização específicas para cada tipo
function inicializaArray(array) {
const comprimento = array.length;
const resultado = new array.constructor(comprimento);
if (comprimento && typeof array[0] === 'string' &&
Object.prototype.hasOwnProperty.call(array, 'index')) {
resultado.index = array.index;
resultado.input = array.input;
}
return resultado;
}
function ehPrototipo(valor) {
return valor === (valor.constructor.prototype || Object.prototype);
}
function inicializaObjeto(objeto) {
return typeof objeto.constructor === 'function' && !ehPrototipo(objeto)
? Object.create(Object.getPrototypeOf(objeto))
: {};
}
function obterTag(valor) {
return Object.prototype.toString.call(valor);
}
// Funções de clonagem específicas para cada tipo
function clonarBuffer(buffer, ehProfundo) {
if (ehProfundo) {
return buffer.slice();
}
const comprimento = buffer.length;
const resultado = Buffer !== undefined
? Buffer.allocUnsafe(comprimento)
: new buffer.constructor(comprimento);
buffer.copy(resultado);
return resultado;
}
function clonarArrayBuffer(arrayBuffer) {
const resultado = new arrayBuffer.constructor(arrayBuffer.byteLength);
new Uint8Array(resultado).set(new Uint8Array(arrayBuffer));
return resultado;
}
function clonarDataView(dataView, ehProfundo) {
const buffer = ehProfundo ? clonarArrayBuffer(dataView.buffer) : dataView.buffer;
return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);
}
function clonarRegExp(regexp) {
const resultado = new regexp.constructor(regexp.source, /\w*$/.exec(regexp));
resultado.lastIndex = regexp.lastIndex;
return resultado;
}
function inicializaPorTag(objeto, tag, ehProfundo) {
const Construtor = objeto.constructor;
switch (tag) {
case TAG_ARRAY:
return clonarArrayBuffer(objeto);
case TAG_BOOLEANO:
case TAG_DATA:
return new Construtor(+objeto);
case TAG_DATA_VISAO:
return clonarDataView(objeto, ehProfundo);
case TAG_NUMERO:
case TAG_STRING:
return new Construtor(objeto);
case TAG_REGEXP:
return clonarRegExp(objeto);
case TAG_MAPA:
case TAG_CONJUNTO:
return new Construtor();
case TAG_SIMBOLO:
return Symbol.prototype.valueOf ? Object(objeto.valueOf()) : {};
default:
return objeto;
}
}
// Funções auxiliares
function ehMapa(valor) {
return obterTag(valor) === TAG_MAPA;
}
function ehConjunto(valor) {
return obterTag(valor) === TAG_CONJUNTO;
}
function obterChaves(objeto) {
return Object.keys(objeto);
}
function obterTodasAsChaves(objeto) {
return [
...Object.getOwnPropertySymbols(objeto).filter(key =>
objeto.propertyIsEnumerable(key)
),
...Object.keys(objeto)
];
}
function obterChavesEm(objeto) {
const resultado = [];
for (const chave in objeto) {
if (chave !== 'constructor' ||
(!ehPrototipo(objeto) && objeto.hasOwnProperty(chave))) {
resultado.push(chave);
}
}
return resultado;
}
function obterTodasAsChavesEm(objeto) {
const resultado = [];
for (const chave in objeto) {
if (chave !== 'constructor' ||
(!ehPrototipo(objeto) && objeto.hasOwnProperty(chave))) {
resultado.push(chave);
}
}
let temp = objeto;
while (temp) {
resultado.push(
...Object.getOwnPropertySymbols(objeto).filter(key =>
objeto.propertyIsEnumerable(key)
)
);
temp = Object.getPrototypeOf(objeto);
}
return resultado;
}
// Função principal de cópia profunda
function copiaBase(valor, mascara, personalizador, chave, objeto, cache = new Map()) {
const ehProfundo = mascara & FLAG_COPIA_PROFUNDA;
const ehPlano = mascara & FLAG_COPIA_PLANA;
const ehCompleto = mascara & FLAG_SIMBOLOS;
const tipo = typeof valor;
const ehArray = Array.isArray(valor);
let resultado;
if (personalizador) {
resultado = objeto ? personalizador(valor, chave, objeto, cache) : personalizador(valor);
}
if (resultado !== undefined) {
return resultado;
}
if (!(valor !== null && (tipo === 'object' || tipo === 'function'))) {
return valor;
}
if (ehArray) {
resultado = inicializaArray(valor);
if (!ehProfundo) {
return resultado.concat(valor);
}
} else {
const tag = obterTag(valor);
const ehFuncao = tag === TAG_FUNCAO;
if (Buffer !== undefined && Buffer.isBuffer(valor)) {
return clonarBuffer(valor, ehProfundo);
}
if (tag === TAG_OBJETO || tag === TAG_ARGUMENTOS || (ehFuncao && !objeto)) {
resultado = ehPlano || ehFuncao ? {} : inicializaObjeto(valor);
if (!ehProfundo) {
const obterChaves = ehPlano ? obterTodasAsChavesEm : obterTodasAsChaves;
return obterChaves(valor).reduce((acc, chaveCopiaPlana) => {
acc[chaveCopiaPlana] = valor[chaveCopiaPlana];
return acc;
}, {});
}
} else {
resultado = inicializaPorTag(valor, tag, ehProfundo);
}
}
if (cache.has(valor)) {
return cache.get(valor);
}
cache.set(valor, resultado);
if (ehMapa(valor)) {
valor.forEach(subValor => {
resultado.set(
copiaBase(subValor, mascara, personalizador, subValor, valor, cache)
);
});
} else if (ehConjunto(valor)) {
valor.forEach(subValor => {
resultado.add(
copiaBase(subValor, mascara, personalizador, subValor, valor, cache)
);
});
}
const obterChavesFuncao = ehCompleto
? ehPlano
? obterTodasAsChavesEm
: obterTodasAsChaves
: ehPlano
? obterChavesEm
: obterChaves;
const props = ehArray ? undefined : obterChavesFuncao(valor);
(props || valor).forEach((subValor, chave) => {
let novaChave = chave;
let novoValor = subValor;
if (props) {
novaChave = novoValor;
novoValor = valor[novaChave];
}
resultado[novaChave] = copiaBase(
novoValor,
mascara,
personalizador,
novaChave,
valor,
cache
);
});
return resultado;
}