Photo by Ries Bosch on Unsplash
From Shallow to Deep: A Comprehensive Guide to JavaScript Data Duplication for Arrays and Objects
Table of contents
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:
string
: Represents textual data, like "Hello, World!".number
: Represents numeric data, like42
or3.14
.boolean
: Represents a logical value, eithertrue
orfalse
.undefined
: Represents a variable that has been declared but not assigned a value.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:
object
: Represents more complex data structures, like{first: "Amy", last: "Doe"}
.array
: Represents a collection of data, like["apple", "banana", "orange"]
.function
: Represents a block of code designed to perform a particular task, likefunction 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 thatObject.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 changingshallowCopy.d
does not changeoriginal.d
butshallowCopy.e.f
does changeshallowCopy.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 changeoriginal.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 todeepCopy
not affectingoriginal
.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
andRamda
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! ❤️