Tuesday, August 17, 2010

Javascript Inheritance Done Right

I've seen a number of different ways of implementing Javascript inheritance. The real test to see if inheritance is working correctly is that the instanceof operator works correctly. So all the approaches that copy methods from the base to the subclass are out of the question because they do not correctly setup the prototype chain. Another consequence of inheritance by copying is that if you change the prototype after the object has been instantiated, the object does not magically inherit the added property.

This leads me to the most common approach which does correctly set up prototype chain but has a few problems:

function Animal(name) {
  this.name = name;
}

// This style of setting the prototype to an object
// works for classes that inherit from Object.
Animal.prototype = {
  sayMyName: function() {
    console.log(this.getWordsToSay() + " " + this.name);
  },
  getWordsToSay: function() {
    // Abstract
  }
}

function Dog(name) {
  // Call the parent's constructor
  Animal.call(this, name);
}

// Setup the prototype chain mmm... calling
// the Animal without the required params?
Dog.prototype = new Animal();

Dog.prototype.getWordsToSay = function(){
  return "Ruff Ruff";
}

var dog = new Dog("Lassie");
dog.sayMyName(); // Outputs Ruff Ruff Lassie
console.log(dog instanceof Animal); // true
console.log(dog.constructor); // Animal ???? That's not right
console.log("name" in Dog.prototype)// true, but undefined

Alright, what's going on?
  • Dog.prototype now has a property called "name" that is set to undefined.
    That wasn't intentional. I knew that call to Animal's constructor was funny. Though that won't cause a problem, because we add a "name" to the object in the constructor, it's not very elegant
  • dog (the instance) has a constructor property but it points to Animal,
    that's just wrong

How can we fix that? Here's a first try

// This is a constructor that is used to setup inheritance without
// invoking the base's constructor. It does nothing, so it doesn't
// create properties on the prototype like our previous example did
function surrogateCtor() {}

function extend(base, sub) {
  // Copy the prototype from the base to setup inheritance
  surrogateCtor.prototype = base.prototype;
  // Tricky huh?
  sub.prototype = new surrogateCtor();
  // Remember the constructor property was set wrong, let's fix it
  sub.prototype.constructor = sub;
}

// Let's try this
function Animal(name) {
  this.name = name;
}

Animal.prototype = {
  sayMyName = function() {
    console.log(this.getWordsToSay() + " " + this.name);
  },
  getWordsToSay: function() {
    // Abstract
  }
}

function Dog(name) {
  // Call the parent's constructor
  Animal.call(this, name);
}

// Setup the prototype chain the right way
extend(Animal, Dog);

Dog.prototype.getWordsToSay = function(){
  return "Ruff Ruff";
}

var dog = new Dog("Lassie");
dog.sayMyName(); // Outputs Ruff Ruff Lassie
console.log(dog instanceof Animal); // true
console.log(dog.constructor); // Dog
console.log("name" in Dog.prototype)// false

Nice isn't it? Let's add some syntactic sugar to make it more user friendly.
  • Add a reference to the base class so we don't have to hard code it
  • Pass the object's prototype methods into the call


function surrogateCtor() {}

function extend(base, sub, methods) {
  surrogateCtor.prototype = base.prototype;
  sub.prototype = new surrogateCtor();
  sub.prototype.constructor = sub;
  // Add a reference to the parent's prototype
  sub.base = base.prototype;

  // Copy the methods passed in to the prototype
  for (var name in methods) {
    sub.prototype[name] = methods[name];
  }
  // so we can define the constructor inline
  return sub;
}

// Use the same animal from above
function Dog(name) {
  // Call the parent's constructor without hard coding the parent
  Dog.base.constructor.call(this, name);
}

extend(Animal, Dog, {
  getWordsToSay: function(){
    return "Ruff Ruff";
  }
});


One more step, I don't even like hard coding the name of the class in the methods in case I rename it, how can we fix that?

The solution is to wrap everything in a self executing function and create a reference to the constructor

Dog = (function(){
  // $this refers to the constructor
  var $this = function (name) {
    // Look, no hardcoded reference to this class's name
    $this.base.constructor.call(this, name);
  };

  extend(Animal, $this, {
    getWordsToSay: function(){
      return "Ruff Ruff";
    }
  });

  return $this;
})();

With this final approach, renaming the class or changing its parent requires changing a single place (for each change).

5 comments:

  1. Hello, Juan!

    I get one mistake in your code:

    Animal.prototype = {
    sayMyName = function() {

    '=' must be replaced with ':'

    Also I want ask you a question: why i can't change method if it declared as 'this.method =' instead of 'Animal.prototype.method = '?
    http://jsfiddle.net/m7m9E/

    ps. It is important to decclare method in constuctor caus of using some "private" variables.

    With best regards,
    Dmitry

    ReplyDelete
  2. Hello Dmitry, sorry it took me so long to reply. I've fixed the mistake you pointed out, thanks. If I understand it right, you would like to declare methods from the constructor. That is fine, but you need to understand that methods declared from the constructor modify the object directly. In your example, your base (Animal) defines getWordsToSay on the object itself. And your base class declares getWordsToSay on the prototype. Therefore, when you create an instance of Dog, when the Animal constructor is run, it creates getWordsToSay on the object itself and the one you attached to Dog.prototype can't be reached. However, if you define getWordsToSay from Dog's constructor, after running Animal's constructor, then you're overwriting it also directly on the object and you should achieve the desired effect.

    I think the lesson is, if you want something that is overridable, you should not set it from the constructor, you should set it on the prototype.

    http://jsfiddle.net/mendesjuan/m7m9E/1/

    ReplyDelete
  3. Just out of curiosity: Why do you use "sub.base = base.prototype;" instead of "sub.prototype.base = base.prototype;" in your "extend" function? If you use the latter, you can directly use "this.base.constructor" in the constrcutor do not need to create a complicated function object to avoid naming the subclass in its constructor.

    ReplyDelete
  4. @shiin but that would still require naming the class twice: once in its function constructor definition and again as an argument to 'extends'.

    What I don't understand is how 'sub.base.constructor' is set. According to MDN, '.constructor', "Returns a reference to the Object function that created the instance's prototype." But 'Animal' is not an instance of an Animal, but instead is a Function, so its prototype was created by Function, and therefore its constructor should be 'Function.prototype' (this corresponds with what I'm seeing in Firefox.) So 'base.constructor.call' will not call Animal's constructor, but instead Function's constructor. What I'm using is: 'sub.base = base;' in 'extends' and '$this.base.call(this, arguments)' in the sub class's constructor. Seems to be working.

    https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/constructor

    ReplyDelete
  5. Thanks for the article. I used it to construct the following method. It allows a much simpler syntax. In the prototype definition, there is a special function called _ctor that it will use for the constructor method.

    var Base = createClass({
    baseAttr: "",

    _ctor: function (param1, param2) {
    // ...
    },
    func1: function () {
    }
    });

    var Derived = createClass(Base, {
    derivedAttr: "",
    _ctor: function (param1) {
    this.baseCtor(param1, "hardcoded");
    },
    func3: function () {
    }
    });

    function createClass(base, prototype) {
    // If the prototype parameter is passed as the only argument,
    // we need to reverse the base and prototype parameters.
    if (typeof (base) == "object") {
    prototype = base;
    base = null;
    }
    var constructor = prototype._ctor;
    // If the passed in prototype does not have a constructor, we
    // need to define a default one.
    if (constructor == undefined) {
    constructor = function () { };
    } else {
    delete prototype._ctor;
    }

    // We want to start with a blank prototype and copy the
    // bases prototype if a base is specified.
    var newPrototype = new function CTor() { };
    if (base) {
    newPrototype = base.prototype;
    // Save a reference to the base constructor so that it
    // can be called from the derived class.
    newPrototype.baseCtor = base.prototype.constructor;
    }
    newPrototype.constructor = constructor;
    constructor.prototype = newPrototype;
    // Copy the new class's prototype to the prototype for
    // the constructor.
    for (var key in prototype) {
    constructor.prototype[key] = prototype[key];
    }

    return constructor;
    }

    ReplyDelete