Blake
Nevulo

Nevulo

Different ways to copy an object in JavaScript

Different ways to copy an object in JavaScript

Blake's photo
Blake
·Apr 21, 2022·

6 min read

Table of contents

  • Using object spread syntax or Object.assign
  • Converting an object to a JSON string and parsing it back
  • Using structuredClone global method

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 Maps, 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.

Did you find this article valuable?

Support Blake by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this