Funções de Cópia Profunda de Objetos em JavaScript

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;
}

Tags: copia-profunda-de-objetos javascript manipulacao-de-dados recursão estruturas-de-dados

Publicado em 6-2 22:35 por Thomas