There are 2 types of array cloning: shallow & deep. Shallow copies only cover the 1st level of the array and the rest are referenced. If you want a true copy of nested arrays, you’ll need a deep clone. For deep clones, go with the JSON way OR better yet use Lodash 👍
const numbers = [1, [2], [3, [4]], 5];
// Using JavaScript
JSON.parse(JSON.stringify(numbers));
// Using Lodash
_.cloneDeep(numbers);
Arrays are Reference Types
In order to understand why there are two types of cloning. Let's dig into the fundamentals and explains what are reference types.
Unlike your primitive types (ie. number or string), arrays are reference types. Which means when you assign an array to a variable, you're assigning a memory address and not the actual array itself. WTH 😱. I know this is a bit confusing. So let's explain with an example.
Copying a Value type
So no biggie here. We're creating a copy of value
. And if we change the valueCopy
, it doesn't affect the original value
. Makes sense - when we change the copy it shouldn't affect the original at all. All good here 👍
let value = 3;
let valueCopy = value; // create copy
console.log(valueCopy); // 3
// Change valueCopy
valueCopy = 100
console.log(valueCopy); // 100
// ✅ Original NOT affected
console.log(value); // 3
Copying a Reference type
Okay, things are about to get weird now. Let's copy our array using the same method as we did to copy a value type.
let array = [1,2,3];
let arrayCopy = array; // create copy
console.log(arrayCopy); // [1,2,3];
// Change 1st element of the array
arrayCopy[0] = '👻';
console.log(arrayCopy); // [ '👻', 2, 3 ]
// ❌Original got affected
console.log(array); // [ '👻', 2, 3 ]
Why did the original array also got affected? That's because what you copied over is not the array itself but the pointer to the memory space the array occupies. Reference types don't hold values, they are a pointer to the value in memory.
Solution to Copying Reference Types
So the solution is to copy over the value NOT the pointer. Like this:
let array = [1,2,3];
let arrayCopy = [...array]; // create TRUE copy
console.log(arrayCopy); // [1,2,3];
// Change 1st element of the array
arrayCopy[0] = '👻';
console.log(arrayCopy); // [ '👻', 2, 3 ]
// ✅ Original NOT affected
console.log(array); // [ 1, 2, 3 ]
Shallow vs Deep Clone
When I used spread ...
to copy an array, I'm only creating a shallow copy. If the array is nested or multi-dimensional, it won't work. Let's take a look:
let nestedArray = [1, [2], 3];
let arrayCopy = [...nestedArray];
// Make some changes
arrayCopy[0] = '👻'; // change shallow element
arrayCopy[1][0] = '💩'; // change nested element
console.log(arrayCopy); // [ '👻', [ '💩' ], 3 ]
// ❌ Nested array got affected
console.log(nestedArray); // [ 1, [ '💩' ], 3 ]
As you can see, the shallow or first layer is fine. However, once we change the nested element, the original array also got affected. So the solution is to do a deep clone:
let nestedArray = [1, [2], 3];
let arrayCopy = JSON.parse(JSON.stringify(nestedArray));
// Make some changes
arrayCopy[0] = '👻'; // change shallow element
arrayCopy[1][0] = '💩'; // change nested element
console.log(arrayCopy); // [ '👻', [ '💩' ], 3 ]
// ✅ Nested array NOT affected
console.log(nestedArray); // 1, [ 2 ], 3 ]
Community Input
Values Not Compatible with JSON
@tailcall: One has to be really careful with JSON solution! It doesn't work with values not compatible with JSON. Consider using a library function if you have to work with such data.
function nestedCopy(array) {
return JSON.parse(JSON.stringify(array));
}
// undefineds are converted to nulls
nestedCopy([1, undefined, 2]) // -> [1, null, 2]
// DOM nodes are converted to empty objects
nestedCopy([document.body, document.querySelector('p')]) // -> [{}, {}]
// JS dates are converted to strings
nestedCopy([new Date()]) // -> ["2019-03-04T10:09:00.419Z"]
deepClone vs JSON
@alfredosalzillo: I'd like you to note that there are some difference between deepClone and JSON.stringify/parse.
- JSON.stringify/parse only work with Number and String and Object literal without function or Symbol properties.
- deepClone work with all types, function and Symbol are copied by reference.
Here's an example:
const lodashClonedeep = require("lodash.clonedeep");
const arrOfFunction = [() => 2, {
test: () => 3,
}, Symbol('4')];
// deepClone copy by refence function and Symbol
console.log(lodashClonedeep(arrOfFunction));
// JSON replace function with null and function in object with undefined
console.log(JSON.parse(JSON.stringify(arrOfFunction)));
// function and symbol are copied by reference in deepClone
console.log(lodashClonedeep(arrOfFunction)[0] === lodashClonedeep(arrOfFunction)[0]);
console.log(lodashClonedeep(arrOfFunction)[2] === lodashClonedeep(arrOfFunction)[2]);
Using Recursion
Tareq Al-Zubaidi: There is another simple and more performant solution to this problem. I would use recursion to solve this.
const clone = (items) => items.map(item => Array.isArray(item) ? clone(item) : item);
See comparison test here
Resources
- MDN Web Docs - JSON.stringify
- MDN Web Docs - JSON.parse
- Lodash: cloneDeep
- Stack Overflow: How do you clone an Array of Objects in Javascript?
- How to differentiate between deep and shallow copies in JavaScript
- JS: Clone, Deep Copy Object/Array
- JavaScript Deep copy for array and object
- Gist: Primitive Types & Reference Types in JavaScript
- Explaining Value vs. Reference in Javascript
- Understanding Deep and Shallow Copy in Javascript
Thanks for reading ❤
Say Hello! Instagram | Twitter | Facebook | Medium | Blog
Top comments (31)
One has to be really careful with JSON solution! It doesn't work with values not compatible with JSON. Examples:
Consider using a library function if you have to work with such data.
Yes! Very good point, let me add that to the code notes! Thanks for noting that 👏
Wow.
This is really cool!
You have added a gem to the post!
Thanks!
Nice summary!
I'd like to add a tiny bit to the nested array example, that when you rest-spread an array, if that array has another nested array as its element, what gets shallow-copied is the reference to the original nested array:
thus creating the mutation effects.
Yup! In those cases, if you want a true copy, you will need to do a deep clone. Thanks for sharing!
As pointed out in other comments, relying on
JSON.parse
andJSON.stringify
to perform a deep cloning is suboptimal (edge cases, performances). Better use the method provided by some library (e.glodash
'scloneDeep
).pulling in an entire library for a single method can also be a kind of suboptimal (versioning, security auditing, vendoring vs. managed dependencies). it's a lot of overhead when there might be a better solution in just not doing deeply nested arrays.
My point is to use an optimised method. Deep cloning arrays or objects may not be very common but if the need arises, I'd recommend using an exisiting solution.
Totally! JSON is the quick&dirty way 😂Lodash is definitely the preferred optimal solution. Thanks for pointing that out 👍
This right here deep copies nested array too
It pushes into a new array with [...matrix[m]]
Hi,
nice post, but i'd like you to note that there are some difference between deepClone and JSON.stringify/parse.
Here an example:
Exactly! Thanks for sharing this! Let me add it to my code notes 👍
In the last clone (recursive code) the function have to return a copy of item, {...item}, in order to create deep cloning, not the item reference.
The whole piece of code is:
const clone = (items) => items.map(item => Array.isArray(item) ? clone(item) : {...item});
Nicely explained. Another quirk of JavaScript I'l know to look out for, but your JSON solution is simple to remember.
I don't think this is javascript specific? And no, it's not a quirk.
Even Java(and most other languages) copies non-primitive data types by reference.
Maybe I misunderstood your comment though.
I'm a primarily a PHP developer so everything about JavaScript is quirky to me. :-)
Let's just pretend I left the obligatory, "But php has a,b,c,d....x,y,z quirks and javascript is so much better" ;)
JSON is your quick and dirty solution. For a more robust solution, I’d go with Lodash 🙂
Just found your ( interesting ) post looking up on how to deep copy an array the right way. While you probably know it by now, the structuredClone() global function now exists since 2022 and is very well supported. It is a good solution to our common headaches !
May this help someone in need, like it did.
Array.form() will do the same operations and same behaviour as [...] spread.
for eg.
let nestedArray = [1, [2], 3];
let arrayCopy = Array.from(nestedArray);
// Make some changes
arrayCopy[0] = '👻'; // change shallow element
console.log(nestedArray); //[1,[2],3]
console.log(arrayCopy); // [ '👻',[2],3]
arrayCopy[1][0] = '💩'; // change nested element
console.log(nestedArray); // [ 1, [ '💩' ], 3 ]