Showing posts with label inheritance. Show all posts
Showing posts with label inheritance. Show all posts

Monday, October 6, 2014

Understanding prototypal inheritance

Many developers coming from a classical inheritance model find it difficult to grasp JavaScript's prototypal inheritance. In this post, I will explain how inheritance is achieved in JavaScript and what pitfalls to avoid.

Consider the following code where we create an instance of "Class" using code that looks like classical inheritance
function Class() {
    this.obj2 = {c: 'c'}
}

Class.prototype = {
    primitive: 'a',
    obj: { a: 'a' },
    arr: [1,2,3]
};

var inst1 = new Class();
var inst2 = new Class();

The following diagram explains which objects are being created. Solid lines are direct links and dashed lines are chaining objects through the prototype.


An equivalent way of creating an instance, in a more prototypal, albeit more verbose fashion is the following:
// Create an empty object chaining it to Class.prototype
var inst1 = Object.create(Class.prototype);
// Initialize the object using the constructor
Class.call(inst1);

The important thing to understand is that the prototype object is shared by all instances. A common pitfall is to instantiate an object/array on the prototype without realizing that mutating that object will modify it for all instances. In our example, the {a: 'a'} object and the [1,2,3] array is shared among the instances, whereas each instance has its own copy of {c: 'c'}
// The two instances share the same instance of obj
// which may be unexpected
inst1.obj.a = 'b';
console.log(inst1.obj); // {a: 'b'}
console.log(inst2.obj); // {a: 'b'}

// same for arrays (since array is a mutable object)
inst1.arr.push(4);
console.log(inst1.arr); // [1,2,3,4]
console.log(inst2.arr); // [1,2,3,4]

// to get around this, create the child objects in constructor
// now changing obj2 in instance1 does not affect instance2
inst1.obj2.c = 'd';
console.log(inst1.obj2); // {c: 'd'}
console.log(inst2.obj2); // {c: 'c'}
Note that this problem doesn't occur when sharing primitives on the prototype. When we read a property from an object, it will first look for the property in the specified object, if it doesn't find it, it will go up the prototype chain looking for that object. However, when you write to an object, it always writes to the specified object, not the prototype. For example

// Here we creating a new property on inst1, which now shadows 
// the "primitive" property on "Class.prototype"
inst1.primitive = 'b';
// Still reading from Class.prototype
console.log(inst2.primitive); // 'a'
// Read "obj" from the prototype and then write over the "a" 
// property on that "obj" (from the prototype)
inst1.obj.c = 5;
// Create a new object that writes a new "obj" property on inst2 
// directly and shadows "obj" on the prototype
inst2.obj =  {c: 6};

Lessons to learn:

  • Initializing immutable objects on a prototype keeps a single copy, so it saves memory and initialization code
  • Don't share mutable objects on the prototype unless you want changes in one instance to reflect in all instances.
  • Initializing primitives on the prototypes is always OK, because they can't be mutated directly through instances
 If you'd like to play around with the above code, go to http://jsfiddle.net/mendesjuan/cdaq5b11/3/

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).

Update

This article was written a while ago, when Object.create wasn't as well supported. The extend function could be simplified to be

function extend(base, sub, methods) {
  sub.prototype = Object.create(base.prototype);
  sub.prototype.constructor = sub;
  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;
}