Clonando objetos em JavaScript - Shallow vs Deep

Compreenda a diferença entre shallow copy e deep copy em JavaScript. Saiba quando usar cada abordagem e explore suas vantagens.

Diagrama ilustrando dois objetos JavaScript clonados
Clonando objetos em JavaScript - Shallow vs Deep

Clonar objetos é uma tarefa comum para desenvolvedores JavaScript. A clonagem nos permite criar versões independentes e isoladas, preservando a integridade dos dados e evitando efeitos colaterais indesejados. Essa prática é especialmente útil em situações em que desejamos modificar apenas uma cópia do objeto, enquanto mantemos o objeto original intacto.

const obj = {
  protocol: 'https',
  host: 'consolelog.com.br',
  path: '/'
};

const clone = {
  ...obj
};

clone.path = '/tag/dicas-artigos-exemplos-javascript/';

console.log(obj.path);
// "/"

console.log(clone.path);
// "/tag/dicas-artigos-exemplos-javascript/"
Exemplo - Clonando um objeto com o spread operator (...)

Além disso, em casos de gerenciamento de estado, como no Redux ou NGRX, é comum lidar com objetos imutáveis. Nestes casos, clonar o objeto se torna necessário para realizar modificações específicas, entregando um novo objeto como resultado do processo.

export const scoreboardReducer = createReducer(
  initialState,
  on(ScoreboardPageActions.homeScore, (state) => ({
    ...state,
    home: state.home + 1,
  }))
);


// https://ngrx.io/guide/store/reducers
Exemplo - Implementação de um reducer retirado da documentação do NGRX

Estudando os métodos de clonagem de objetos em JavaScript podemos evitar problemas como no exemplo abaixo:

const obj = {
  startDate: new Date("2023-01-01T12:00:00.000Z")
};

const clone = Object.assign(obj, {});

// Somando 5 minutos no clone.startDate
const fiveMinInMS = 5 * 60 * 1e3;
clone.startDate.setTime(
	clone.startDate.getTime() + fiveMinInMS
);

// "2023-01-01T12:05:00.000Z"
console.log(clone.startDate.toISOString());

// O obj.startDate === clone.startDate
// "2023-01-01T12:05:00.000Z"
console.log(obj.startDate.toISOString());

Há diversas abordagens para clonar objetos em JavaScript, tais como usar Object.assign()(exemplo acima) ou o spread operator (...) para efetuar uma shallow copy (cópia "superficial"). Também podemos recorrer a função nativa structuredClone() ou bibliotecas como lodash.cloneDeep() para efetuar uma deep copy (cópia "profunda"). A escolha da abordagem depende das necessidades específicas de cada cenário e é isso que vamos explorar neste texto.

Shallow Copy

Quando utilizamos o ... (spread operator) ou o Object.assign(), estamos efetuando uma shallow copy de um objeto. Isso significa que estamos criando um novo objeto com uma nova referência. Mas é importante destacar alguns detalhes:

  • as propriedades do tipo primitivo (primitive type), como por exemplo string e number, são copiadas gerando um novo valor, ocupando um novo espaço de memória, ou seja, são cópias independentes.
  • As propriedades do tipo referência (reference type), como arrays e objetos, tem suas referências copiadas, ou seja, não é feita uma cópia do objeto em si, mas sim do endereço de memória onde o objeto está armazenado. Portanto, as propriedades reference type do clone apontarão para o mesmo endereço das propriedades do objeto original. Podemos visualizar esse comportamento no exemplo a seguir:
const obj = {
    nome: 'consolelog.com.br',
    tags: [1, 2, 3],
    objs: {
        item1: 'valor1',
        item2: 'valor2'
    }
};

// Utilizando o spread operator (...)
// para clonar o objeto
const clone = {...obj};

// ou poderíamos utilizar o Object.assign:
// const clone = Object.assign({}, obj);

console.log(
    typeof obj,      // object (reference type)
    typeof obj.nome, // string (primitive type)
    typeof obj.tags, // object (reference type)
    typeof obj.objs, // object (reference type)
);

console.log(
    obj === clone,           // false
    obj.nome === clone.nome, // true
    obj.tags === clone.tags, // true
    obj.objs === clone.objs, // true
);

Analisando o código acima:

  1. a variável obj tem um campo primitivo (nome)  e dois referenciados (tags e objs)
  2. clone foi gerado a partir do obj utilizando o spread operator, portanto podemos afirmar que obj !== clone
  3. O campos tags e objs das váriaveis obj e clone apontam para o mesmo objeto. Isso é um aspecto crucial a ser destacado, pois as cópias foram feitas por referência. Portanto, se fizermos uma alteração em obj.tags, o resultado será refletido em clone.tags, uma vez que ambos apontam para o mesmo local na memória. Por essa razão, se adicionarmos um novo item utilizando clone.tags.push(4), o mesmo valor será adicionado a obj.tags.
clone.tags.push(4);

// [1, 2, 3, 4]
console.log(clone.tags);

// [1, 2, 3, 4] (4)
console.log(obj.tags);

O mesmo exemplo acima se aplica a obj.objs ou clone.objs.

Um outro cenário comum é quando o objeto clonado tem algum campo do tipo Date, veja no exemplo abaixo:

const obj = {
  data: new Date("2023-08-01T12:00:00.000-0300")
};

const clone = Object.assign({}, obj);

// Adiciona 10 minutos
const dezMinutos = 10 * 60 * 1e3;
clone.data.setTime(clone.data.getTime() + dezMinutos);

console.log(clone.data);
console.log(obj.data);

// Output:
Tue Aug 01 2023 12:10:00 GMT-0300 (Brasilia Standard Time) (7.16.0.min.js, line 2)

Tue Aug 01 2023 12:10:00 GMT-0300 (Brasilia Standard Time) (7.16.0.min.js, line 2)

Deep Copy

Ao contrário da shallow copy, uma deep copy cria uma nova instância do objeto original e também cria novas instâncias de todos os objetos referenciados. Isso significa que todas as propriedades e membros do objeto original são copiados, não apenas as referências. Como resultado, as alterações feitas em um objeto não afetam o outro.

Uma das formas de realizar uma deep copy é através do uso do structuredClone, que é razoavelmente recente, mas suportado pelos navegadores atuais e também a partir da versão 17 do Node.js. Veja o exemplo abaixo:

const obj = {
  nome: "consolelog.com.br",
  tags: [1, 2, 3],
  objs: {
    item1: "valor1",
    item2: "valor2",
  },
  data: new Date("2023-12-31T23:59:59.000-0300"),
};

const clone = structuredClone(obj);

console.log(
  typeof obj, // object
  typeof obj.nome, // string
  typeof obj.tags, // object
  typeof obj.objs // object
);

// Alterando o valor do nome, que é do
// tipo primitivo:
clone.nome = "https://consolelog.com.br";

console.log(
  obj === clone, // false
  obj.nome === clone.nome, // false
  obj.tags === clone.tags, // false
  obj.objs === clone.objs, // false
  obj.data === clone.data  // false
);

// Problema:
console.log(
  obj.data, // Sun Dec 31 2023 23:59:59 GMT-0300 (Brasilia Standard Time)
  obj.data instanceof Date, // true
  clone.data, // Sun Dec 31 2023 23:59:59 GMT-0300 (Brasilia Standard Time)
  clone.data instanceof Date // true
);

Se o objeto original possuir alguma função que não é serializável, você tomará o seguinte erro: DataCloneError

DataCloneError DOMException
Thrown if any part of the input value is not serializable.

https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#exceptions

Um outro exemplo envolvendo datas:

const obj = {
  data: new Date("2023-08-01T12:00:00.000-0300")
};

// Trecho comentado e utilizado no
// exemplo shallow copy
// const clone = Object.assign({}, obj);

const clone = structuredClone(obj);

// Adiciona 10 minutos
const dezMinutos = 10 * 60 * 1e3;
clone.data.setTime(clone.data.getTime() + dezMinutos);

console.log(clone.data);
console.log(obj.data);

Embora tenhamos o recurso structuredClone nativo, pessoalmente, desencorajo o uso de bibliotecas ou mesmo da combinação JSON.stringify + JSON.parse. No entanto, para fins de estudo, vamos examinar o método JSON.parse(JSON.stringify(obj)):

const obj = {
  nome: "consolelog.com.br",
  tags: [1, 2, 3],
  objs: {
    item1: "valor1",
    item2: "valor2",
  },
  data: new Date("2023-12-31T23:59:59.000-0300"),
  funcao: () => console.log('ola'),
};

const clone = JSON.parse(JSON.stringify(obj));

console.log(
  typeof obj, // object
  typeof obj.nome, // string
  typeof obj.tags, // object
  typeof obj.objs // object
);

// Alterando o valor do nome, que é do
// tipo primitivo:
clone.nome = "https://consolelog.com.br";

console.log(
  obj === clone, // false
  obj.nome === clone.nome, // false
  obj.tags === clone.tags, // false
  obj.objs === clone.objs, // false
  obj.data === clone.data, // false
  obj.funcao === clone.funcao // false
);

// Problema:
console.log(
  obj.data, // Sun Dec 31 2023 23:59:59 GMT-0300 (Brasilia Standard Time)
  obj.data instanceof Date, // true
    
  clone.data, // 2024-01-01T02:59:59.000Z
  clone.data instanceof Date, // false
    
  typeof obj.funcao, // function
  typeof clone.funcao // undefined
);

Podemos destacar alguns pontos do código acima:

  • tivemos problemas na cópia do objeto Date. Quando executamos o JSON.stringify, o objeto Date é convertido para o formato ISO8601, ou seja, uma string. Então quando efetuarmos o parse a data continuará como string e não um objeto Date.
  • Não funcionará com Map e Set

Considerações

Ao clonar objetos em JavaScript, é importante entender a diferença entre deep clone e shallow clone. O deep clone cria uma cópia completa e independente do objeto, incluindo todos os seus níveis de aninhamento. Por outro lado, o shallow clone cria uma cópia superficial, onde as propriedades de níveis mais profundos são compartilhadas entre o objeto original e o clone.

Existe uma diferença entre performance destes dois métodos. Fiz uma pequena simulação apenas para dar uma ideia. Veja abaixo:

const obj = {
  "product": "Live JSON generator",
  "version": 3.1,
  "releaseDate": "2014-06-25T00:00:00.000Z",
  "demo": true,
  "person": {
    "id": 12345,
    "name": "John Doe",
    "phones": {
      "home": "800-123-4567",
      "mobile": "877-123-1234"
    },
    "email": [
      "[email protected]",
      "[email protected]"
    ],
    "dateOfBirth": "1980-01-02T00:00:00.000Z",
    "registered": true,
    "emergencyContacts": [{
        "name": "Jane Doe",
        "phone": "888-555-1212",
        "relationship": "spouse"
      },
      {
        "name": "Justin Doe",
        "phone": "877-123-1212",
        "relationship": "parent"
      }
    ]
  }
}
console.time('shallow copy');
const shallowClone = {
  ...obj
};
console.timeEnd('shallow copy');

console.time('deep copy');
const deepCopy = structuredClone(obj);
console.timeEnd('deep copy');

// Output:
// shallow copy: 0.004ms
// deep copy: 0.027ms

É possível observar que existe uma diferença significativa na performance. Então ao lidar com objetos grandes ou complexos, também é essencial considerar o impacto na performance.

Em resumo, a escolha entre deep e shallow copy depende das necessidades de cada caso.

Links: