Search justacoding.blog. [Enter] to search. Click anywhere to close.

December 10th, 2021

What Is A Shallow Copy (Or Clone) In JavaScript?

There is often a degree of confusion when first beginning to copy (or clone) objects in JavaScript.

We can attribute a significant portion of this confusion to a misunderstanding with regards to the types of clone: shallow clones and deep clones.

There are fundamental behavioural differences between these two concepts, and it’s very important to grasp these differences to avoid issues within your own code moving forward.

So let’s firstly begin by explaining what a shallow copy actually is, we’ll then contrast that with deep copies and run through the differences between the two.

Shallow copies (or clones) in JavaScript – the basics

In JavaScript, the behaviour of a shallow copy is best explained with a simple example.

Consider this person object:

const person = {
  name: "Sarah",
  address: {
    number: 10,
    street: "Functional Street"
  }
}

If we were to clone this person object (in this case, using the spread operator) — everything at first may appear to be just fine.

const clonedPerson = { ...person }
// {name: "Sarah", address: {number: 10, street: "Functional Street"}}

So the new clonedPerson object appears to match the original person object exactly. Great, right? That’s what we wanted!

Well, not really.

If you were to change a property within the one of the nested objects within either the cloned object (clonedPerson) or within the original object (person) you’ll notice some (seemingly) strange behaviour.

clonedPerson.address.number = 15
console.log(clonedPerson.address.number) // 15
console.log(person.address.number) // also 15 (???)

Updating a property of a nested object (in this case, the address) in one instance will actually modify this value in the other instance!

This is due to the shallow copy we have created.

That’s why it’s called a shallow copy — it doesn’t properly account for “nested” properties (within objects/arrays) that are deeper down within the target item.

You’ll notice that this behaviour doesn’t exist when you alter one of the top-level properties, such as the name:

clonedPerson.name = "Alex"
console.log(clonedPerson.name) // Alex
console.log(person.name) // Sarah

The behaviour is specific to nested objects (and arrays, too — more on that later).

The top-level properties in the respective objects behave as you’d expect, they are entirely detached from one another and don’t share the same reference(s) at all.

So why do shallow copies behave like this?

When copying an object, if it has a nested object — this nested object will be copied as a reference to the original nested object. Not actually a true copy in the strictest sense of the word.

That’s why updating the cloned nested object will also update the original nested object (and vice-versa). Because both objects are now pointing to the same place (or location/address in memory, to use more technical terminology).

Or to word it differently, they both refer to the same thing!

But does changing the name property not behave in the same way?

This is a good question to ask.

The answer is, name is a string, and in JavaScript — string is what’s known as a primitive type.

Primitive types are not subject to this same behaviour. That’s why modifying the name of our person (the original object, or the cloned object) won’t have any side-effects at all.

That’s because the primitive types are not copied by reference, but by value instead:

Members of nested objects refer to the same address or location in memory!

Copy by reference (and the behaviours outlined above) is specific to objects and arrays only, and thus, we only need to really worry about the potential side-effects when nested objects are involved (like the address of our person object, and not the name).

Why are shallow copies problematic?

It’s fine to utilize shallow copies in some scenarios. However, it’s important to understand and to differentiate between shallow copies and deep copies to avoid unexpected side-effects in your code.

It would be natural to assume that “copying” an object creates an entirely new version of the object, detached in every way. But as explained, this isn’t the case with shallow copies.

To re-iterate, nested objects in shallow copies are only copied by reference!

The two main ways to clone or copy an object that you may be familiar with with would likely be the spread operator as well as the Object.assign method:

const newPerson = { ...person } // shallow!
const anotherNewPerson = Object.assign({}, person) // also shallow!

These both create shallow copies, not deep ones. So when cloning or copying objects in this way, you should be aware of the potential pitfalls in doing so.

In short, problems with shallow copying can occur when the developer doesn’t understand that they are (potentially) impacting more objects than the new, cloned object via their shallow copy.

This is definitely a common JavaScript pitfall that can catch many developers out.

It’s also the same when copying arrays…

In the examples and explanations above, we’ve been using objects ({}). However, it’s worth noting that the exact same behaviour is also present for arrays.

If you’re array isn’t multi-dimensional, you’ll not notice any problems:

let colours = ["orange", "red", "blue"]
let coloursClone = [ ...colours ]

colours = []

console.log(colours) // []
console.log(coloursClone) // ["orange", "red", "blue"]

However, if your array is multi-dimensional, you’ll encounter the type of issues we’ve looked at for the Object examples above:

const multidimensionalArray = [[1, 2, 3], [6, 7]]
const cloneMultidimensionalArray = [ ...multidimensionalArray ]

multidimensionalArray[0][0] = 55

console.log(multidimensionalArray[0][0]) // 55
console.log(clonedMultidimensionalArray[0][0]) // 55

The first item in the first nested array points to the same reference (with a value of 55) in both arrays, now — even though we only (seemingly) updated it in one of the arrays to begin with.

Deep copies in JavaScript

For cases where a shallow copy is not sufficient, there are a few ways you can go about creating a deep copy.

Firstly, let’s recap what a deep copy actually is, just for clarity.

The concept of a deep copy is fairly simple to understand. It’s probably what you actually initially imagine when you think of “copying” an object in JavaScript.

A deep copy is an entirely new object that matches the original object exactly.

However, any modifications or updates to this deep copy don’t affect the original object at all. None of the properties within the deep copy (nested or otherwise) contain references back to the original object. It’s an entirely new thing of its own.

So it’s as simple as that.

A deep copy is what we can refer to as a “true” copy in every way, with no quirks to consider and no references back to any part of the original (cloned) object. And as such, the complications regarding copy by reference aren’t present when working with deep copies.

How to create a deep copy

Unfortunately, and perhaps surprisingly, there isn’t an ideal out-of-the-box approach to handle this scenario within the native JavaScript toolkit.

This means that instead of relying directly on a single spread operator or Object.assign, you’ll likely want to use one of the approaches listed below.

Doing it “manually” when you know about the shape of the original object

Firstly, you can manually copy the nested objects of your target object in each case to create a true deep copy.

Based on our original person object:

const person = {
  name: "Sarah",
  address: {
    number: 10,
    street: "Functional Street"
  }
}

We can do this:

const newPerson = { ...person, address: { ...person.address} }

In this case, we know that address is a nested object. We know this will be copied by reference if we only perform a shallow copy (using the spread operator or Object.assign).

So we can circumvent this shallow copy by manually copying the address property directly, as demonstrated.

The resulting newPerson object is now a true copy, it’s detached from person in every way:

newPerson.address.number = 99
console.log(newPerson.address.number) // 99, as expected
console.log(person.address.number) // 10 -- unchanged!

As you can see, we’re not dealing with references anymore. newPerson‘s address properties don’t refer to the same place (address/location in memory) as person‘s address properties, so updating one set won’t impact the other!

This approach is perfectly fine, but it’s not ideal, since we’ll need to manually handle the deep-copy separately every time (since each of the objects we want to clone may have a different structure or shape).

Deep clone with JSON.stringify and JSON.parse

Another approach is to use the stringify and parse methods of the JSON object.

This one is more re-useable than the previous example (the manual approach), but there are some caveats (more on that shortly).

To use this method, however, you can follow these steps.

Firstly, stringify the target object:

const stringifiedPerson = JSON.stringify(person)
// '{"name":"Sarah","address":{"number":10,"street":"Functional Street"}}'

So as you may be aware, stringify is converting our person object to a JSON string.

Next, we can parse this JSON string back to an actual object:

const clonedPerson = JSON.parse(person)
// {name: "Sarah", address: {number: 10, street: "Functional Street"}}

This new resultant object, clonedPerson, isn’t a shallow copy. It’s a deep copy — it doesn’t refer back to the same references as the original object in any way.

This is due to the stringify and parse combination that’s been used.

This is generally a fairly reasonable solution, however, there are some caveats to be aware of.

The first caveat – circular dependencies

Firstly, if your object contains circular dependencies, this approach won’t work at all.

Circular dependencies will cause an error immediately within your application as stringify won’t even attempt to unravel these for dependencies for you.

A circular dependency looks a little bit like this:

const parentObj = {
  childObjs: []
}

const childObj = {
  parent: parentObj
}

parentObj.childObjs.push(childObj)

// Uncaught TypeError: Converting circular structure to JSON
JSON.stringify(parentObj)

There’s a circular dependency here: childObj references parentObj and parentObj also references childObj (via the childObjs array). JSON.stringify does not like this.

Now, there are various ways to handle these circular dependencies, such as libraries/packages like flatted — or custom approaches (many of which you’ll find on StackOverflow).

But still, they won’t be handled out of the box for you, so to speak. So that’s definitely something to be aware of when cloning using this approach.

The second caveat – data loss

It’s also worth noting that if your object contains any “complex” object types, such as Dates or functions these won’t be handled correctly by this approach.

Take this example:

const someObject = {
  someDate: new Date()
}
const someClonedObject = JSON.parse(JSON.stringify(someObject))

console.log(someObject.someDate) // an actual Date object
console.log(someClonedObject.someDate) // an ISOString -- '2021-12-08T08:24:24.202Z'

So the original Date object (someDate) isn’t correctly cloned via this method.

It’s actually converted to a string as part of this process, more specifically, the results of .toISOString() when applied to the original date.

This type of inconsistent cloning is also present with other complex types, like functions, undefined and regular expressions (RegExp).

In short, this JSON cloning method is fine unless your object:

  • contains complex types (dates, functions, regular expressions and so on)
  • has circular dependencies

This leaves one remaining approach to the deep clone problem.

Using a custom deep clone method

Lastly, you can create your own (or find an existing) custom approach to this problem.

Conceptually, you’ll need to clone each value in your object in a recursive fashion, until the entire object has been cloned and you have a full, copied version of the original object.

This would be considered the “best” approach to deep-cloning an object by many, as you’ll have a reusable method with no caveats or pitfalls (such as the circular dependency issue with the JSON approach).

If you don’t want to write your own, you can find many examples of custom deep clone functions all across StackOverflow as well as within other resources, like this deep clone gist here.

If you’re using a framework or library…

If you’re using a framework or a library such as lodash or jQuery, it’s highly likely that you already have the facility to deep copy objects at your disposal.

This is great, as these methods are undoubtedly highly robust and can be relied upon to do a good job in an efficient manner!

In lodash, you can use _.cloneDeep (not to be confused with _.clone, which deals with shallow copies).

In jQuery, there is clone. This method creates a deep copy, as required.

In closing

Thanks for reading!

I hope this article has been of some use to you.

Please feel free to check out other, similar articles in the JavaScript category.

Thanks for reading!

Have any questions? Ping me over at @justacodingblog
← Back to blog