Cloning Objects in JavaScript


JavaScript made its way from being a toy language for simple animations to becoming a language for client side and server side web application development. Some generic concepts also made their way to the JavaScript world and developers become more and more aware of them. Cloning objects is one of these concepts.

Cloning can be quite complex. Prototypal inheritance, reference types and methods associated with an object may require a specialized approach. Restrictions on cloned data may simplify cloning. It is the responsibility of the developers to understand and apply the correct cloning method on a case by case basis.

Shallow Copy

Cloning methods of most libraries are implemented using shallow copying. One example is _.clone, the clone method of UnderscoreJs.

Shallow Copy: all field keys and values of the original object are copied to the new object.

This definition has some implications. Recall the shopTransaction object from last week’s article.

Both clonedTransactionand shopTransaction have the same properties. As price and amountPaid are value types (numbers), their values are copied while cloning. The items property is a reference type. Therefore, both shopTransaction and clonedTransaction point to the exact same array in memory.

Changing the value of primitive types in clonedTransaction has no effects on the original object. However, shopTransaction.items and clonedTransaction.item are references pointing to the exact same array. It does not matter which reference we use to add an element to the array, the results are going to be visible under both references. Therefore, the result of the execution of the above code is:

Modifying a value through a reference inside a cloned object also modifies the original object.

Check out a visual representation of the objects created by pythontutor.com. Click on the image for the interactive version of the example.

shoptransaction.

Prototypal Inheritance

The extend method of UnderscoreJs adds all properties inherited via the prototype as own properties.

Learn ES6 in Practice

Sign up below to access an ES6 course with many exercises and reference solutions.

While some shallow copy implementations behave in the same way, others completely omit properties coming from the prototype chain.

More often than not, the prototype should not be considered at all. In case of cloning objects with a prototype, make sure you consider how the clone method treats prototypes.

Are shallow copies dangerous?

We can conclude that making shallow copies are sometimes not enough. When the copied object is modified, it may result in unwanted consequences. Most of the time, software development best practices and restricted workflow allow us to use shallow copies of objects without problems.

For instance, in BackboneJs, the default implementation of the toJSON method of models looks like this:

I have not seen complaints on BackboneJs forums addressing the problem of retrieving the results of the toJSON method and modifying the contents of the model attributes. The reason is that the toJSON method is mostly used for one of the two purposes:

  • Serialization: preparing a JSON payload for an AJAX request,
  • Presentation: preparing a JavaScript object and giving it to a templating engine.

Although some extreme cases may apply, in general, both of these operations only access the clone. Modifying nested data structures are hardly required.

It is very important to detect situations when shallow copies are not enough. In these cases, either think about refactoring your code or using a deep copy.

Deep Copy

When deeply cloning an object, all references are dereferenced. Only the structure of the object, key names and the atomic values are kept. A deep copy requires traversal of the whole object and building the cloned object from scratch.

The deep copy made it possible to modify the items member of both transaction objects individually.

The object visualizer on pythontutor.com shows that shopTransaction and clonedTransaction are fully distinct: no values are reachable from both objects.

transactiondeepcopy.

Another necessary condition for a sound deep clone function is cycle detection. Cloning functions should terminate even if an object references itself. It is possible to clone these objects in case a lookup table of the references are constructed.

Many cloning methods based on an intermediate representation fail though as the intermediate representation has to be finite.

Restricted deep copy implementations

JSON methods

There is a very easy implementation for making deep copies of JavaScript objects. Convert the JavaScript object into a JSON string, then convert it back into a JavaScript object.

Restrictions:

  • Object o has to be finite, otherwise JSON.stringify throws an error.
  • The JSON.stringify conversion has to be lossless. Therefore, methods are not allowed as members of type function are ignored by the JSON stringifier. The undefined value is not allowed either. Object keys with undefined value are omitted, while undefined values in arrays are substituted by null.
  • Language hacks won’t work. Associative properties of an array will not appear in the cloned object. Example: a = []; a.b = 'language hack';. The value a.b will be accessible in the original object, but it will disappear from the deep copy.
  • The prototype of the copy becomes Object. All properties coming from the prototype chain will be discarded
  • Members of the Date object become ISO-8601 strings, not representing the timezone of the client. The value new Date(2011,0,1) becomes "2010-12-31T23:00:00.000Z" after executing the above deep copy method in case you live in Central Europe.

I have hardly ever found any problems with these restrictions when using JSON methods to implement the deep copy functionality. It is still important to know what can potentially go wrong.

jQuery Extend

When the first argument of $.extend is true, it performs a deep extension. Deeply extending the empty object is the same as cloning the object.

Restrictions:

  • Object o has to be finite, otherwise $.extend throws a RangeError for exceeding the maximum stack size
  • The prototype of the copy becomes Object. All properties coming from the prototype chain will be added as own properties

There are multiple implementations for performing a deep clone. Most of them are restricted in some way. Relying on a generic deep cloning method is hardly ever needed. Whenever an object becomes too complex to clone, it is a good sign that the code has to be refactored.

Know what you’re cloning

Cloning in JavaScript is not straightforward. As there is no native implementation for cloning, we have to implement it ourselves or we have to rely on a third party solution. These solutions work perfectly under some restrictions. Be aware of whether you perform deep or shallow cloning and also consider the restrictions. Whenever simple cloning solutions don’t work, more often than not, think about refactoring your code instead of looking for a more reliable cloning function.

  • Sergey Samokhov

    You can also use _.clone(o, true) or simply _.cloneDeep(o) in Lodash. To be honest, I’ve had zero experience with Underscore, and I don’t know if Lodash can be used as its drop-in replacement, but word is, Lodash is basically a better Underscore. At least Lodash was significantly faster in some tests I’ve seen. It’s also pretty stable and well-used, unlike some projects like Lazy.js.
    (Then again, I’ve just read that a) Underscore is significantly lighter than Lodash (1.5k lines vs 12k), which might be a big deal for client, b) both libs are actively developed, so Underscore is learning some tricks from Lodash, including some speed improvements, and c) a merge between the two is proposed. Which is pretty sweet in my book. I love it when the OS community is capable of joining forces.)

    • Thanks for the tip, Sergey, Lodash deep cloning is also a good option.

      According to my experience, Lodash is very stable indeed. We replaced Underscore with Lodash about 2 years ago and never had any problems with it.

      There was also a GitHub issue about a possible merge here: https://github.com/jashkenas/underscore/issues/2182.