Understanding backbone.js extend method, a meta factory


Reading Backbones code makes me think of a Japanese zen garden or old style Japanese dwelling. There is no waste or excess. It's like an author who rewrote and improved their work over and over such that each statement belongs. Not an instruction is wasted. Its minimal but very powerful.

The following is my step by step timeline analysis of the Backbone library for how custom Classes are created from the core Backbone classes such as Model, View and Collection.
I will focus on Backbone.Model but the same creation pattern is used for Backbone Model, Collection and View ....but not Backbone.Events.
Backbone.Events is just a plain object literal with properties.

Step 1.
Backbone.Model is declared as a function: 
var Model = Backbone.Model = function(attributes, options) { .. }

Backbone.Model is a function. Functions are objects in js. Every object in js has a prototype property which points to an object. Functions inherit from Function.prototype (which has methods like call, bind, etc.):
Backbone.Model ---> Function.prototype ---> Object.prototype ---> null


Step 2.
Copy Backbone.Events properties (methods and properties) and all the methods custom to Backbone.Model into Backbone.Model.prototype object using underscores _.extend():

  // Attach all inheritable methods to the Model prototype.
  _.extend(Model.prototype, Events, {
   
     set: function(key, val, options) {

     ...etc...
  });

After the call to _.extend() completes, Backbone.Model.prototype now has all the properties from Backbone.Events function object as well as its own model methods such as set, save, fetch etc.
Backbone.Model itself only has a few standard object properties, its the prototype object which has all the goodness. Thats how prototypes should work.
Backbone.Model does have an extend() method which is a class property since its on the Constructor fn. We talk about it in #3

Interestingly, the underscore _.extend() method used by Backbone is at its core very close to "JS Definitive Guide" extend code. But underscore extend can extend from 1 or many source objects into target, whereas "JS Definitive Guide" just extend target o from source p
/* from "JS Definitive Guide" set in o all properties in p */
var extend = function(o, p) {
    for (prop in p) {
        o[prop] = p[prop];
    }
    return o;
}

The code to update Backbone.Model.prototype object with its methods and properties (behavior and state) could also have written like the following by setting properties directly on the prototype object (a common approach), but _.extend() is a pretty neat way to do the same thing into Model.prototype
e.g.
    Backbone.Model.prototype.changed = null;
    Backbone.Model.prototype.idAttribute = 'id';
    Backbone.Model.prototype.initialize = function(){};
    Backbone.Model.prototype.toJSON = function(options) {
      return _.clone(this.attributes);
    },
    ....etc....



Step 3. 
Now the Backbone.js code has been parsed and somewhere in your code a call is made to Backbone.Model.extend()
e.g.
    var app.MyModel = Backbone.Model.extend({  .... });

Backbones extend() is cool and a big part of the magic in Backbone.
Its a factory for creating factories, a meta factory. When you call Backbones extend, you get back a Constructor function which you can use to make new objects of that class (a class in js is the set of objects which share the same prototype object - JS Definitive Guide) or even extend subclasses from that Constructor function from which to make subclass instances!

Lets look at Backbones extend() which is set as a "class" property on Backbone Model, View and Collection and thus available to call on each.

Your custom app code calls Backbone.Model.extend(), basically a factory method, to create a new custom Constructor function (a new Type if you will, a class) for your code. In the line below app.MyModel becomes a constructor fn (a new class/Type) and in my code I create instances of MyModel using new against it:
e.g.
    var myModelInstance = new app.MyModel();

When your code invokes the extend() method on Backbone.Model fn object then at fn entry, "this" is set to Backbone.Model object and is set as local var parent in the extend() code (when calling with Backbone.View then "this" is Backbone.View etc.)
 var parent = this;
 var child;


Next the constructor fn for your new type is assigned. You can pass your own constructor but I've never passed in a constructor so this code always runs:

  child = function(){ return parent.apply(this, arguments); };
 
This code basically makes your new type, child, a constructor fn which when new is called on it will execute the parent method. See step 4 for more.
This allows you to make new instances of your new type e.g.    ...   new app.MyModel()

Next extend() code adds class properties into new child:
     _.extend(child, parent, staticProps);
This includes copying into child, Backbones own extend() method so you can extend/inherit from your own classes
e.g. I can inherit subclasses from app.MyModel by calling app.MyModel.extend({})

Next now this code in extend() is very interesting:
    // Set the prototype chain to inherit from `parent`, without calling `parent`'s constructor function.
    var Surrogate = function(){ this.constructor = child; };
    Surrogate.prototype = parent.prototype;
    child.prototype = new Surrogate;
     
This code (and setting of super later) tracks very closely to the pattern identified as "Classical Pattern #5 - A Temporary Constructor" in Stoyan Stefanovs "Javascript Patterns" book (great read) which he explains in detail and indicates a similar function exists in the YUI library. This pattern is also known as a "proxy constructor" pattern. Backbones annotated source gives a nod to goog.inherit for inspiration.
The primary advantage of this pattern is that the parent constructor is not run when creating an instance for the child prototype property (the code makes a new Surrogate, not new Backbone.Model). Plus a child cannot modify the parents prototype object and thus affect all parent and child instances, by modifying its prototype. That link is broken by the Surrogate which serves as a proxy between parent and child, when you modify child.prototype you're modifying a Surrogate instance, not modifying true parent i.e. Backbone.Model.prototype.
This pattern does add 1 more step in the prototype chain for lookups but thats a small con.



child has no methods of its own yet but because of prototype chaining, any request for a Backbone.Model method such as fetch(), save() etc. at this stage will run on the same object as Backbone.Model.prototype. Here's the prototype chain once done:
app.MyModel.prototype ----> Surrogate.prototype ---> Backbone.Model.prototype ---> Object.prototype ---> null

Next add into childs.prototype object all instance properties passed in protoProps param (these are your custom methods and properties defined inthe call to Backbone.Model.extend():

  if (protoProps) _.extend(child.prototype, protoProps);

Finally child.__super__ is set to parents prototype, just in case you need it
 child.__super__ = parent.prototype;

This somewhat negates the whole Surrogate proxy pattern, but I think the big idea of the Surrogate is just to protect us devs changing child.prototype object and inadvertently changing Backbone.Model.prototype which would probably be unintended.

Finally, the child object returned is set into app.MyModel.


Step 4.
Now you can call new on your new Constructor function, app.MyModel, and make a new object instance that inherits from it
   var myModelInstance = new app.MyModel();

When you new app.MyModel() is called, then the constructor function which was previously (typically) assigned in Backbone.extend()  to child is executed
i.e.
   child = function(){ return parent.apply(this, arguments); };

....inside that fn, "parent" is Backbone.Model and parent.appy() calls the Constructor fn that is Backbone.Model i.e. defined in Step 1. Attributes are setup and initialize() is called.

Because this is a function within a function, a nested function within extend(), then due to Javascripts Closure support the value of "parent" exists as it did at the time this constructor function was declared when extend() was executed in Step 3. (i.e. within a closure). Thats some more neat magic.

...and thats it!


You can also call app.MyModel.extend({...}) to create subclasses.



References:
Backbones annotated source
Stoyan Stefanovs "Javascript Patterns"  
JS Definitive Guide











Comments

Post a Comment

Popular posts from this blog

deep dive into Material UI TextField built by mui

angular js protractor e2e cheatsheet

react-router v6.4+ loaders, actions, forms and more