After a brief hiatus, we're back!  Today, we'll look at how to perform a 'deep copy' of structures in JavaScript.  We'll look at a couple of different out-of-the-box solutions and then roll our own recursive solution.  This tutorial will also provide some background on the difference between copying by reference and by value in JS, and the difference between 'deep' and 'shallow' copies.

This post is an expansion (including tests) of part of our JS Quick Reference, available here.

Isn't a copy a copy?!

Unfortunately (later I hope you'll find the power afforded by such granularity fortunate), that's not necessarily the case.  When working with primitives (e.g. Booleans, Integers, etc.) the answer is essentially yes: a copy is a copy.  However when working with Arrays, Objects, or compound objects, a copy is actually a new variable that points to the original structure in memory, or a new reference.

This can lead to problems if you expect to be able to copy an Object and modify it without touching the original.  If your 'copy' is in fact a reference to the original object, any changes you make via the new reference will appear when you access the object by either the original, or new reference.

Here's a simple example:

let person = {name: "Alice", species: "Human"};
let person2 = person; // Naive copy
person2.name = "Bob";
console.log(person)
console.log(person2)
> person
// {name: "Bob", species: "Human"}
> person2
// {name: "Bob", species: "Human"}

Potentially confounding!

So what are my options?

In order to asses our options, we need to consider two distinct, and often referenced types of copying: shallow and deep.

shallow copy is essentially a 'bitwise', or literal copy.  This means that whatever is stored at the memory location pointed to by some reference is copied verbatim to a new location, and a reference to that location is returned:

let myArray = ["Alice", "Bob", "Trudy"];
let shallowCopy = myArray.slice();

let myObject = {name: "Alice", age: 39};
let shallowCopy = Object.assign({}, myObject);

When our structures themselves contain only primitives, this is enough to solve our problem.  For instance, this is sufficient to achieve the desired result from our first example:

let person = {name: "Alice", species: "Human"};
let person2 = Object.assign({}, person);
person2.name = "Bob";
console.log(person)
console.log(person2)
> person
// {name: "Alice", species: "Human"}
> person2
// {name: "Bob", species: "Human"}

Looks good, so what's the catch?

Because an object may contain references to other objects, a shallow copy will copy those references, and thus not, in fact, produce a true copy of the entire object's structure (i.e. contents).  Let's consider the following situation:

let people = [
    {name: "Alice", age: 39},
    {name: "Bob", age: 42},
];
let peopleCopy = people.slice();
peopleCopy[0].age = 120;
> people[0]
// {name: "Alice", age: 120}
> peopleCopy[0]
// {name: "Alice", age: 120}

Despite actually copying the contents of the 'people' array, we still ran into by reference issues.  This is due to the fact that despite returning a copy of the people array, slice() copied the references the array contained, and so when we accessed them via our copy, we still ended up accessing the original objects.  This is why shallow copies are called 'shallow', they only copy the 'top' level of a structure, references and all, instead of descending into the potentially very deep object structures.

Okay okay, so how do I perform a deep copy?!

In JS, there are a couple options, all with caveats.

First, common JavaScript libraries like jQuery and Lodash contain functions to do just this (clone() and cloneDeep() respectively).  We won't cover those here because frankly, that's boring.

Next up, is the tried and true JSONifying technique.  JSON.stringify() will take any JS structure and turn it into a JSON (JavaScript Object Notation) string.  In order to do this, it needs to completely enumerate an object.  We can take advantage of this by wrapping a stringify() call inside its opposite operation, parse() to get a quick-and-dirty deep copy mechanism:

let thing = Object();
let copy = JSON.parse(JSON.stringify(thing));

This will only successfully copy things that can be represented in JSON.  Notable exceptions includes Dates (which in JS inherit from Object), and Functions.

JSONifying also incurs an unnecessary performance hit.  Unnecessary because we can write our own deep copy method!

Get on with it!

Okay.  So what exactly do we need to do in order to perform a deep copy?  As hinted at before, we basically need to descend into every structure we encounter inside the object to be copied until we find a primitive, and then copy that primitive.  "Descend into every" smells like recursion.  How about this:

function deepCopy(obj) {
    let result = Object();
    if (Array.isArray(obj)) {
        result = Array();
    }
    for (let key in obj) {
        const prop = obj[key];
        if (prop !== null && prop !== undefined && typeof prop === 'object') {
            result[key] = deepCopy(prop);
        } else {
            result[key] = prop;
        }
    }
    return result;
}

This keeps calling itself on anything that's an object or an array.  When it finds a primitive, it simply copies it and moves on.

As with JSONifying, in this form our function will only handle things that are objects (not object extensions like Dates) or arrays.  However unlike JSONifying, we're free to handle those cases as we wish.  For example, Dates can be "deeply" copied as follows:

let d = new Date();
let deepCopy = new Date(d.getTime());

Armed with this knowledge, we can augment our deepCopy function to handle Dates:

function deepCopy(obj) {
    let result = Object();
    if (Array.isArray(obj)) {
        result = Array();
    }
    for (let key in obj) {
        const prop = obj[key];
        if (prop !== null && prop !== undefined && typeof prop === 'object') {
            /* Handle special types of Objects here */
            // Date:
            if (prop.constructor === Date) {
                result[key] = new Date(prop.getTime());
            } else {
                result[key] = deepCopy(prop);
            }
        } else {
            result[key] = prop;
        }
    }
    return result;
}

Now we've got an extensible deepCopy function that already handles Dates!

Great, but will it play Crysis?

Let's test it out:

let complexObj = Object({
    name : "asdf",
    val : 5,
    obj: {
        n : "n1",
        v: "v1",
        innerObj : {
            setting1 : [null, null, null, undefined],
            setting2 : true,
            date : new Date(),
        },
    },
    ar: [1,2,3, null]
});
let complexCopy = deepCopy(complexObj);
> complexObj.obj.innerObj === complexCopy.obj.innerObj
// false i.e. references not identical!
>  complexCopy
// Looks identical to complexObj!

Looks like it should!

# Reads: 4056