4 different techniques for copying objects in JavaScript

When working with functional programming a good rule of thumb is to always create new objects instead of changing old ones. In doing so we can be sure that our meddling with the object’s structure won’t affect some seemingly unrelated part of the application, which in turn makes the entire code more predictable.

How exactly can we be sure that the changes we make to an object do not affect the code elsewhere? Removing the unwanted references altogether seems like a good idea. To get rid of a reference we need to copy all of the object’s properties to a new object. There are many ways to do this and each of them yields a slightly different result. We are going to take a look at the most popular ones: shallow copy, deep copy, merging and assigning.

For every method we analyze, we will look at two different variations - each having a mildly different outcome. Also, on top of listing the pros and cons of every approach, we are going to compare these variations in terms of their performance. I am also going to provide links to the production-ready equivalents to use in an actual, real-life application.

If you wish to see the entire code of a given solution just click on a title. The link will redirect you to a Github repository.

To shallow copy an object means to simply create a new object with the exact same set of properties. We call the copy shallow because the properties in the target object can still hold references to those in the source object.

Before we get going with the implementation, however, let’s first write some tests, so that later we can check if everything is working as expected.

Version 1

Our first implementation works recursively. We write a deep function, which checks the type of the argument sent to it and either calls an appropriate function for the argument being an array or an object or simply returns the value of the argument (if it is neither an array nor an object).

Version 2

Now, let’s take a different approach: our goal is to create a new object without any reference to the previous one, right? Why don’t we use the JSON object then? First, we stringify the object, then parse the resulting string. What we get is a new object totally unaware of its origin.

Note: In the previous solution the methods of the object are retained but here they are not. JSON format does not support functions, therefore they are just removed altogether.

Performance test

When to use?

Deep copying should be used whenever we feel like there might be a need to change a given object on a deeper level (nested objects/arrays). I would, however, recommend trying to use it only when absolutely necessary since it can often slow the program down when working with big collections of objects.

Version 2

This is a safe version in which, instead of mutating the target object, we create an entirely new one which we later assign to a variable. This means we don’t need to pass the target argument at all. Unfortunately, this version does not work with the keyword this because this can’t be reassigned.

Performance test

When to use?

Version 1 is the standard implementation of an assign function. By passing {} as the target we can be sure that no object is mutated. We would like to use assign whenever there is a need to assign some new properties to an existing object, for example:

Production-ready equivalent

This function works like assign but instead of replacing properties in the target it actually adjoins them. If a value is either an array or an object the function proceeds to merge the properties recursively as well. Non-object-like properties (not arrays and not objects) are simply assigned and undefined properties are omitted altogether.

Version 1

What we are going to look at now bears some resemblance to the first version of our deep copy function. This is because we are going to work with a recursive use of functions.

Function mergeValues accepts two arguments: target and source. If both values are objects we call and return mergeObjects with the aformentioned target and source as arguments. Analogically, when both values are arrays we call and return mergeArrays. If the source is undefined we just keep whatever value was previously there which means we return the target argument. If none of the above apply we just return the source argument.

Version 2

This approach may actually seem odd to you because we can easily predict that it is going to be slower. It is, nevertheless, worthwhile to take a look at different angles from which we can tackle the same problem.

The idea here is that we want to first get all the properties of the source object - even if they are nested 3 objects deep - and save a path to them. This will later allow us to set the value at the proper path inside the target object.

A path is an array of strings that looks something like this: [‘firstObject’, ‘secondObject’, ‘propertyName’].

We call the getValue function to get an array of objects that contain paths and values of the properties. Let’s take a look at how this function works. If the argument value is null or is not object-like we simply, since we can’t go any deeper, return an object containing the argument value and its path. Otherwise, if the argument is object-like and not null, we can be sure it is either an array or an object. If it is an array we call getArrayValues and if an object - getObjectValues.

After getting the paths and values of an entire source object we can see that they are deeply nested. We would, however, like to keep all of them in a single array. This means that we need to flatten the array.

Flattening an array boils down to iterating over each item to check if it is an array. If it is we flatten it and then concat the value to the result array.

Now that we have covered how to get the path let’s consider how to set all these properties in the target object.

Let’s talk about the setAtPath function that we are going to use to set the values at their respective paths. We want to get access to the last property of the path to set the value. In order to do so we need to go over the path’s items, that is of properties’ names, and each time get the property’s value.

We start the reduce function with the target object which is then available as the result argument. Each time we return the value under result[key] it becomes the result argument in the next iteration. This way, when we get to the last item of the path the result argument is the object or array where we set the value.

In our example the result argument, for each iteration, would be: target -> firstObject -> secondObject.

We have to keep in mind that the target might be an empty object whereas sources can be many levels deep. This means we might have to recreate an object’s or an array’s structure ourselves before setting a value.

We set the value at the last item of the path and return the object we started with.

if(index===path.length-1){result[key]=valuereturntarget}

If inside the firstObject there were no secondObject we would get undefined and then an error if we tried to set undefined[‘property’]. To prevent this we first check if result[key] even exists to begin with. If it doesn’t we need to create it - either as an object or as an array but how can we know which? Well, the next item in the path is the answer. If the type of the next item is a ‘number’ (so effectively an index) we need to create an array. If it is a string, we create an object.

Performance test

When to use?

Merging objects is not very common. We might, however, find ourselves in a situation where we want to, for example, merge configs with a lot of deep properties in order to set some nested default values.

Note: Merging actually doesn’t lose references to sources. If we wanted to lose them we could create a deep copy of a merged object.

Production-ready equivalent

Conclusion

To sum up, we use shallow copy when we need to get rid of a reference to an object but we care little about references to any of its deeper properties, for example when returning from a function. Deep copy ensures that there are no references to the source object or any of its properties but comes at a cost of slowing down the application. Assign is a great way to merge properties of objects together or just to assign some new values to an existing object. Finally merge, albeit not very popular, allows us to merge properties of objects no matter how deeply nested the objects are.