Javascript: Deep & Shallow Clone Complete Guide

lukewanghanxiang

Luke

Posted on August 28, 2024

Javascript: Deep & Shallow Clone Complete Guide

Shallow Clone

Shallow clone refers to creating a new object that exactly copies the original object's properties. If the copied value is a primitive data type, the value itself is copied. If it's a reference data type, the memory address is copied. If the memory address of one object changes, the other object will also reflect that change.

(1) Object.assign()

Object.assign() is a method introduced in ES6 for copying objects. The first parameter is the target object, and the remaining parameters are source objects. The syntax is Object.assign(target, source_1, ...). This method can perform shallow copying and can also perform deep copying of one-dimensional objects.

Notes:

  • If the target object and source objects have properties with the same name, or if multiple source objects have properties with the same name, the properties of the latter objects will overwrite those of the former.

  • If the function has only one parameter, and that parameter is an object, it returns the object directly. If the parameter is not an object, it converts the parameter to an object and then returns it.

  • Since null and undefined cannot be converted to objects, the first parameter cannot be null or undefined, or it will throw an error.

let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);  
console.log(target);  // {a: 1, b: 2, c: 3}

Enter fullscreen mode Exit fullscreen mode

(2) Spread Operator

The spread operator can be used to copy properties when constructing literal objects. The syntax is: let cloneObj = { ...obj };.

let obj1 = {a: 1, b: {c: 1}};
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); // {a: 2, b: {c: 1}}
console.log(obj2); // {a: 1, b: {c: 1}}
obj1.b.c = 2;
console.log(obj1); // {a: 2, b: {c: 2}}
console.log(obj2); // {a: 1, b: {c: 2}}

Enter fullscreen mode Exit fullscreen mode

(3) Array Methods for Shallow Copying Arrays

Array.prototype.slice

  • The slice() method is a JavaScript array method that returns selected elements from an existing array. The syntax is array.slice(start, end), and the method does not modify the original array.

  • The method takes two optional parameters. If neither is provided, the method creates a shallow copy of the array.

let arr = [1, 2, 3, 4];
console.log(arr.slice()); // [1, 2, 3, 4]
console.log(arr.slice() === arr); // false

Enter fullscreen mode Exit fullscreen mode

Array.prototype.concat

  • The concat() method is used to merge two or more arrays. This method does not modify the existing arrays but returns a new array.

  • The method takes two optional parameters. If neither is provided, the method creates a shallow copy of the array.

let arr = [1, 2, 3, 4];
console.log(arr.concat()); // [1, 2, 3, 4]
console.log(arr.concat() === arr); // false

Enter fullscreen mode Exit fullscreen mode

(4) Implementing Shallow Clone

Here is a JavaScript code example to manually implement a shallow copy:

// Implementation of shallow copy
function shallowClone(object) {
  // Only copy objects
  if (object === null || typeof object !== "object") return object;

  // Determine whether to create a new array or object based on the type of the original object
  let newObject = Array.isArray(object) ? [] : {};

  // Iterate over the original object, and only copy its own properties
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }

  return newObject;
}

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Type Checking: The function first checks if the input is an object (not null or a primitive type). If not, it returns itself.

  • Type Determination: Based on whether the original object is an array or a plain object, the function creates a new array or a new object to hold the copied properties.

  • Property Copying: The function then iterates over the original object using a for...in loop. It checks if the property is directly on the object (i.e., not inherited) using hasOwnProperty(). If so, it copies the property to the new object.

  • Return: Finally, the new object (or array) with the copied properties is returned.

Test Case:

const original = {
  name: 'Alice',
  age: 25,
  details: {
    city: 'New York'
  },
  hobbies: ['reading', 'swimming']
};

const copy = shallowClone(original);

console.log(copy);
// Output: { name: 'Alice', age: 25, details: { city: 'New York' }, hobbies: ['reading', 'swimming'] }

copy.name = 'Bob';
copy.details.city = 'Los Angeles';
copy.hobbies[0] = 'running';

console.log(original.name); // 'Alice', the basic property of the original object is not modified

console.log(original.details.city); // 'Los Angeles', the reference of the nested object is shared

console.log(original.hobbies[0]); // 'running', the reference of the array elements is shared

Enter fullscreen mode Exit fullscreen mode

Deep Clone

Deep clone, in contrast to shallow clone, creates a new reference type and copies the corresponding values when encountering properties that are reference types. This means the object obtains a new reference type rather than a reference to the original type. Deep clone can be achieved for some objects using JSON's two functions, but since the JSON format is stricter than the JavaScript object format, it may fail when the property values include functions or Symbol types.

(1) JSON.stringify()

JSON.parse(JSON.stringify(obj)) is one of the more commonly used methods for deep copying. Its principle is to use JSON.stringify to serialize the JavaScript object into a JSON string, and then use JSON.parse to deserialize (restore) the JavaScript object.

This method can achieve deep copying in a simple and straightforward manner, but it has some limitations. If the object being copied contains functions, undefined, Date, RegExp, Map, Set, or Symbol values, these will be lost after processing with JSON.stringify().

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

const original = {
  name: 'Alice',
  age: 25,
  details: {
    city: 'New York',
    hobbies: ['reading', 'swimming']
  }
};

const copy = deepClone(original);

copy.name = 'Bob';
copy.details.city = 'Los Angeles';
copy.details.hobbies[0] = 'running';

console.log(original.name); // 'Alice'
console.log(original.details.city); // 'New York'
console.log(original.details.hobbies[0]); // 'reading'

Enter fullscreen mode Exit fullscreen mode

(2)_.cloneDeep provided by Lodash

The lodash library provides the _.cloneDeep method for performing deep copies.

const _ = require('lodash');

const original = {
  name: 'Alice',
  age: 25,
  details: {
    city: 'New York',
    hobbies: ['reading', 'swimming'],
    birthDate: new Date('1995-01-01')
  }
};

const copy = _.cloneDeep(original);

copy.name = 'Bob';
copy.details.city = 'Los Angeles';
copy.details.hobbies[0] = 'running';
copy.details.birthDate.setFullYear(2000);

console.log(original.name); // 'Alice'
console.log(original.details.city); // 'New York'
console.log(original.details.hobbies[0]); // 'reading'
console.log(original.details.birthDate.getFullYear()); // 1995
Enter fullscreen mode Exit fullscreen mode

(3) structuredClone() global function

structuredClone() is a native JavaScript API used for deep copying. It can clone a JavaScript object and recursively copy all of its nested child objects, rather than just copying references. Compared to traditional deep copy methods, structuredClone() is more powerful and flexible.

Features of structuredClone()

Deep Copy: structuredClone() recursively copies an object and all of its nested child objects and arrays, creating a completely new object copy. Modifying the copy does not affect the original object, and vice versa.

Support for Multiple Data Types: structuredClone() can correctly handle most JavaScript data types, including:
Primitive data types (e.g., strings, numbers, booleans)
Objects and arrays
Date objects
RegExp objects
Map and Set
ArrayBuffer, TypedArray, Blob, File, FileList, and other types

Unsupported Types: structuredClone() cannot clone the following types:
Functions (Function)
Symbol
Objects with circular references (this will throw an error)
Performance Optimization: Since structuredClone() is natively implemented by the browser, it generally offers better performance and stability compared to manually implemented deep copy methods.

const original = {
  name: 'Alice',
  age: 25,
  details: {
    city: 'New York',
    birthDate: new Date('1995-01-01'),
    pattern: /abc/g,
  },
  hobbies: ['reading', 'swimming'],
  nestedArray: [1, [2, 3]],
};

// Perform deep copy using structuredClone
const copy = structuredClone(original);

// Modify the copy
copy.name = 'Bob';
copy.details.city = 'Los Angeles';
copy.hobbies[0] = 'running';
copy.nestedArray[1][0] = 99;

console.log(original.name); // 'Alice'
console.log(original.details.city); // 'New York'
console.log(original.hobbies[0]); // 'reading'
console.log(original.nestedArray[1][0]); // 2

console.log(copy.name); // 'Bob'
console.log(copy.details.city); // 'Los Angeles'
console.log(copy.hobbies[0]); // 'running'
console.log(copy.nestedArray[1][0]); // 99

Enter fullscreen mode Exit fullscreen mode

structuredClone() is particularly well-suited for the following scenarios:

  • Deep Copying Complex Data Structures: When you need to copy an object that contains nested objects, arrays, or other complex structures, structuredClone() ensures complete independence between the copy and the original object.

  • Avoiding the Complexity of Manual Deep Copying: Manually implementing deep copying requires handling various edge cases and data types. structuredClone() provides a native solution that handles these situations more simply and reliably.

(4) Implementing Deep Clone

function deepClone(object, hash = new WeakMap()) {
  // Non-object types or null, return directly
  if (object === null || typeof object !== "object") return object;

  // Check for circular references
  if (hash.has(object)) return hash.get(object);

  // Handle special object types
  let newObject;
  if (Array.isArray(object)) {
    newObject = [];
  } else if (object instanceof Date) {
    newObject = new Date(object);
  } else if (object instanceof RegExp) {
    newObject = new RegExp(object);
  } else {
    newObject = {};
  }

  // Record the current object to avoid circular references
  hash.set(object, newObject);

  // Recursively copy all properties
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = deepClone(object[key], hash);
    }
  }

  return newObject;
}

// Test example
const original = {
  name: "Alice",
  age: 25,
  details: {
    city: "New York",
    hobbies: ["reading", "swimming"],
    birthDate: new Date("1995-01-01"),
    pattern: /abc/g,
  },
};

// Introduce a circular reference
original.self = original;

const copy = deepCopy(original);

console.log(copy);

Enter fullscreen mode Exit fullscreen mode

Notice:

  • Return value for non-object types: If object is null or a non-object type (such as string, number, boolean), the function directly returns object. This ensures that the deep copy function returns the original value instead of undefined when encountering non-object types.

  • Handling circular references: A WeakMap is used to track objects that have already been copied. If the same object is encountered again, the function returns the previously created copy to prevent infinite recursion.

  • Handling special object types: The function includes special handling for Date and RegExp objects to correctly copy instances of these types. If additional types (e.g., Map, Set) need to be supported, the code can be further extended based on specific requirements.

💖 💪 🙅 🚩
lukewanghanxiang
Luke

Posted on August 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related