Unpacking Underscore: Clone and Extend
Underscore's _.clone and _.extend are both shallow — and the difference between a shallow and deep copy is exactly where bugs hide. A look at what each method actually does under the hood, plus a deepExtend pattern.
In my post comparing slice versus splice, I mentioned that slice creates a shallow copy of an array. So what exactly is a shallow copy, and what makes a copy "deep"?
Extend
I'll start by taking a look at the _.extend():
_.extend = function(destination) {
each(slice.call(arguments, 1), function(source) {
if (source) {
for (var prop in source) {
destination[prop] = source[prop];
}
}
});
return destination;
};You call it with _.extend(destination, source), and you can have more than one source. It iterates over the source(s), and for each source, copies the properties to the destination object. This will override any properties set on the destination, as well as properties set by previous sources, and the resulting object is returned.
It is worth noting: if a property is a reference type - that is, it is a pointer to an object, the pointer would be copied over and not the entire object.
Clone
Since objects are reference types, it is often useful to have a copy that you can modify without changing the original. For that purpose, underscore provides a _.clone() method.
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};If the argument obj is a primitive type, then we simply return it.
For arrays, we can create a clone by calling obj.slice() &emdash; as previously discussed, slice allows you to create an array and leave the original unchanged.
To create a shallow clone of object, we can call _.extend(). Extending one object with another is similar to "merging" the two objects. If you merge an object with an empty object, what you'll get in return is a copy of the original object.
Deep Extend
Although underscore doesn't provide a deepExtend method, I came across this snippet from http://youmightnotneedjquery.com/#deep_extend:var deepExtend = function(out) {
out = out || {};
for (var i = 1; i < arguments.length; i++) {
var obj = arguments[i];
if (!obj)
continue;
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object')
deepExtend(out[key], obj[key]);
else
out[key] = obj[key];
}
}
}
return out;
};
deepExtend({}, objA);This is similar to calling $.extend(true, {}, objA); in jQuery. A few things worth noting:
- Check
hasOwnPropertyso that we aren't copying stuff from the prototype - Check if the property is a reference type. If so, recursively call
deepExtendto dive into the object and copy its properties over.