Shallow copy vs deep copy
A shallow copy will clone the top-level object, but nested object are shared between the original and the clone. That's it: an object reference is copied, not cloned. So if the original object contains nested object, then the clone will not be a distinct entity.
A deep copy will recursively clone every objects it encounters. The clone and the original object will not share anything, so the clone will be a fully distinct entity.
Shallow copies are faster than deep copies.
When it is ok to share some data, you may use shallow copy. There are even use case where it is the best way to do the job. But whenever you need to clone a deep and complex data structure, a tree, you will have to perform deep copy. Have in mind that on really big tree, it can be an expensive operation.
Okey, so let's modify the shallowCopy() function of the previous article.
We need to detect properties containing objects, and recursively call the deepCopy() function again.
Here is the result:
function naiveDeepCopy( original )
{
// First create an empty object with
// same prototype of our original source
var clone = Object.create( Object.getPrototypeOf( original ) ) ;
var i , descriptor , keys = Object.getOwnPropertyNames( original ) ;
for ( i = 0 ; i < keys.length ; i ++ )
{
// Save the source's descriptor
descriptor = Object.getOwnPropertyDescriptor( original , keys[ i ] ) ;
if ( descriptor.value && typeof descriptor.value === 'object' )
{
// If the value is an object, recursively deepCopy() it
descriptor.value = naiveDeepCopy( descriptor.value ) ;
}
Object.defineProperty( clone , keys[ i ] , descriptor ) ;
}
return clone ;
}
By the way, if the property is a getter/setter, then descriptor.value
will be undefined, so we won't perform recursion on them, and that's what we want. We actually don't care if the getter return an object or not.
There are still unsolved issues:
- Circular references will produce a stack overflow
- Some native objects like
Date
or Array
do not work properly
- Design pattern emulating private members using a closure's scope cannot be truly cloned (e.g. the revealing pattern)
What is this circular reference thing?
Let's look at that object:
var o = {
a: 'a',
sub: {
b: 'b'
},
sub2: {
c: 'c'
}
} ;
o.loop = o ;
o.sub.loop = o ;
o.subcopy = o.sub ;
o.sub.link = o.sub2 ;
o.sub2.link = o.sub ;
This object self-references itself.
That means that o.loop.a = 'Ha!
implies that console.log( o.a )
outputs "Ha!" rather than "a". You remember how object assignment works? o
and o.loop
simply point to the same object.
However, the naiveDeepCopy() method above does not check that fact and therefore is doomed, iterating o.loop.loop.loop.loop.loop...
forever.
That what is called a circular reference.
Even without the loop property, it happens that the original object want that the subcopy and sub properties point to the same object. Here, again, the naiveDeepCopy() method would produce two differents and independent clone.
A good clone method should be able to overcome that.
The closure's scope hell
Okey, let's examine this code:
function myConstructor()
{
var myPrivateVar = 'secret' ;
return {
myPublicVar: 'public!' ,
getMyPrivateVar: function() {
return myPrivateVar ;
} ,
setMyPrivateVar( value ) {
myPrivateVar = value.toString() ;
}
} ;
}
var o = myContructor() ;
So... o
is an object containing three properties, the first is a string, the two others are methods.
The methods are currently using a variable of the parent scope, in the scope of myConstructor(). That variable (named myPrivateVariable) is created when the constructor is called, however while it is not part of the contructed object in any way, it still remains used by those methods.
Therefore, if we try to clone the object, methods of both the original and the clone will still refer to the same parent's scope variable.
It would not be a problem if this was not a common Javascript's pattern to simulate private members...
As far as I know, there is no way to alter the scope of a closure, so this is a dead-end: pattern using the parent scope cannot be cloned correctly.
Next step: using a library
Okey, so far, we have done a good job hacking Javascript, and it was fun.
Now how about using a ready to use library?
The tree-kit library has a great clone() method, that works in most use case.
It happens that I'm actually the author of this lib, probably some kind of coincidence! ;)
clone( original , [circular] )
- original
Object
the source object to clone
- circular
boolean
(default to false) if true then circular references are checked and each identical objects are reconnected (referenced), if false then nested object are blindly cloned
It returns a clone of the original object.
How to use it? That's pretty straightforward:
- first run the command
npm install tree-kit --save
into your project directory
- then use it like this:
var tree = require( 'tree-kit' ) ;
var myClone = tree.clone( myOriginal ) ;
... where myOriginal is the object you want to clone.
Some optimization work have been done, so tree.clone()
should be able to clone large structure efficiently.
One big step in optimization: removing recursivity in the algorithm – it's all taking place in a loop. It avoids stack-overflow and function's call overhead. As a side-effect, depth-first search has been replaced by a breadth-first search algorithm.
Great news: this method is able to detect circular references and reconnect them if the circular option is set to true! Oooh yeah!
If you are interested, you can visit the related source code on github.
If you are that kind of lazy guy, here is the code as of version 0.4.1 (MIT license):
exports.clone = function clone( originalObject , circular )
{
// First create an empty object with
// same prototype of our original source
var propertyIndex ,
descriptor ,
keys ,
current ,
nextSource ,
indexOf ,
copies = [ {
source: originalObject ,
target: Object.create( Object.getPrototypeOf( originalObject ) )
} ] ,
cloneObject = copies[ 0 ].target ,
sourceReferences = [ originalObject ] ,
targetReferences = [ cloneObject ] ;
// First in, first out
while ( current = copies.shift() )
{
keys = Object.getOwnPropertyNames( current.source ) ;
for ( propertyIndex = 0 ; propertyIndex < keys.length ; propertyIndex ++ )
{
// Save the source's descriptor
descriptor = Object.getOwnPropertyDescriptor( current.source , keys[ propertyIndex ] ) ;
if ( ! descriptor.value || typeof descriptor.value !== 'object' )
{
Object.defineProperty( current.target , keys[ propertyIndex ] , descriptor ) ;
continue ;
}
nextSource = descriptor.value ;
descriptor.value = Array.isArray( nextSource ) ?
[] :
Object.create( Object.getPrototypeOf( nextSource ) ) ;
if ( circular )
{
indexOf = sourceReferences.indexOf( nextSource ) ;
if ( indexOf !== -1 )
{
// The source is already referenced, just assign reference
descriptor.value = targetReferences[ indexOf ] ;
Object.defineProperty( current.target , keys[ propertyIndex ] , descriptor ) ;
continue ;
}
sourceReferences.push( nextSource ) ;
targetReferences.push( descriptor.value ) ;
}
Object.defineProperty( current.target , keys[ propertyIndex ] , descriptor ) ;
copies.push( { source: nextSource , target: descriptor.value } ) ;
}
}
return cloneObject ;
} ;
Happy hacking!