Search code examples
javascriptdeep-copy

Cloning arrays of objects with Object.assign


I discovered a bug on a project I'm working on that can be replicated by this snippet:

const original = [ { value: 1 } ];
function test() {
    const copy = Object.assign([], original);
    copy.forEach(obj => obj.value = obj.value + 1);
}

console.log(original[0].value); // -> 1, expected 1
test();
console.log(original[0].value); // -> 2, expected 1
test();
console.log(original[0].value); // -> 3, expected 1

I do not understand why this is the case. In the MDN web docs, the following statements can be found in the deep copy warning section:

For deep cloning, we need to use alternatives, because Object.assign() copies property values.

If the source value is a reference to an object, it only copies the reference value.

How do these notes apply to arrays / in this case? Are array values somehow considered as properties?

Looking back now, the method was probably not intended to work with arrays, so I guess I reap what I sow... but I'd still like to understand what's going on here. The intent was to deep copy the array in order to mutate the objects inside while keeping the original intact.


Solution

  • Are array values somehow considered as properties?

    Yes. In JavaScript, arrays are objects (which is why Object.assign works with them), and properties with a special class of names called array indexes (strings defining decimal numbers in standard form with numeric values < 232 - 1) represent the elements of the array. (Naturally, JavaScript engines optimize them into true arrays when they can, but they're defined as objects and performing object operations on them is fully supported.) I found this sufficiently surprising when getting deep into JavaScript that I wrote it up on my anemic old blog.

    Given:

    const obj = {a: 1};
    const arr = [1];
    

    these two operations are the same from a specification viewpoint:

    console.log(obj["a"]);
    console.log(arr["0"]); // Yes, in quotes
    

    Of course, we don't normally write the quotes when accessing array elements by index, normally we'll just do arr[0], but in theory, the number is converted to a string and then the property is looked up by name — although, again, modern JavaScript engines optimize.

    const obj = {a: 1};
    const arr = [1];
    console.log(obj["a"]);
    console.log(arr["0"]); // Yes, in quotes
    console.log(arr[0]);


    If you need to clone an array and the objects in it, map + property spread is a useful way to do that, but note the objects are only cloned shallowly (which is often sufficient, but not always):

    const result = original.map((value) => ({...value}));
    

    For a full deep copy, see this question's answers.