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.
var shopTransaction = {
items: [ { name: 'Astro Mint Chewing Gum' } ],
price: 1,
amountPaid: 1000
}
var clonedTransaction = _.clone( shopTransaction );
clonedTransaction.price = 3;
clonedTransaction.items.push( { name: 'Tom&Berry Frozen Yoghurt' } );
console.log( 'clonedTransaction.price = ', clonedTransaction.price );
console.log( 'shopTransaction.price = ', shopTransaction.price );
console.log( 'clonedTransaction.items.length = ', clonedTransaction.items.length );
console.log( 'shopTransaction.items.length = ', shopTransaction.items.length );
Both clonedTransaction
and 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:
clonedTransaction.price = 3
shopTransaction.price = 1
clonedTransaction.items.length = 2
shopTransaction.items.length = 2
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.
.
Prototypal Inheritance
The extend method of UnderscoreJs adds all properties inherited via the prototype as own properties.
var proto = { protoProperty: 'proto' };
var o = Object.create( proto );
var c = _.extend({}, o);
console.log( 'o.hasOwnProperty( "protoProperty" )', o.hasOwnProperty( "protoProperty" ) );
// o.hasOwnProperty( "protoProperty" ) false
console.log( 'c.hasOwnProperty( "protoProperty" )', c.hasOwnProperty( "protoProperty" ) );
// c.hasOwnProperty( "protoProperty" ) true
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:
toJSON: function(options) {
return _.clone(this.attributes);
}
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.
var shopTransaction = {
items: [ { name: 'Astro Mint Chewing Gum' } ],
price: 1,
amountPaid: 1000
}
var clonedTransaction = deepCopy( shopTransaction );
clonedTransaction.price = 3;
clonedTransaction.items.push( { name: 'Tom&Berry Frozen Yoghurt' } );
console.log( 'clonedTransaction.price = ', clonedTransaction.price );
console.log( 'shopTransaction.price = ', shopTransaction.price );
console.log( 'clonedTransaction.items.length = ', clonedTransaction.items.length );
console.log( 'shopTransaction.items.length = ', shopTransaction.items.length );
The deep copy made it possible to modify the items
member of both transaction objects individually.
clonedTransaction.price = 3
shopTransaction.price = 1
clonedTransaction.items.length = 2
shopTransaction.items.length = 1
The object visualizer on pythontutor.com shows that shopTransaction
and clonedTransaction
are fully distinct: no values are reachable from both objects.
.
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.
var deepCopy = function( o ) {
return JSON.parse(JSON.stringify( o ));
}
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
var deepCopy = function( o ) {
return $.extend( true, {}, o );
}
var shallowCopy = function( o ) {
return $.extend( {}, o );
}
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.