Back to basics: prototypal inheritance
JavaScript flavoured inheritance differs somewhat from the traditional OOP based programming. Even though now we have class
keyword in ES6 to pretend that JavaScript can wear a disguise like OOP, it's still prototype-based inheritance under the hood.
Inherited Vs. Owned
Objects have properties (method is a special type of property whose value is a function), and these properties can be defined on the object itself or inherited from its prototype chain.
When a property is not found on the object itself, JavaScript engine inspects the prototype chain to find the said property. A property is considered not found if it's not available on any prototype (or link) on the prototype chain. So if a property is found in an earlier link of the chain, then it will shadow the same property found in the later link of the chain.
In a sense, JavaScript inheritance is finding shared properties available on a common breadcrumb.
There are a few ways of inheriting from an object in JavaScript:
- Using function to return you a base object. All properties you want to inherit is already on the base object. But instead of inheriting it, you're making a copy of it to use as part of this object.
- Using
Object.create(baseObject)
to set upbaseObject
as the first link on the prototype chain. Created object will have__proto__
property (depending on the JavaScript engine, this property may not be available to you, and ES6 also said this property should not be available to you) - Using constructor and prototype chain, this is the most common or textbook style of inheritance you will see and it's also the main topic of this post.
Constructor and Prototype
If you don't know this yet, constructor manipulates this
to create instance-owned properties and prototype manipulates __proto__
to create instance-shared properties. This is the biggest difference between them.
The created instance will have two special properties, and it's rather interesting why inheritance is indicated via not one, but two properties:
-
constructor
, used to indicate which constructor it was created from -
__proto__
, first link of the prototype chain. Since it's a chain, in order to get to the second link, all you need to do is continue adding__proto__
property to the path until you reachnull
. In other words, if you are to write a lookup function, it would probably look like this:
function lookupPrototypeChain(obj, prop) {
// this is not an object, so it's not a valid lookup
if (obj !== Object(obj)) return undefined;
while (obj.__proto__ !== null) {
if (obj.hasOwnProperty(prop)) {
return obj[prop];
}
// lookup the next item in the chain
obj = obj.__proto__;
}
// nothing is found, return undefined explicitly
return undefined;
}
__proto__
vs. prototype
Before jumping into an example, let's take a look at the difference between __proto__
and prototype
.
__proto__
property is available on any JavaScript object to get its prototype chain whileprototype
is a property on the constructor function that is used to generate__proto__
object for all instances created by the constructor function.
Once that's cleared up, let's take a look at an example of inheritance using constructor and prototype:
function A() {}
A.prototype.isBored = function() {
return Math.random() > 0.5;
};
function B() {}
// B gets A's prototype as its `__proto__`
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
B.prototype.haveFun = function() {
console.log('Hooray!');
};
var b = new B();
// { constructor, __proto__, haveFun }, B's prototype
console.log(b.__proto__);
// constructor is function B, is the same as b.__proto__.constructor
// it's basically saying you don't have `constructor` property
// but your prototype chain does, so let's return that
b.constructor === B;
// makes sense, huh?
b.constructor.prototype === B.prototype;
// constructor of any function is the top-level Function itself.
// You don't get A here because prototype chain doesn't live on `constructor` property, it lives on `__proto__` property
b.constructor.constructor === Function;
// { constructor, __proto__, isBored }, A's prototype
console.log(b.__proto__.__proto__);
// next link on the prototype chain is A
// again this is the same as b.__proto__.__proto__.constructor
b.__proto__.constructor === A;
b.__proto__.constructor.prototype = A.prototype;
// Again, this is top-level Function
b.__proto__.constructor.constructor === b.__proto__.constructor;
Constructor and instantiation
Since constructor is just a function you can invoke it directly, it is possible to just call it to evaluate. However, the caveat is that it might involve calling this
inside the constructor function. Under strict mode, JavaScript engine would result in an error when executing the function, so beware of its usage.
Other than invoking directly, the most common usage of constructor is to call it using the new
operator. Here is what StackOverflow says about the process http://stackoverflow.com/questions/1646698/what-is-the-new-keyword-in-javascript
In short, the process goes like this: __proto__
object is generated from prototype
first, bind this
to current instance, execute constructor function, and return. This means a few things:
Inside the constructor function,
this
can only refer to properties on the prototype object. You can't call owned properties outside of the constructor function because it's not declared yet.Even if you don't specify a
prototype
property, one is added automagically when you instantiate.this
insideprototype
methods and constructor function refer to the instance, not theprototype
object or the constructor function. So if you want to refer to the prototype object itself inside a prototype method, you can either refer it asConstructor.prototype
orthis.constructor.prototype
Changing and Breaking things
If you modified prototype
function after you instantiated, the two instances would share their .constructor.prototype
property, but their .__proto__
is different. The reason is that on their prototype chain, they are pointing to the same object (.prototype
) from which __proto__
is generated from while __proto__
is a snapshot of what prototype
looks like at the time of instantiation.
Here is an example:
function A() {}
A.prototype.sayYo = function() {
console.log('yo');
};
var a1 = new A();
A.prototype.sayHo = function() {
console.log('ho');
};
var a2 = new A();
a1.constructor.prototype === a2.constructor.prototype;
a1.__proto__ === a2.__proto__;
If you modified constructor function after you instantiated, you need to ensure that prototype is set again. Otherwise, you're overwriting the function.
instanceof
operator
In JavaScript, you can use instanceof
operator to check if an object is an instance of a constructor.
A function object has an internal method called HasInstance
that takes a value and compares its prototype with the function (or constructor)'s prototype. If you're to write instanceof
yourself, you'd end up with something like this
function isInstanceOf(func, val) {
if (val !== Object(val)) return false;
while (val.constructor.prototype !== null) {
if (val.constructor.prototype !== Object(val.constructor.prototype)) {
throw new TypeError('input object does not have a prototype chain');
}
if (val.constructor.prototype === func.prototype) {
return true;
}
val = val.__proto__;
}
return false;
}
Prototypal inheritance vs. OOP inheritance
Here is a quick summary of their differences
| Terms | JavaScript | OOP |
|------------------|------------|-----|
| private variable | closure | `private` |
| private function | declared inside constructor function | `private` |
| public variable | this.varName | `public` |
| public function | this.methodName | `public` |
| static variable | Constructor.varName | `static` |
| static function | Constructor.prototype.methodName | `static` |
In the end
Depending on your usage of objects, here is a brief summary of things you can do with each way of inheriting an object
| | function | `Object.create` | constructor/prototype |
|-----------------------|----------|-----------------|-----------------------|
| has `constructor` | false | false | true |
| has `__proto__` | false | true | true |
| properties are shared | false | true | true |
| properties are owned | true | false | true |
| customise instance | true | false | true |
Sources: