Object Cloning in Javascript

Object Cloning in Javascript

Exploring Ways to Copy an Object in JavaScript

ยท

9 min read

While coding in Javascript have you ever encountered a situation where you needed to copy an object? and when you try to copy an object, you face some issues like all of the properties are not cloned, or when changing the copied object somehow the original object is changed, leaving you without the desired results. Well, In this article we'll explore some simple solutions to these issues.

First thing that came to your mind is that why not simply use ๐ŸŸฐ to copy objects, but it'll not work b'coz Javascript objects are reference type values. worry not, there are 3 basic ways of object cloning:

const originalObject = { name: 'John', age: 30 };

// Using Spread Operator (...)
const spread_Cloned = { ...originalObject };

// Using Object.assign() Method
const assign_Cloned = Object.assign({}, originalObject);

// Using JSON.stringify() and JSON.parse() 
const cloned_JSON = JSON.parse(JSON.stringify(originalObject));

console.log(spread_Cloned); // { name: 'John', age: 30 }
console.log(assign_Cloned); // { name: 'John', age: 30 }
console.log(cloned_JSON); // { name: 'John', age: 30 }

Introduction

If you're dealing with tricky data setups, making exact copies, or just want to keep things from changing unexpectedly, copying objects can be super helpful in JavaScript. This article is all about diving into object cloning in JavaScript. We'll check out different ways to do it and figure out when and why it's important. By the time you finish reading, you'll totally get how to clone objects and be ready to use it in your own coding projects.

Shallow Clone vs Deep Clone

When you shallow clone something in JavaScript, you're basically making a copy that only goes skin-deep. It means you get a new object with the same basic stuff as the original, but if those basic things include other objects, those aren't copied โ€“ they're just referenced. Now, deep cloning is like making a photocopy of everything. You get a brand-new object where every little thing, even the stuff inside other stuff, gets copied over completely. So, you end up with a totally separate copy of everything from the original.

Techniques for Object Cloning

Using Spread Operator (...)

The spread operator in JavaScript is a concise and efficient way to clone objects. It allows us to create a copy of an object, including all its enumerable properties, into a new object.

const originalObject = { name: 'John', age: 30 };
const clonedObject = { ...originalObject };

console.log(clonedObject); // { name: 'John', age: 30 }

Advantages:

  • Very simple syntax

  • Supports cloning both Shallow and moderately nested objects

Limitations:

  • Cannot handle deeply nested objects

  • Does not clone non-enumerable properties or methods

Using Object.assign() Method

The Object.assign() method is another powerful technique to clone objects. It copies the values of all enumerable properties from one or more source objects into a target object, returning the target object.

const originalObject = { name: 'John', age: 30 };
const clonedObject = Object.assign({}, originalObject);

console.log(clonedObject); // { name: 'John', age: 30 }

Advantages:

  • Handles cloning of non-enumerable properties and methods

  • Supports cloning moderately nested objects

Limitations:

  • Cannot clone deeply nested objects directly

  • Overwrites properties if they already exist in the target object

Using JSON.stringify() and JSON.parse()

The combination of JSON.stringify() and JSON.parse() methods can be used as a workaround for cloning complex objects. By converting the object to a JSON string and then parsing it back into an object, we can achieve a Deep Clone of the original object.

const originalObject = { name: 'John', age: 30 };
const clonedObject = JSON.parse(JSON.stringify(originalObject));

console.log(clonedObject); // { name: 'John', age: 30 }

Advantages:

  • Supports cloning deeply nested objects

  • Can be used to clone objects with circular references

Limitations:

  • Does not preserve non-enumerable properties or methods

  • May not preserve certain data types (e.g., dates, regular expressions) during cloning

Using Third-party Library

Lodash is a popular JavaScript utility library that provides a powerful method called cloneDeep() for object cloning. It performs a deep copy of the entire object hierarchy, ensuring accurate replication of the original object's structure.

const originalObject = { name: 'John', age: 30 };
const clonedObject = _.cloneDeep(originalObject);

console.log(clonedObject); // { name: 'John', age: 30 }

Advantages:

  • Can handle cloning of any level of object nesting

  • Preserves non-enumerable properties and methods

Limitations:

  • Adds a dependency on an external library (Lodash)

  • May have performance implications for large objects due to deep copying

If you don't want to use any third-party library, there is an alternate solution: using a custom deep cloning function we'll discuss it further in this article. Both have their advantages and limitations.

Shallow Cloning

Shallow cloning is the simplest form of object cloning. It creates a new object with the same top-level properties as the original object. However, if those properties are themselves objects, they are still references to the original objects. Shallow cloning is a quick and easy way to duplicate objects when there's no need to clone nested objects.

Shallow Clone Example:

const originalObject = {
  name: 'John',
  age: 25,
  hobbies: ['reading', 'swimming'],
};

const clonedObject = { ...originalObject };

In this example, the spread operator (...) creates a shallow clone of originalObject. Although clonedObject appears to be an identical copy, modifying the hobbies property in either object will affect both, as they still share a reference to the same array.

We can fix this issue by Deep Cloning.

Deep Cloning

Deep cloning is a more thorough form of object cloning. It creates a new object and recursively clones all nested properties and their descendants, ensuring each object is an independent copy. Deep cloning is indispensable when working with complex data structures and avoiding unintended side effects.

Using JSON.stringify() and JSON.parse():

//Deep Clone Using JSON.stringify() and JSON.parse()
const originalObject = {
  name: "John",
  age: 30,

  // Nested object
  address: {
    city: "New York",
    state: "NY",
  },

  // Method
  doAction: function () {
    console.log(this.name + " is walking in " + this.address.city);
  },
};
const clonedObject = JSON.parse(JSON.stringify(originalObject));

console.log(clonedObject);
 // { name: 'John', age: 30, address: { city: 'New York', state: 'NY' } }

As we can see in the above example we are losing our data, the method "doAction" is not cloned b'coz it does not preserve non-enumerable properties or methods. The solution for this issue is given below.

Using Custom Recursive Function:

// Deep Clone Using Custom Recursive Function
// Custom Recursive Function 
function deepClone(obj, visited = new WeakMap()) {
  // Handle non-object types and null
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // Handle circular references
  if (visited.has(obj)) {
    return visited.get(obj);
  }

  // Handle dates
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }

  // Handle regular expressions
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }

  // Handle arrays
  if (Array.isArray(obj)) {
    const cloneArray = [];
    visited.set(obj, cloneArray);
    obj.forEach((item, index) => {
      cloneArray[index] = deepClone(item, visited);
    });
    return cloneArray;
  }

  // Handle objects
  const cloneObj = {};
  visited.set(obj, cloneObj);
  for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      cloneObj[key] = deepClone(obj[key], visited);
    }
  }
  return cloneObj;
}

const clonedObject = deepClone(originalObject);
console.log(clonedObject); /*{
  name: 'John',
  age: 30,
  address: { city: 'New York', state: 'NY' },
  doAction: [Function: doAction]
}*/

console.log(clonedObject.doAction()); // John is walking in New York

In this example, deepClone is a recursive function that clones all properties and nested objects of originalObject. The resulting clonedObject is a deep copy, ensuring no changes made to original one and no object will impact the other.

  • We use a WeakMap to keep track of visited objects to handle circular references.

  • We add support for cloning Date objects and RegExp objects.

  • We handle cloning of arrays and objects separately to ensure correct cloning and handling of circular references.

  • We use Object.prototype.hasOwnProperty.call(obj, key) to ensure that properties from the object's prototype chain are not mistakenly cloned.

This deepClone function can handle a wider range of data types and scenarios.

Let's see another example of 'deepClone' function with different data types:

// Define an object with various data types including arrays, objects, dates, and regular expressions
const originalObject = {
  string: "Hello",
  number: 123,
  boolean: true,
  array: [1, 2, 3],
  object: { a: 1, b: 2 },
  date: new Date(),
  regexp: /test/g,
};

// Add circular reference
originalObject.circular = originalObject;

// Clone the original object
const clonedObject = deepClone(originalObject);

// Modify the cloned object
clonedObject.string = "Modified";
clonedObject.number = 456;
clonedObject.array.push(4);
clonedObject.object.c = 3;

// Log both original and cloned objects
console.log("Original Object:", originalObject);
/*Original Object: <ref *1> {
  string: 'Hello',
  number: 123,
  boolean: true,
  array: [ 1, 2, 3 ],
  object: { a: 1, b: 2 },
  date: 2024-02-23T13:18:38.521Z,
  regexp: /test/g,
  circular: [Circular *1]
}*/
console.log("Cloned Object:", clonedObject);
/*Cloned Object: <ref *1> {
  string: 'Modified',
  number: 456,
  boolean: true,
  array: [ 1, 2, 3, 4 ],
  object: { a: 1, b: 2, c: 3 },
  date: 2024-02-23T13:18:38.521Z,
  regexp: /test/g,
  circular: [Circular *1]
}*/

in this example:

  • We define an originalObject containing various data types including arrays, objects, dates, and regular expressions.

  • We add a circular reference to the originalObject.

  • We then clone the originalObject using the deepClone function.

  • We modify some properties of the cloned object to demonstrate that the original object remains unaffected by the modifications to the clone.

  • Finally, we log both the original and cloned objects to see the differences.

The deepClone function has several advantages and limitations:

Advantages:

  • Deep Cloning: The function can recursively clone nested objects and arrays, ensuring a deep copy of the original object.

  • Support for Various Data Types: It supports cloning of various data types including objects, arrays, dates, and regular expressions, making it versatile for different use cases.

  • Handling Circular References: It can handle circular references gracefully by keeping track of visited objects using a WeakMap, preventing infinite recursion.

  • Maintains Object Structure: It maintains the structure of the original object, including nested objects and arrays, ensuring the cloned object has the same structure.

  • Flexible and Customizable: It can be extended to support additional data types or custom cloning logic based on specific requirements.

Limitations:

  • Performance: Deep cloning can be computationally expensive, especially for large and deeply nested objects, potentially leading to performance issues.

  • Prototype Chain: It does not preserve the prototype chain of objects. Cloned objects will lose their original prototype chain and inherit from Object.prototype.

  • Non-Clonable Properties: Certain properties like non-enumerable properties or properties with getters/setters may not be cloned accurately.

  • External References: If the original object contains references to external resources or mutable shared state, the cloned object will still reference the same external resources, potentially leading to unintended side effects.

  • Memory Consumption: The use of a WeakMap for handling circular references may consume additional memory, especially for large datasets with many circular references.

Overall, while the deepClone function provides a robust solution for deep copying objects, it's important to be aware of its limitations and potential impact on performance and memory usage, especially in scenarios involving large or complex data structures.

Conclusion

Remember, when cloning objects, it's crucial to choose the appropriate method based on your specific needs. Whether you opt for native JavaScript techniques or leverage external libraries like Lodash or Immer, object cloning is a valuable tool for maintaining data integrity and preventing unintended side effects.

So, the next time you find yourself needing to duplicate an object in JavaScript, don't go down the path of manual copying and mutation. Instead, harness the power of object cloning and unlock a world of possibilities.

"Object cloning in JavaScript is the key to maintaining data integrity and avoiding unintended side effects."

ย