Let's start with a simple example using Underscore's (or jQuery's) extend() method. We'll pretend that we're writing an image Gallery module, and that we can set a timeout value to determine how long an image stays visible:
This is usually known as object-literal inheritance, or as I like to call it, the "hash-smash" pattern. But in fact, it's not technically inheritance. Each call to extend() will simply merge the properties of the two objects returning a new object. So in reality, we've got three copies of Gallery floating around in memory.
Contrast this with the more traditional approach of using object prototypes and constructors:
Notice that because we've assigned setTimeout() to the object prototype, the property is shared between all it's children. However, the timeout property itself is unique to the instance objects (because we hung it off of 'this' in our constructor function).
Okay, now let's look at another version of an extend() method. This time from Backbone.js:
So looking at this, it would seem to do exactly the same things as the above plain-prototype example. We're using 'new' to get a new instance object that has its own timeout property. Meanwhile, setTimeout() stays attached to the prototype. At the same time, the syntax pattern is closer to the previous extend() method we used from Underscore, where we pass in an object literal that extends Backbone's base Model. It kind of seems we're getting the benefits of prototypes with the simplicity of of the object literal hash-smash.
But in reality, there's a pretty serious defect in the above code. Those tests are simply providing a false positive. Let's look at a more complex example where it's easier to see what we're doing wrong. Here, we're adding another property, an object to describe our gallery's data source:
Holy failing tests! What's going on? It looks like the timeout property is still be set correctly, but when we try to set the URL it's being overwritten by the other instance. Let's look at Backbone's extend method directly to see how it works:
Indeed, we can see that Backbone's extend() works quite differently than its Underscore and jQuery counterparts. Much like a constructor function, it sets up the prototype chain and returns a child object. But whereas a constructor is just limited to assigning a prototype, Backbone's extend() will actually create a Surrogate object that sits in the prototype chain between the Model object and the instance object. So all the properties we pass into extend() are added to the prototype (extend() also takes a second optional argument for static properties, as well).
So why did our tests fail? Why are we able to set the timeout value on the instance objects, but not the dataSource? The problem actually lies with how we (aren't) declaring these properties on the instance object itself. Timeout only has a simple scalar value, and when we set it using 'this', we're implicitly declaring a new property called timeout on the instance object, which overrides the prototype property.
However, when it comes to the dataSource property, we're actually using 'this' to access the dataSource property on the prototype, not set it.
Is this a design flaw in Backbone? Not at all. We simply need to be careful about where we're assigning properties. Here's a final example, where we're using Backbone's initialize() to properly declare and assign instance properties:
Until we can all standardize on ECMAScript 6, it's likely we'll continue to see different methods for extending objects and setting up prototypes.