Different ways to copy an object in JavaScript
Copying any data, like an object, is a problem every developer encounters at one point or another.
Here’s an example of the problem: you have an object stored in a variable called user
which contains information related to a user in your application.
let user = { name: "Blake", age: 20 };
Let’s say the user wants to update their first name, but you also need to keep the data for what their previous name was, perhaps for an audit log.
How can you retain the contents of the “old” user but make the changes required for the “new” user?
You could store the users “old” name in a variable, but it becomes cumbersome when there are many changes you need to keep track of.
This is one example where copying objects comes in handy – when copying data to a new variable, we don’t need to mutate the original value directly and potentially lose data. We can safely store a separate copy of the object contents in another place, and work on that object.
There are a few ways in JavaScript to create a new object using the exact data from the original value. Keep reading to learn about any “gotchas” between these different methods.
Using object spread syntax or Object.assign
One of the simplest ways to copy an object in JavaScript is using Object.assign
, which copies all properties from one or more source objects to a single target object.
In our case, we don’t really have a target object, as we want to create a new object as a clone.
So, we can pass in an empty object for the first argument (the target object), and pass in the object we would like to clone in the second argument (the source object(s))
let obj = { a: 1, b: "hi" };
let clonedObj = Object.assign({}, obj);
console.log(clonedObj); // { a: 1, b: "hi" }
A shorter way to do this is using spread syntax, which allows for object expressions to be “expanded”. One implication of this is that we can expand the key/value pairs from our original object into a new object, effectively making a clone.
const obj = { a: 1, b: "hi" };
const objClone = { ...obj }; // pass all key/value pairs from an object
console.log(objClone); // { a: 1, b: "hi" }
Essentially, the ...obj
gets replaced with all the keys and values from obj
.
Both methods are functionally identical, the only difference being that Object.assign
sets the properties on the target object, whereas using spread syntax creates a new object – meaning that Object.assign
would also trigger any setter functions on the object. This isn’t essential for many people, but an interesting thing to note.
Another interesting thing to note is that both methods only perform a shallow copy. What exactly does that mean?
const user = {
id: "abc123",
// an object inside of an object
name: {
first: "John",
last: "Doe",
},
};
const userClone = { ...user };
userClone.id = "newId";
// Note that the original object is not mutated when modifying primitive values like a string
console.log(userClone); // { id: "newId", name: { first: "John", last: "Doe" } }
console.log(user); // { id: "abc123", name: { first: "John", last: "Doe" } }
// When modifying an object inside of a shallow copy...
userClone.name.first = "Johnny";
// ...the original object will also be mutated
console.log(userClone); // { id: "abc123", name: { first: "Johnny", last: "Doe" } }
console.log(user); // { id: "abc123", name: { first: "Johnny", last: "Doe" } }
A shallow copy of an object means that when you mutate non-primitive values (like the name
object), it will mutate the clone as expected, but also the original object.
This is because any values stored as an object are reference values, meaning that there’s a reference to that object stored. So, when you copy an object with other objects inside, you’re really copying an object with references to those other objects.
This doesn’t apply with primitive (simple) values like numbers and strings. When making a shallow copy of an object with other objects inside, you need to be wary of making sure you’re not mutating the original object unintentionally when mutating the clone.
Converting an object to a JSON string and parsing it back
This method leverages JSON (JavaScript Object Notation) which can hold information about key/value pairs and has support for most basic data types you’d need.
The JSON.stringify
method allows us to pass an object in the first argument, which is the object to convert to string format. Once we have converted our object to a string, we can take that string and parse it back to an actual object using the JSON.parse
method.
let obj = { a: 1, b: "hi" };
// create a JSON representation of an object in the form of a string, convert back to object
let objClone = JSON.parse(JSON.stringify(obj));
console.log(objClone); // { a: 1, b: "hi" }
Unlike the previous method, this method of cloning does support nested objects better, creating a “deep copy”, meaning all values get dereferenced from the original object. With a deep copy, there’s no worry about mutating the original object.
However, because JSON only supports a limited subset of data types in JavaScript such as booleans, strings, arrays, etc. but not things like Date
objects or Map
s, these will not be converted properly when creating the clone unless you take additional steps to transform the incompatible values in the object first to something JSON can understand, and then transform them back to the desired data type you want in JavaScript.
On top of this, JSON.stringify
won’t be able to convert a value to a string if the value references itself. This is known as a circular reference, and trying to convert an object that references itself would result in an infinite loop.
Using structuredClone
global method
structuredClone
is a fairly new, global method, meaning access it from anywhere in our code. The benefit of structuredClone
is that it was made for this purpose; “cloning” or copying an object.
As of the writing of this post, the structuredClone
method has been implemented by major browsers in recent versions (including Node 17+).
// Create an object with a value and a circular reference to itself
let original = { name: "test" };
original.self = original;
// Clone it
let clone = structuredClone(original);
console.assert(clone !== original); // the objects are don't have the same identity
console.assert(clone.name === "test"); // they do have the same values
console.assert(clone.self === clone); // and the circular reference is preserved
Unlike the JSON.parse/stringify
method, this does support circular references, and it preserves most values and data types.
Of note, structuredClone
will throw an error when cloning Error
types or DOM nodes. If your object contains functions, these will be quietly discarded from the result. Read more about features and limitations of structuredClone
.
For most cases, though, this will create a deep copy of whatever objects you throw at it.