There are several ways to copy an object in Javascript, and they fall into two categories: shallow copies and deep copies. Before going deeper, it might be helpful to define the two:
- Shallow copy: a copy of an object which shares references with the original. Modifying properties on either object might affect both.
- Deep copy: a copy of an object which is entirely independent from the original. Modifying either object will not affect the other.
If you’re copying a simple Javascript object with top-level properties only, all copies will be deep copies, regardless of what method you use. If you have an object with nested properties, however, then you need to be more careful.
Let’s see some examples using a very common shallow-copying method, spread syntax:
// Simple, top-level properties only
const original = { name: "justin", age: 100 };
const copied = { ...original };
// These changes will not affect the original
copied.name = "frank";
copied.age = 50;
console.log(original);
// { name: "justin", age: 100 }
The two objects above are independent. Changing one has no effect on the other because those are all “top-level” properties; there are no nested values.
Here’s a more complicated example, where the objects are not independent:
// The "first" name property is now nested inside another object
const original = { name: { first: "justin" }, age: 100 };
const copied = { ...original };
// This change will affect both objects, this property is nested
copied.name.first = "frank";
// This change will NOT affect both, it isn't a nested property
copied.age = 50;
console.log(original);
// {name: {first: "frank"}, age: 100}
cosole.log(copied);
// {name: {first: "frank"}, age: 50}
In this example, changing a value on a nested property changed the value for both objects. That’s because when copying nested objects, javascript copies them by reference1, rather than creating entirely new objects.
Note: this holds true for Javascript Arrays also, since arrays are just objects. An array containing only top-level numbers or strings will always be deep copied. An array containing objects (or sub-arrays) will have each of those nested objects passed by reference unless you explicitly deep copy them.
Now that we have that explained, let’s move on to common copying methods.
Shallow Copies
Here are the common ways people shallow copy objects in JS:
Spread syntax
This is by far the most commonly-used method:
const original = { name: "justin", job: "dev" };
const copy = { ...original };
// You'll often see this with some properties overridden
const copyTwo = {...original, name: "joyce" };
// You can combine multiple objects
const copyThree = {
...original,
...another,
...somethingElse,
};
Object.assign
const original = { name: "justin", job: "dev" };
const copy = Object.assign({}, original)
// You override properties here too
const copyTwo = Object.assign({}, original, { name: "joyce" });
// You can combine multiple objects this way also
const copyTwo = Object.assign({}, original, another, somethingElse);
Array.concat
const one = [1];
const two = [{ two: 2 }];
// merged will be shallow
const merged = one.concat(two); // [ 1, { two: 2} ]
// this change will affect array 'two' also
merged[1].three = 3;
console.log(two); // [{ two: 2, three: 3 }]
The other common array methods also create shallow copies: Array.from()
and Array.prototype.slice()
.
Deep Copies
The only surefire way to create a deep copy in javascript (without a library) is via the built-in JSON module.
const original = { name: { first: "justin", last: "joyce" }};
const copy = JSON.parse(JSON.stringify(original));
// 'copy' is now a completely independent object
// changing it will not affect the original
copy.name.first = "Tom";
console.log(original.name.first);
// "justin"
The JSON method requires that your original object is fully string-serializable, which is not always the case. Functions, for example, cannot be serialized via JSON, and won’t be copied this way:
const original = { func: () => console.log("hi"), name: "justin" };
const copy = JSON.parse(JSON.stringify(original));
// The 'func' param will be lost
console.log(copy);
// { name: "justin" }
In cases like this one, you might want to use a library function like lodash cloneDeep.
Helpful Links
- For more on the limitations of deep-copying via JSON, head over to the Moz Docs.
Notes
- It’s essentially passing a pointer reference โฉ๏ธ