JavaScript Programing #2: Inheritance and Prototype chain
JavaScript do not have the conventional OOP keywords such as 'class', so the syntax is slightly different from C++ and Java if you want to organize your code in terms of objects. There are lots of great tutorials on Google about how to make public and private variables/member functions, so I am not going to cover these today. I will cover a slightly more confusing topic, which is inheritance in JavaScript, achieved through prototype chaining.
Lets say we have a class called Car. Nice and simple.
'use strict'; var sys = require('sys'); function Car(plate, owner) { // you can assign the property to this this._plate = plate; this._owner = owner; } Car.prototype.details = function() { console.log('plate: ', this._plate, ', Owner:', this._owner); }; function Toyota(model, owner) { Car.call(this, 'no plate!', owner); this._model = model; } // we are basically assigning what methods Toyota should have based on // what methods Car has Toyota.prototype = Object.create(Car.prototype); // we can assign extra methods to the prototype of Toyota Toyota.prototype.recall = function() { console.log(this._plate, 'is getting recalled'); }; var myToyota = new Toyota('yaris', 'Alice'); myToyota.details(); myToyota.recall();
If you run this code, you will see
plate: Rand , Owner: BennyChen
Now if I want to inherit from this definiion of car, and make a Toyota out of it?
Think of prototype in JavaScript as assignments to the prototype property.
If we do some system inspecting, as the following
console.log("Toyota's prototype", sys.inspect(Toyota.prototype, true), "\n"); console.log("Toyota's properties", sys.inspect(Object.getPrototypeOf(Toyota), true), "\n"); console.log("Toyota's prototype's prototype", sys.inspect(Object.getPrototypeOf(Toyota.prototype), true), "\n");
You will see the following output
Toyota's prototype { recall: { [Function] [length]: 0, [name]: '', [arguments]: [Getter/Setter], [caller]: [Getter/Setter], [prototype]: { [constructor]: [Circular] } } } Toyota's properties { [Function: Empty] [length]: 0, [name]: 'Empty', [arguments]: null, [caller]: null, [constructor]: { [Function: Function] [length]: 1, [name]: 'Function', [arguments]: null, [caller]: null, [prototype]: [Circular] }, [bind]: { [Function: bind] [length]: 1, [name]: 'bind', [arguments]: null, [caller]: null }, [toString]: { [Function: toString] [length]: 0, [name]: 'toString', [arguments]: null, [caller]: null }, [call]: { [Function: call] [length]: 1, [name]: 'call', [arguments]: null, [caller]: null }, [apply]: { [Function: apply] [length]: 2, [name]: 'apply', [arguments]: null, [caller]: null } } Toyota's prototype's prototype { [constructor]: { [Function: Car] [length]: 2, [name]: 'Car', [arguments]: [Getter/Setter], [caller]: [Getter/Setter], [prototype]: [Circular] }, details: { [Function] [length]: 0, [name]: '', [arguments]: [Getter/Setter], [caller]: [Getter/Setter], [prototype]: { [constructor]: [Circular] } } }
Toyota's prototype, recall, is easy enough to understand, since we declared that. Toyota's propertis are mostly the default properties associated with the JavaScript Function (you can get more details about the built-in Function here.
The important takeaway for the properties are, details function is not present in the properties or the prototype of Toyota.
So how does JavaScript know which details to call? As it turns out, it actually inspect the prototype of the prototype of the Toyota object to find a details function to call. When we do
Toyota.prototype = Object.create(Car.prototype);
the statement essentially copy Car.prototype to the properties of Toyota.prototype. By doing this, you would be able to find a details function to call. Note that Toyota.prototype.prototype is actually null; but as long as Toyota.prototype has a property 'details', it will be able to call the function you expect.
This is what we call prototype chaining. JavaScript will keep on inspecting the properties of prototype to the next level, until there are no more prototype to look for. The following extreme example fully illustrate the idea of prototype chaining.
'use strict'; var sys = require('sys'); function Car(plate, owner) { this._plate = plate; this._owner = owner; } Car.prototype.details = function() { console.log('plate: ', this._plate, ', Owner:', this._owner); }; function Toyota(model, owner) { Car.call(this, 'no plate!', owner); this._model = model; } Toyota.prototype = Object.create(Car.prototype); Toyota.prototype.recall = function() { console.log(this._plate, 'is getting recalled'); }; function Corolla() { Toyota.call(this, 'Corolla', 'Bob'); } Corolla.prototype = Object.create(Toyota.prototype); Corolla.prototype.insurance = function() { console.log('this car is not ensured!'); }; var myCorolla = new Corolla(); console.log("Corolla's prototype", sys.inspect(Corolla.prototype, true), "\n"); console.log("Corolla's prototype's prototype", sys.inspect(Object.getPrototypeOf(Corolla.prototype), true), "\n"); console.log("Corolla's prototype's prototype", sys.inspect(Object.getPrototypeOf(Object.getPrototypeOf(Corolla.prototype)), true), "\n");
The output of those inspection statements are
Corolla's prototype { insurance: { [Function] [length]: 0, [name]: '', [arguments]: [Getter/Setter], [caller]: [Getter/Setter], [prototype]: { [constructor]: [Circular] } } } Corolla's prototype's prototype { recall: { [Function] [length]: 0, [name]: '', [arguments]: [Getter/Setter], [caller]: [Getter/Setter], [prototype]: { [constructor]: [Circular] } } } Corolla's prototype's prototype { [constructor]: { [Function: Car] [length]: 2, [name]: 'Car', [arguments]: [Getter/Setter], [caller]: [Getter/Setter], [prototype]: [Circular] }, details: { [Function] [length]: 0, [name]: '', [arguments]: [Getter/Setter], [caller]: [Getter/Setter], [prototype]: { [constructor]: [Circular] } } }
Note now Car's prototype becomes Corolla's prototype's prototype. JavaScript will basically perform the same set of action as we did here to find a member function to call.
This programming model has significant implications. Since JavaScript has to go up the chain to find the right prototype to call, complicated inheritance pattern would not be so good for JavaScript code that has a strict performance requirement. It also means a programmer would be able to inspect the inheritance hierary of a JavaScript object with ease.
Hopefully I have made prototype chaining very clear in the tutorial. If it is still not clear, please leave a comment and I will try by best to answer. All the code in this blog can be found here Have fun with JavaScript!