Published on 4/21/2022 by
NevuloCopying 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.
1let 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.
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))
1let obj = { a: 1, b: "hi" };2let clonedObj = Object.assign({}, obj);3console.log(clonedObj); // { a: 1, b: "hi" }
A shorter way to do this is using
, 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.1const obj = { a: 1, b: "hi" };2const objClone = { ...obj }; // pass all key/value pairs from an object3console.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?
1const user = {2 id: "abc123",3 // an object inside of an object4 name: {5 first: "John",6 last: "Doe",7 },8};910const userClone = { ...user };11userClone.id = "newId";12// Note that the original object is not mutated when modifying primitive values like a string13console.log(userClone); // { id: "newId", name: { first: "John", last: "Doe" } }14console.log(user); // { id: "abc123", name: { first: "John", last: "Doe" } }1516// When modifying an object inside of a shallow copy...17userClone.name.first = "Johnny";18// ...the original object will also be mutated19console.log(userClone); // { id: "abc123", name: { first: "Johnny", last: "Doe" } }20console.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.
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
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 method.1let obj = { a: 1, b: "hi" };2// create a JSON representation of an object in the form of a string, convert back to object3let objClone = JSON.parse(JSON.stringify(obj));4console.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
structuredClone
global methodstructuredClone
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
1// Create an object with a value and a circular reference to itself2let original = { name: "test" };3original.self = original;45// Clone it6let clone = structuredClone(original);78console.assert(clone !== original); // the objects are don't have the same identity9console.assert(clone.name === "test"); // they do have the same values10console.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
For most cases, though, this will create a deep copy of whatever objects you throw at it.
Subscribe to the Nevuletter so you never miss new posts.
Comments
• 0
You need to be signed in to comment