You have most certainly dealt with copies in JavaScript before, even if you didn’t know it. Maybe you have also heard of the paradigm in functional programming that you shouldn’t modify any existing data. In order to do that, you have to know how to safely copy values in JavaScript. Today, we’ll look at how to do this while avoiding the pitfalls!
First of all, what is a copy?
A copy just looks like the old thing, but isn’t. When you change the copy, you expect the original thing to stay the same, whereas the copy changes.
In programming, we store values in variables. Making a copy means that you initiate a new variable with the same value(s). However, there is a big potential pitfall to consider: deep copying vs. shallow copying. A deep copy means that all of the values of the new variable are copied and disconnected from the original variable. A shallow copy means that certain (sub-)values are still connected to the original variable.
Shallow copy
A shallow copy of an object is a copy whose properties share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you may also cause the other object to change too — and so, you may end up unintentionally causing changes to the source or copy that you don’t expect. That behavior contrasts with the behavior of a deep copy, in which the source and copy are completely independent.
For shallow copies, it’s important to understand that selectively changing the value of a shared property of an existing element in an object is different from assigning a completely new value to an existing element.
For example, if in a shallow copy named copy
of an array object, the value of the copy[0]
element is {"list":["butter","flour"]}
, and you do copy[0].list = ["oil","flour"]
, then the corresponding element in the source object will change, too — because you selectively changed a property of an object shared by both the source object and the shallow copy.
However, if instead you do copy[0] = {"list":["oil","flour"]}
, then the corresponding element in the source object will not change — because in that case, you’re not just selectively changing a property of an existing array element that the shallow copy shares with the source object; instead you’re actually assigning a completely new value to that copy[0]
array element, just in the shallow copy.
In JavaScript, all standard built-in object-copy operations (spread syntax, Array.prototype.concat()
, Array.prototype.slice()
, Array.from()
, Object.assign()
, and Object.create()
) create shallow copies rather than deep copies.
Example
Consider the following example, in which an ingredients_list
array object is created, and then an ingredients_list_copy
object is created by copying that ingredients_list
object.
let ingredients_list = ["noodles", { list: ["eggs", "flour", "water"] }];
let ingredients_list_copy = Array.from(ingredients_list);
console.log(JSON.stringify(ingredients_list_copy));
// ["noodles",{"list":["eggs","flour","water"]}]
Changing the value of the list
property in ingredients_list_copy
will also cause the list
property to change in the ingredients_list
source object.
ingredients_list_copy[1].list = ["rice flour", "water"];
console.log(ingredients_list[1].list);
// Array [ "rice flour", "water" ]
console.log(JSON.stringify(ingredients_list));
// ["noodles",{"list":["rice flour","water"]}]
Assigning a completely new value to the first element in ingredients_list_copy
will not cause any change to the first element in the ingredients_list
source object.
ingredients_list_copy[0] = "rice noodles";
console.log(ingredients_list[0]);
// noodles
console.log(JSON.stringify(ingredients_list_copy));
// ["rice noodles",{"list":["rice flour","water"]}]
console.log(JSON.stringify(ingredients_list));
// ["noodles",{"list":["rice flour","water"]}]
Deep copy
A deep copy of an object is a copy whose properties do not share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you can be assured you’re not causing the other object to change too; that is, you won’t unintentionally be causing changes to the source or copy that you don’t expect. That behavior contrasts with the behavior of a shallow copy, in which changes to either the source or the copy may also cause the other object to change too (because the two objects share the same references).
In JavaScript, standard built-in object-copy operations (spread syntax, Array.prototype.concat()
, Array.prototype.slice()
, Array.from()
, Object.assign()
, and Object.create()
) do not create deep copies (instead, they create shallow copies).
One way to make a deep copy of a JavaScript object, if it can be serialized, is to use JSON.stringify()
to convert the object to a JSON string, and then JSON.parse()
to convert the string back into a (completely new) JavaScript object:
let ingredients_list = ["noodles", { list: ["eggs", "flour", "water"] }];
let ingredients_list_deepcopy = JSON.parse(JSON.stringify(ingredients_list));
// Change the value of the 'list' property in ingredients_list_deepcopy.
ingredients_list_deepcopy[1].list = ["rice flour", "water"];
// The 'list' property does not change in ingredients_list.
console.log(ingredients_list[1].list);
// Array(3) [ "eggs", "flour", "water" ]
As can be seen from the code above, because a deep copy shares no references with its source object, any changes made to the deep copy do not affect the source object.
However, while the object in the code above is simple enough to be serializable, many JavaScript objects are not serializable at all — for example, functions (with closures), Symbols, objects that represent HTML elements in the HTML DOM API, recursive data, and many other cases. Calling JSON.stringify()
to serialize the objects in those cases will fail. So there’s no way to make deep copies of such objects.
For objects that are serializable, you can alternatively use the structuredClone()
method to create deep copies. structuredClone()
has the advantage of allowing transferable objects in the source to be transferred to the new copy, rather than just cloned. But note that structuredClone()
isn’t a feature of the JavaScript language itself — instead it’s a feature of browsers and any other JavaScript runtimes that implement a global object like window
. And calling structuredClone()
to clone a non-serializable object will fail in the same way that calling JSON.stringify()
to serialize it will fail.