Search code examples
javascriptdeep-copyshallow-copy

Does Spread Syntax create a shallow copy or a deep copy?


I am extremely confused for days now regarding the true definition of a shallow copy and a deep copy.

When I read the mdn docs (https://developer.mozilla.org/en-US/docs/Glossary/Shallow_copy) on shallow copy, it all made sense. The first paragraph clearly explains what a shallow copy is. The docs says

A shallow copy of an object is a copy whose properties share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you may also cause the other object to change too — and so, you may end up unintentionally causing changes to the source or copy that you don't expect.

I totally got that part. From my understanding, the below code example is the example of a shallow copy since changing either the source or the copy causes the other object to change too.

let a = {
    food: "pasta",
    restaurantName: "myPastPlace"
}

let b = a

b.food = "hamburger"

console.log(b.food) //hamburger
console.log(a.food) //hamburger

So, the confusing part was that when I use the spread syntax to make a copy. Is this deep copy or shallow copy? Because for the first level deep, to me, the spread syntax(operator) is making a deep copy. However, the MDN doc says the spread syntax creates a shallow copy rather than a deep copy.

In JavaScript, all standard built-in object-copy operations (spread syntax, Array.prototype.concat(), Array.prototype.slice(), Array.from(), Object.assign(), and Object.create()) create shallow copies rather than deep copies.

let a = {
    food: "pasta",
    restaurantName: "myPastPlace"
}

let b = {...a}
console.log(b)

b.food = "hamburger"

console.log(b.food) //hamburger
console.log(a.food) //pasta


Solution

  • A variable can contain either a value (in case of primitive values, like 1), or a reference (in case of objects, like { food: "pasta" }. Primitive types can only be copied, and since they contain no properties, the shallow/deep distinction does not exist.

    If you considered references themselves as primitive values, b = a is a copy of a reference. But since JavaScript does not give you direct access to references (like C does, where the equivalent concept is a pointer), refering to copying a reference as "copy" is misleading and confusing. In context of JavaScript, "copy" is a copy of a value, and a reference is not considered a value.

    For non-primitive values, there are three different scenarios:

    • Coreferences, as created by b = a, merely point to the same object; if you modify a in any way, b is affected the same way. Intuitively you'd say that whichever object you modify it is reflected in the copy, but it would be wrong: there is no copy, there is just one object. Regardless of which reference you access the object from, it is the same entity. It is like slapping Mr. Pitt, and wondering why Brad is mad at you — Brad and Mr. Pitt are the same person, not clones.

    let a = {
        food: "pasta",
        contents: {
            flour: 1,
            water: 1
        }
    }
    let b = a;
    a.taste = "good";
    a.contents.flour = 2;
    console.log(b);

    • Shallow copy makes a copy of an object, but any properties that contain references remain so — resulting in coreferent properties. If you change either object, the other is not affected; but if you change any object that is refered to by a property, you will see the change in the other as well. In this example, you will see b.contents.flour is affected by the change in a (because b.contents and a.contents refer to the same object, { flour: 1, water: 2 }), but a.taste does not exist (since a and b are objects in their own right).

    let a = {
        food: "pasta",
        contents: {
            flour: 1,
            water: 1
        }
    }
    let b = {...a};
    a.taste = "good";
    a.contents.flour = 2;
    console.log(b);

    • Deep copy copies each property as well, recursively, so that there are no coreferences — whatever happens to the original object and its properties, the copy is not affected. Here, both b.taste and b.contents.flour are unaffected by changes in a.

    let a = {
        food: "pasta",
        contents: {
            flour: 1,
            water: 1
        }
    }
    let b = structuredClone(a);
    a.taste = "good";
    a.contents.flour = 2;
    console.log(b);

    Note that at this time structuredClone is still pretty new; if any users are using older browsers, you might need to use a polyfill.