From Shallow to Deep: A Comprehensive Guide to JavaScript Data Duplication for Arrays and Objects

Behnam AmiriBehnam Amiri
8 min read

Introduction

Let's quickly review data types in JavaScript. In JavaScript, data types are broadly categorized into two categories: primitive and non-primitive.
Primitive data types are the predefined data types provided by the JavaScript language. They are not objects, have no methods or properties, and are immutable. There are seven primitive data types in JavaScript:

  1. string: Represents textual data, like "Hello, World!".

  2. number: Represents numeric data, like 42 or 3.14.

  3. boolean: Represents a logical value, either true or false.

  4. undefined: Represents a variable that has been declared but not assigned a value.

  5. null: Represents the intentional absence of any object value.

Non-primitive data types are derived from primitive data types and are mutable. They are also known as reference types because they store references to the data. There are three main non-primitive data types in JavaScript:

  1. object: Represents more complex data structures, like {first: "Amy", last: "Doe"}.

  2. array: Represents a collection of data, like ["apple", "banana", "orange"].

  3. function: Represents a block of code designed to perform a particular task, like function add(a, b) { return a + b; }.

Also good to know 👇

A JavaScript array is actually a specialized type of JavaScript object, with the indices being property names that can be integers used to represent offsets. However, when integers are used for indices, they are converted to strings internally in order to conform to the requirements for JavaScript objects.

Data Structures and Algorithms with JavaScript by Michael McMillan

Shallow Copy: A shallow copy creates a new object, but does not create copies of the objects that the original object references. Instead, it copies the references to those objects, which means changing the copied object changes the original object as well, and vice versa. For more info visit: Mozilla Shallow Copy

Deep Copy: A deep copy creates a new object and recursively copies all objects it references, creating new instances rather than copying references. In contrast to a shallow copy, changing the copied object does not affect the original object. For more info visit: Mozilla Deep Copy

Code Examples

Shallow Copy Examples

  • Using =: In this case, both the source and the copy point to the same underlying values; hence, changing either one results in changing the source object.

      const original = { a: 1, b: { c: 2 }, d: 3 };
      const shallowCopy = original;
    
      console.log(shallowCopy); // { a: 1, b: { c: 2 }, d: 3 }
    
      shallowCopy.d = 'yes';
      original.b.c = 'no';
    
      console.log(original); // { a: 1, b: { c: 'no' }, d: 'yes' }
      console.log(shallowCopy); // { a: 1, b: { c: 'no' }, d: 'yes' }
    
  • Using Object.assign(): The key point is that Object.assign() creates a shallow copy, not a deep copy. This means only the first level of the properties is copied. For nested objects, only the reference is copied. Notice how changing shallowCopy.d does not change original.d but shallowCopy.e.f does change shallowCopy.e.f

      const original = { a: 1, b: { c: 2 }, d: 3, e: { f: 4 } };
      const shallowCopy = Object.assign({}, original);
    
      console.log(shallowCopy); // { a: 1, b: { c: 2 }, d: 3 }
    
      original.b.c = 'new original';
      shallowCopy.d = 'new shallowCopy';
      shallowCopy.e.f = 100;
    
      // { a: 1, b: { c: 'new original' }, d: 3, e: { f: 100 } }
      console.log(original);
    
      // { a: 1, b: { c: 'new original' }, d: 'new shallowCopy', e: { f: 100 }}
      console.log(shallowCopy);
    
  • Using Spread Operator: The spread operator is a way to create a shallow copy of arrays and objects in JavaScript. However, be mindful of the shallow nature of the copy, especially when dealing with nested structures. Notice shallowCopy.a does not change original.a.

      const original = { a: 1, b: { c: 2 }, d: 3, e: { f: 4 } };
      const shallowCopy = { ...original };
    
      console.log(shallowCopy); // { a: 1, b: { c: 2 }, d: 3, e: { f: 4 } }
    
      shallowCopy.b.c = 'from shallow';
      shallowCopy.a = 22;
      original.e.f = 'from original';
    
      // { a: 1, b: { c: 'from shallow' }, d: 3, e: { f: 'from original' } }
      console.log(original);
    
      // { a: 22, b: { c: 'from shallow' }, d: 3, e: { f: 'from original' } }
      console.log(shallowCopy);
    
  • Using Array.from(): This method creates a new, shallow-copied array instance from an array-like or iterable object. Let's take a look at the example below.

      // Case A:
      const original = [1, 2, 3, 4, 5];
      const shallowCopy = Array.from(original);
    
      console.log(shallowCopy); // [1, 2, 3, 4, 5]
    
      // Modifying the copy will NOT affect the source array
      shallowCopy[3] = 'new value';
      console.log(shallowCopy);    // [1, 2, 3, 'new value', 5]
      console.log(original);       // [1, 2, 3, 4, 5]
    
      // Case B:
      const nestedArray = [1, 2, [3, 4], { a: 5 }];
      const shallowCopyNested = Array.from(nestedArray);
    
      console.log(shallowCopyNested); // [1, 2, [3, 4], { a: 5 }];
    
      // Changing a nested element AFFECTS both arrays because
      // of the shallow copy nature
      shallowCopyNested[2][0] = 'new value';
      console.log(shallowCopyNested);  // [1, 2, ['new value', 4], { a: 5 }];
      console.log(nestedArray);        // [1, 2, ['new value', 4], { a: 5 }];
    
  • Using Array.prototype.slice: This method returns a new array containing the same elements as the original array, but the new array is a separate object. Changes to the new array do not affect the original array, and vice versa. However, if the array contains objects or other reference types, the references to these objects are copied, not the objects themselves. This means changes to the properties of these objects will be reflected in both arrays.

      const original = [1, 2, 3, 4, { a: 'yes' }];
    
      // or could do "original.slice();"
      const shallowCopy = Array.prototype.slice.call(original);
    
      console.log(shallowCopy); // [1, 2, 3, 4, { a: 'yes' }];
    
      shallowCopy[0] = 99;
      shallowCopy[4].a = 'new value';
    
      console.log(original);     // [1, 2, 3, 4, { a: 'new value' }];
      console.log(shallowCopy);  // [99, 2, 3, 4, { a: 'new value' }];
    
  • Using Array.prototype.concat(): This method provides a convenient way to create a shallow copy of an array in JavaScript, maintaining the original array's elements. Modifications to the copy do not affect the original array, except for objects or nested arrays, where changes are shared due to the shallow nature of the copy.

      const original = [1, 2, 3, 4, { a: 'yes' }];
      const shallowCopy = [() => true].concat(original);
    
      console.log(shallowCopy); // [1, 2, 3, 4, { a: 'yes' }]
    
      shallowCopy[0] = 99;
      shallowCopy[5].a = 'nooice';
    
      console.log(original); // [1, 2, 3, 4, { a: 'nooice' }]
      console.log(shallowCopy); // [99, 2, 3, 4, { a: 'nooice' }]
    

A shallow copy means that only the first level of the array or object is copied. If the original array or object contains other objects or arrays (nested structures), the references to those objects or arrays are copied, not the objects or arrays themselves. This means that changes to nested objects or arrays in the copied version will affect the original.

Deep Copy Examples

In JavaScript, creating a deep copy can be a bit tricky due to the language's dynamic nature and nested structures like objects and arrays. Here are several methods to achieve deep copying: JSON Methods, Recursion, Iterative Deep Copy, Lodash or Ramda , and Custom Copying Logic.

  • Using JSON Methods: If an object can be serialized, then we can utilize JSON methods for deep copying ensures that the original and copied objects are completely independent, as demonstrated by the changes to deepCopy not affecting original.

      const original = { a: 1, b: { c: 2}, d: 3};
      const deepCopy = JSON.parse(JSON.stringify(original));
    
      console.log(deepCopy); // { a: 1, b: { c: 2}, d: 3};
    
      deepCopy.a = 'hello';
      deepCopy.b.c = 'new value';
    
      console.log(original); // { a: 1, b: { c: 2}, d: 3};
      console.log(deepCopy); // { a: 'hello', b: { c: 'new value'}, d: 3};
    
  • JSON methods are simple but have limitations. Take a look at the following example.

      const deepCopy = (data) => JSON.parse(JSON.stringify(data));
    
      // undefined is converted to null
      const one = nestedCopy([1, undefined, 2]);
    
      // Date objects are converted to strings and lose their Date properties.
      const two = nestedCopy([new Date()]);
    
      // Infinity and NaN are converted to null.
      const three = nestedCopy([Infinity, NaN, 3]);
    
      // Functions are omitted entirely from the copied object.
      const four = nestedCopy([{ a: 1, b: function() { return 2; }}]);
    
      // Regex is converted to empty objects.
      const five = nestedCopy([/abc/]);
    
      try {
        const circular = {};
        circular.self = circular;
        nestedCopy(circular);
      } catch(e) {
        console.log(e.message); // Converting circular structure to JSON
      }
    
      console.log(one);   // [ 1, null, 2 ]
      console.log(two);   // [ '2024-05-20T04:40:29.477Z' ]
      console.log(three); // [ null, null, 3 ]
      console.log(four);  // { a: 1 }
      console.log(five);  // [ {} ]
    

JSON.stringify/parse only work with Number and String and Object literal without function or Symbol properties. Moreover, Circular references will throw an error because JSON.stringify cannot handle them. These examples show the limitations of using JSON.parse(JSON.stringify(data)) for deep copying complex objects. In such cases, a more robust deep cloning solution is needed, such as using a library like Lodash, Ramda, or writing a custom deep copy function.

Optimizing Performance

For performance optimization, especially with large objects, consider the following strategies:

  • Avoid Unnecessary Copies: Only copy when you need to modify the original object. Use references where possible.

  • Incremental Copies: Instead of deep copying the entire object, copy only the parts that change.

  • Use Efficient Libraries: Libraries like Lodash and Ramda that are optimized for performance and handle many edge cases.

Conclusion

  • Shallow Copy: Quick and suitable for non-nested objects, but shares references to nested objects.

  • Deep Copy: Recursively copies all nested objects, useful for complete duplication but slower and more resource-intensive.

  • Choose the method based on the specific needs of your application.

To wrap up, deep copying in JavaScript is essential for managing complex data structures without unintended side effects. Understanding and handling special cases, optimizing performance, and leveraging advanced techniques like Proxies and immutable data structures can significantly enhance your ability to manage object copies efficiently.

Thank you for reading my blog! ❤️

1
Subscribe to my newsletter

Read articles from Behnam Amiri directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Behnam Amiri
Behnam Amiri