Today I’m going to be talking about Object.extend
without much introduction or context. Bear with me. This is a prerequisite blog post for something I’ll be talking about in a few days.
Extending objects in JavaScript
Prototype has a function named Object.extend
. It takes two objects and copies all properties from the second object onto the first. This is what it looks like, more or less:
Object.extend = function(destination, source) {
for (var property in source)
destination[property] = source[property];
return destination;
};
One major advantage of this approach is that it modifies only the destination
object and leaves the source
object untouched. Except… not really, in certain complex cases.
For example…
Consider this:
var destination = { foo: "bar", baz: { bang: "zoom" } };
var source = { baz: { thud: "narf" } };
Object.extend(destination, source);
destination.baz.thud; // -> "narf"
destination.baz.bang; // -> undefined
source.baz.thud = "troz";
destination.baz.thud; // -> "troz"
See the problem? I assigned destination.baz
to point to the same thing as source.baz
— but that thing is an object, and objects are copied by reference. So the existing destination.baz
property — an object with its own bang
key — gets replaced entirely. Plus, when I redefine the destination.baz.thud
property, I’m also redefining source.baz.thud
.
In other words, Object.extend
makes a “shallow” copy of source
, rather than a “deep” copy.
The fix
Let’s take a stab at a “deep extend” function. We want it to work much like Object.extend
, but behave a bit differently when it encounters an object.
Object.deepExtend = function(destination, source) {
for (var property in source) {
if (typeof source[property] === "object") {
destination[property] = destination[property] || {};
arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
};
Remember that arguments.callee
refers to the function itself. In other words, recursion is the magic ingredient to turn a shallow copy into a deep copy.
Wait! We’re not done, you lazy bastards. This simple function will cause some unexpected bugs.
The first one is simple to fix. One of the quirks of JavaScript is that typeof null
returns "object"
. But it’s not an object. So let’s check for that, too:
Object.deepExtend = function(destination, source) {
for (var property in source) {
if (typeof source[property] === "object" &&
source[property] !== null ) {
destination[property] = destination[property] || {};
arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
};
The second one is a little trickier. If you don’t see it now, you’d probably see it the first time you tried something like this:
Object.deepExtend(options, { sourceElement: $('names_list') });
Yes, that property value is a DOM node. Try typeof document.body
in your Firebug console and you’ll see it returns “object” instead of a far more useful value. Copying all the individual properties of a DOM node onto another object would be like gluing a bunch of feathers onto a throw pillow and calling it a chicken.
DOM nodes should always be passed by reference, even when doing a deep extend. In fact, the only type of object that should not be passed by reference is a plain‐old JavaScript object. But how do we detect a POJO? Keep in mind:
-
As explained, host objects (like DOM nodes) identify themselves as objects.
-
Anything created with the
new
keyword (e.g., an instance of a Prototype class) identifies itself as an object. -
Arrays (and the
arguments
collection) identify themselves as objects.
For these reasons, the typeof
operator won’t work, nor will Juriy’s clever approach. Instead we’ll have to check the constructor
.
Object.deepExtend = function(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor &&
source[property].constructor === Object) {
destination[property] = destination[property] || {};
arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
};
Does it work? Let’s try the example from earlier.
var destination = { foo: "bar", baz: { bang: "zoom" } };
var source = { baz: { thud: "narf" } };
Object.extend(destination, source);
destination.baz.thud; // -> "narf"
destination.baz.bang; // -> "zoom"
source.baz.thud = "troz";
destination.baz.thud; // -> "narf"
In the real world, we write unit tests. But that’s the boring part, so it’s left as an exercise for the reader. (Suckers.)
The tease
As I said, this isn’t the real blog post. The real blog post will be here in a few days. But I’m getting this part out of the way so that it doesn’t clutter up the rest.
Comments
Nice post :)
I’m wondering why don’t you check the source[property] and not the destination one ?
Could this work:
p.s. Object.deepExtend could be very useful in Prototype :)
A few thoughts:
Object
will be differentThanks Andrew, this is great. What would be the best way to deep clone objects having arrays as properties, with these arrays containing objects?
@sethaurus: True in both cases. That’s why
Object.deepExtend
would be safe to use only in narrow circumstances, and one major reason why I would not want to include this in Prototype.@Joran: I don’t know — what’s the use case? In my implementation, I chose to copy arrays by reference because they’re not object literals, and treating them as such would cause some bizarre side-effects.