Wednesday, November 21, 2012

On Prototypes, Inheritance and 'this' in JavaScript

The Great Debate

Another thread on the JSMentors mailing list on the merits of Prototype vs. Classes in JavaScript has prompted me to write this. I'm not here to project the 'right' or 'wrong' way to write JavaScript. It is up to you to use your own judgement on the best way to write your programs. I think the debate at it's base is flawed. It's not about Prototype vs. Classes. It's really about Prototype vs. Not Prototype. Either way, it's really important that if you are to choose one way or another, you need to have a deeper understanding of Prototypes and Inheritance in JavaScript.

On Prototypes

Prototypes in JavaScript are an implementation of the Prototype-based programming style of object-oriented programming. As per the Wikipedia article:

Prototype-based programming is a style of object-oriented programming in which classes are not present, and behavior reuse (known as inheritance in class-based languages) is performed via a process of cloning existing objects that serve as prototypes.

Based on this description, I can posit the following:

  • JavaScript is object-oriented;
  • but classes are not present in JavaScript;
  • however, inheritance is possible by cloning and reusing existing objects.

Classes do not exist in JavaScript. Not really a debatable point. However, JavaScript is such a powerful language that you can mimic a near-perfect Class system, which means you can (in a somewhat convoluted way) program in a Class 'style' in Javascript. This is where I think a lot of people get tripped up, because they make assumptions that JavaScript will behave just like any other Class based programming language.

On Inheritance

I won't argue about whether or not you should use Inheritance in JavaScript. What I do want to talk about is how Inheritance really works in JavaScript, hopefully to dispel any mystery behind it.

Prototypes in JavaScript are how the language expects a programmer to implement Inheritance. How it works is that each object in JavaScript has a hidden link system, the Prototype. You can link properties and methods of an object to other objects, so if you called 'toJSON' on an object, and it doesn't exist on an object, it will look in it's prototype to see if a 'toJSON' method is linked to another object, and call it there. Object prototypes work in a chain fashion, so you can have (theoretically) infinite chain of objects that will look up properties and methods until it gets to the beginning of the chain.

There are a lot of subtle nuances to using Prototypes. Just as with any language, there are a set of conventions or rules on how to build programs in JavaScript that may not be readily apparent to everyone.

JavaScript Objects 101

The first thing you need to remember about JavaScript is that everything is an object, excepting a few primitives (such as integers, floats, undefined and null). Functions are objects. Strings are objects.

// A new Object, the base of all creation in JavaScript
var myObject = {};

// You can add properties and methods to Objects:
myObject.id = '123';
myObject.sayHi = function() {
  return 'hi';
};

All objects created in JavaScript have Prototypes, and they are all chained together to the base Object. If you call a method on an object that doesn't exist, it will look up the chain.

myObject.sayYo();

>> TypeError: Object has no method 'sayYo'

Since myObject is just a plain old object at the top of the Prototype chain, it has nowhere else to look for methods. A better example is a string, which is one level down the chain. Remember, a string is an object too.

var myString = "i am a string";

// toUpperCase() is a prototype method on String object

console.log(myString.toUpperCase());
>> "I AM A STRING"

// capitalize() is not an existing method on a String object, and does not exist up the prototype chain.

console.log(myString.capitalize())
>> TypeError: Object has no method 'capitalize'

By modifying the prototype of a String, we can add methods in the chain and make them available to all objects that point the the String prototype. So this is possible:

String.prototype.capitalize = function() {
  // code to return string capitalized...
};
console.log(myString.capitalize());

>> "I am a string"

You can put it all the way at the base of the chain (probably not the best idea, but it's possible):

Object.prototype.capitalize = function() {
  // code to return string capitalized...
};

console.log(myString.capitalize());
>> "I am a string"

By modifying the prototype of String, you are creating a sort of template for methods and properties on all objects that point to that Object's prototype. A string as you know it in JavaScript can be referred to as an instance of a String object. But in reality, it's just a brand new object with it's prototype pointed at the String prototype. So when toUpperCase() is called on a string 'instance', it's actually looking up one level to the String prototype, since on that particular instance the method did not exist. The toUpperCase() method does not actually exist on each string, but linked via Prototype.

Alright, so how do we create 'instances' in JavaScript?

Using the 'new' keyword

In JavaScript, to create the framework to produce 'instances' of objects, you can follow a few simple steps.

1. Define a Constructor

A Constructor function is convention in JavaScript to create the base prototype to which all 'instances' will point to. In this function, you can initialize variables from arguments and set up anything else you need.

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

2. Define the Prototype as needed

Generally, for any properties or methods that all 'instances' can share, you can build into the prototype. You can do it like so:

Animal.prototype.speak = function() {
  return '???';
};

Or alternatively, you can do it all in one go:

Animal.prototype = {
  speak: function() {
    return '???';
  }
};

The important takeaway here is that the Prototype itself is just an Object. You can add properties and methods to it just like a regular object. The difference is that JavaScript treats it a bit differently by linking it to other Objects automatically when using the 'new' keyword.

3. Create an 'instance' of a Constructor

var myAnimal = new Animal('Bird');

console.log(myAnimal.name);
>> 'Bird'

console.log(myAnimal.speak());
>> '???'

It's important to remember that when using the 'new' keyword, JavaScript roughly does the following:

  • Creates a new empty Object
  • Allows you to set any properties of the newly created Object (inside the Constructor)
  • Points the new object's Prototype to the Prototype of the Constructor
  • Return the new object for you to use

So with that out of the way, the next thing we need to talk about is the infamous 'this' in Javascript.

Referencing properties and methods using 'this'

A lot of programmers get confused by 'this' in JavaScript, and rightly so. This is another part of the language that unless you know the rules and conventions, it may appear to be voodoo magic. Hopefully I can clear that up a little bit.

While referring to 'this' is possible anywhere in JavaScript, the most common use case is when you are referring to an 'instance' of an object.

In the Constructor function for Animal, you may have noticed:

this.name = name;

When referring to 'this' in the Constructor, you are always referring to the 'instance' or the newly created object. So by creating a new Animal, it's creating a new object with the 'name' property set to the name argument that was passed in. This is unique to each 'instance' created using the 'new' keyword, whereas anything defined in the Animal Prototype is shared by each 'instance'.

When defining methods on the Prototype for Animal, 'this' will always refer to the current 'instance'.

Animal.prototype.getName = function() {
  return this.name;
};

Caveat: JavaScript allows you to invoke functions with 'call' and 'apply' to change the context of 'this', but generally speaking, when used as JavaScript intended for Prototypes, you can rely on 'this' referring to the 'instance' of Animal.

Inheritance using Prototypes

By using the Prototype in JavaScript, Inheritance is possible, but not obvious or intuitive. As an example, we can create a Bird Constructor that inherits from Animal. By doing this, we can inherit all the characteristics of an animal, but have the ability to define different aspects of Bird as needed. So how do we do this? Follow these steps:

1. Create a new Constructor function

function Bird(name) {
  // Set up anything you like here...
};

2. Set the new Constructor's prototype to a copy of the original Constructor.

Remember, inheritance is achieved by creating a copy of an object to serve as the Prototype. By using the 'new' keyword on Animal, we're creating a new object based on the Animal Prototype and then using it to set the Bird Prototype.

Bird.prototype = new Animal();

Caveat: when doing this, notice that we don't pass in an argument to new Animal(). This can cause problems on Constructors that expect arguments. It's a good idea on Constructors that you expect to inherit from, to check for arguments before doing anything:

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

3. Add more methods to the new Constructor's Prototype

Now you can define or redefine Prototype methods for Bird. We can override the 'speak' method defined on Animal:

Bird.prototype.speak = function() {
  return 'chirp';
};

By modifying the Bird prototype with the speak method, when invoked on a Bird instance it will find the speak method first in the Bird prototype and won't go any further up the chain to Animal.

4. Invoke the original Constructor on the new Constructor (optional)

This is completely optional, but if you want the original Constructor function to also run on your newly inherited Constructor when 'new' is called, you have to do it yourself. This does not happen automatically in JavaScript.

function Bird(name) {
  Animal.call(this, name);
  // Set up anything you like here...
}

So what did we just do here? We called the Animal Constructor in the context of Bird, and passed the name argument. So this line in the Animal Constructor:

this.name = name;

is run when creating a new Bird, in context of Bird. The context of 'this' changes to Bird, but only on one call. Now we don't have to copy that particular line into the Bird Constructor, as it is already called through Animal.

Following these steps will get you basic inheritance in JavaScript as it is generally intended. But of course there are other ways to pull off inheritance.

One Alternative to using Prototypes and Inheritance

Probably the most debated topic among JavaScript developers is the use of the 'new' keyword when creating new objects. The language itself bases everything around the idea of creating instances of objects using 'new', but it's also possible in JavaScript to avoid using 'new' altogether.

To mimic Prototyping and Inheritance, one alternative that I've seen often is creating what is essentially a factory function. Let's update our previous examples:

function Animal(name) {
  var animal = {};
  animal.name = name;
  animal.speak = function() {
    return '???';
  };
  animal.getName = function() {
    return animal.name;
  };
  return animal;
}

function Bird(name) {
  var bird = Animal(name);
  bird.speak = function() {
    return 'chirp';
  };
  return bird;
};

var bird = Bird('Polly');

bird.speak();
>> 'chirp'

We are stepping around using Prototypes by exploiting closure in JavaScript. When Animal is called, it creates a new variable, animal, which is a new object. It then adds properties and methods to it, and returns the new object, just as using the 'new' keyword would. In methods that are defined on animal inside the Animal function, due to the way closures work, a reference to the animal variable is always available inside each function, avoiding having to use 'this'.

Keep in mind that while this factory pattern behaves very similar to Prototypes, it is not the same. The main difference is that any new object returned from the Animal or Bird method will contain complete copies of methods and properties instead of the more efficient link to one copy in the Constructor prototype. They are also indistinguishable from any other plain old objects. Running instanceof will return Object instead of the original Constructor.

Two sides of the same coin

Looking at these two patterns for programming in JavaScript, there are a lot of similarities. The factory method nearly mimics the Prototype system JavaScript has built in, but loses out on some optimizations that JavaScript can provide for you. In losing some optimizations, it arguably is more readable and understandable for some folks. I hope by better understanding the mysteries of Prototypes and Inheritance in JavaScript, you can make a more informed decision as to what you should use for your project.

No comments:

Post a Comment