I am always baffled by the complicated graphs people draw to explain the ES3 concept for inheritance. I don't think inheritance is the right word for what is in the language. The more appropriate word would be composition. Prototypal composition, because an object called prototype
is used to bring in that magic. If you think this means JS doesn't have inheritance, don't forget that the goal is not to be able to inherit. What we are trying to achieve is code reuse, small memory footprint and polymorhism (defined here as an ability to mutate object slightly in relation to their generic prototypes).
ES3 in it's attempt to imitate Java (for familiarity purposes) emphasised the role of the class in instances creation. The idea was/is as follows
//1. Functions also serve as classes. There is no separate `class` keyword.
function Animal( sound ) {
this.sound = sound;
}
//2. Functions have a magic object property called `prototype`.
//Adding properties to prototypes is like extending a class.
//All instances are going to have access to those properties - they are *shared* by all instances.
Animal.prototype.makeSound = function() {
alert( this.sound );
}
//3. Now create an instance of an Animal
var lion = new Animal( 'roar' );
The idea was a good compromise. The best thing about it is it's opennes and good memory management - all common properties are shared in memory, which makes many instances cheap. Everything is tied by this
keyword, which is loosely scoped. This looseness enables methods borrowing.
function Human( sound ) {
this.sound = sound;
}
//this one is just to distinguish Human from Animal
Human.prototype.createFire = function() {
//implement how it's done
}
Human.prototype.makeSound = Animal.prototype.makeSound;
This is a very efficient and expresive code reuse pattern. The downside of this is it is not very formal - it's just an assigment statement that happens to work in a way that is useful most of the time. The idea here is if you don't necesarilly care about setting up A Human class
to inherit from Animal, but just want to borrow a couple of properties, then yes, you can. This is still object oriented pattern (we have inheritance), but not the kind that requires you to write a lot of boilerplate code of explicitly setting up inheritance just to borrow a few properties. This situation, ocurring in less dynamic languages was best described as follows:
You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.” — Joe Armstrong
The ES3 pattern's emphasis was clearly on the class/function. The improvements implemented in the new version of JavaScript (called Ecmascript 5) put the emphasis back on the prototype. Here's how the Animal and Human instances could be created
var AnimalProto = {
makeSound: function() {
alert( this.sound );
}
};
var HumanProto = {
makeSound: AnimalProto.makeSound,
createFire: function() {
//implement how it's done
}
}
var lion = Object.create(AnimalProto, {
sound: {
value: 'roar'
}
});
var australopithecus = Object.create(HumanProto, {
sound: {
value: 'ouououou'
}
});
The nature of composition or mixin is very visible here. It's obvious that prototypes are objects and can have their properties changed at runtime, even after some objects were created with those prototypes. As importantly this effectively removes confusion about using types in Javascript. They are not to be used. The philosophy here, whether you like it or not is:
- Everything is an object
- Everything's methods can be borrowed and used on anohter type of object
- If anohter object implements properties required by a borrow method, it will just work (an example of this is makeSound, which only requires that its host object have a property
sound
). This is known as duck typing. - Prototypes can be updated even after the object, that is linked to a prototype, was created.
Example:
//as time passed, people domesticate animals
//getting a handle on the australopithecus prototype
var australoProto = Object.getPrototypeOf( australopithecus );
//this is the same object as the previously created
australoProto === HumanProto; //true
//now all instances of Human will know how to tame animals
australoProto.tameAnimal = function( animal ) {
//in this function we can (ignoring coupling)
//add a method to an instance of animal
if (animal) {
animal.followHuman = function() {
}
}
}