Introduction #
Lately, I’ve been digging deeper into JavaScript (courtesy of this book, Secrets of the JavaScript Ninja). One question I kept asking myself as I went along: how does one actually achieve/implement inheritance? This post is a record of my learnings along the way.
Let’s start with the rudimentals, “what is inheritance and why?”:
- Inheritance: defining a general thing once and creating specialized versions which automatically get all of the general’s features plus their own unique ones
- Why inheritance: code reuse, polymorphism et cetera
First Attempt at Inheritance #
I read elsewhere that some of the modern JavaScript features are syntactic sugar
over what was already present in the language, specifically class and
extends. So as I was working through the “Object Orientation with Prototypes”
chapter, I got curious as to how inheritance could be achieved with prototypes.
Suppose we’ve got a Person:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, this is ${this.name}`);
};
And a Ninja who is a Person i.e. should inherit from Person:
function Ninja(name, skill) {
// we need to call the parent/superclass to initialize the `Person` parts of
// a Ninja object
this.skill = skill;
}
Ninja.prototype.fight = function () {
console.log(`${this.name} can fight at ${this.skill} level`);
};
This was my first stab at inheritance:
function Ninja(name, skill) {
// inherit from Person
Person.call(this, name);
Object.setPrototypeOf(Object.getPrototypeOf(this), Person.prototype);
// now init Ninja-specific stuff
this.skill = skill;
}
We can now do the following:
const n = new Ninja("Alice", "Advanced");
n.greet();
n.fight();
// Hello, this is Alice
// Alice can fight at Advanced level
A couple of notes:
- The goal I had in mind with
Person.call(this, name)is to achieve constructor delegation: instead of repeating the work that thePersonconstructor already does, just delegate to it directly - To use
Personas a constructor, we need to invoke it with thenewkeyword, however, this will just instantiate an entirely different object - Hence the usage of the
callmethod which lets us set thethisvalue in addition to providing the required arguments for thePersonconstructor - From what I know wrt OOP in other languages, the
Person.call(this, name)has to come before setting the properties specific toNinjajust in case initialzing any of theNinja-specific properties depends on thePerson-specific properties. The inverse cannot be true (none of thePersonproperties could depend onNinjasince the inheritance relationship flows one-way from Parent to Child)
So far so good.
Now, let’s go to this line:
Object.setPrototypeOf(Object.getPrototypeOf(this), Person.prototype);. My goal
here was to set up the prototype chain such that a Ninja object has access to
the Person methods (a Ninja is a Person). Unfortunately, I was being too
clever with the Object.getPrototypeOf(this) part since I could have as well
just used Ninja.prototype directly.
Functions’ __proto__ vs .prototype
#
When you create an object via a constructor function in JavaScript, that
object’s internal prototype (__proto__) is automatically set to the function’s
.prototype property. Worth emphasizing, a function’s .prototype is not the
same thing as the function’s own internal prototype __proto__.
Allow me to go over this distinction again: when you define a function in
Javascript, it comes with two different prototype-related things. Let’s consider
Person, it comes with:
Person.prototype: Every constructible function in Javascript gets a.prototypeproperty which is an object.- When you use the function as a constructor e.g.
new Person(...), this object becomes the prototype of the newly created instances. - When you add methods and properties to this
.prototypeobject, all instances will have access to them (it becomes part of their prototype chain). - Also worth pointing out now, this
.prototypeobject comes with a.constructorproperty which points back to the function itself. The property is non-enumerable which means it won’t show up infor...in,Object.keysand so on. - The
.constructorproperty is there so that when we createPersoninstances, we can retrieve the constructor function that was used if needed.
- When you use the function as a constructor e.g.
Person.__proto__( orObject.getPrototypeOf(Person)): This is the function’s own prototype.- Since functions are also objects, they get their own prototype chain.
- Note, for
Person, its__proto__is set toFunction.prototype. This means the functionPersonis itself an instance of the built-inFunctionconstructor. - Through its prototype chain, it inherits methods like
.callwhich we used earlier, as well as.apply,.bindamong others.
import assert from "assert";
function Person(name) {
this.name = name;
}
// Person.prototype is different from Person.__proto__
console.log(Person.prototype); // {}
console.log(Object.getPrototypeOf(Person)); // [Function (anonymous)] Object
assert(Object.getPrototypeOf(Person) !== Person.prototype);
// Person is an instance of Function
assert(Person instanceof Function);
// Hence Person's prototype is Function.prototype
assert(Object.getPrototypeOf(Person) === Function.prototype);
Fixing the Inheritance Setup Code #
Now, back to the my ‘inheritance’ code. Let’s use Person.prototype in lieu of
Object.getPrototypeOf(this):
function Ninja(name, skill) {
// inherit from Person
Person.call(this, name);
assert(Object.getPrototypeOf(this) === Ninja.prototype);
Object.setPrototypeOf(Ninja.prototype, Person.prototype);
// now init Ninja-specific stuff
this.skill = skill;
}
Rewritten this way, the issue jumps out: why is the prototype of Ninja being reset on every single instantiation? It’s unnecessary and a potential performance problem
To fix it, let’s set up the prototype chain once outside of the constructor:
function Ninja(name, skill) {
// inherit from Person
Person.call(this, name);
// now init Ninja-specific stuff
this.skill = skill;
}
Object.setPrototypeOf(Ninja.prototype, Person.prototype);
Digging into .prototype
#
Let’s go back to the Person function for a moment this time minus the greet
method:
function Person(name) {
this.name = name;
}
As already mentioned, every function in Javascript automatically gets a
.prototype property when it’s created. If we print this object, it seems
empty:
console.log(Person.prototype); // {}
Properties can be enumerable or non-enumerable. If non-enumerable, they
won’t show up when you use common traversal or inspection methods (e.g. when
using console.log, Object.keys,Object.values,Object.entries or
JSON.stringify).
There are other property attributes too besides enumerability.
Property Descriptors #
In fact, if we take the property key, value and attributes, these encompass what’s referred to as “property descriptors”. The attributes govern behavious like:
- Can the property be deleted from the object?
- Can its value be changed?
- Will it show up in
for...in, Object.keys and so on? - Can the property’s attributes be reconfigured
- When accessing it do we implicitly use getter/setter functions or just get/modify the value directly
There are two kinds of properties descriptors:
- data descriptor: has value that may or may not be writable. Configured
throught he following attributes: (writable, configurable, enumerable):
- writable (true/false): if true, the value of a property can be modified via the assignment operator
- configurable (true/false):
- if set to false, the property cannot be deleted (e.g.
delete obj.foo) - also, if false, the descriptors of the property cannot be modified, e.g.
set
writablefromtruetofalselater on, or change from data to accessor descriptor
- if set to false, the property cannot be deleted (e.g.
- enumerable (true/false):
- if false, the property will not show up during commonly used object
traversal and inspection methods, such as
for-inloops,console.log,Object.keys,Object.entriesandJSON.stringify
- if false, the property will not show up during commonly used object
traversal and inspection methods, such as
- accessor descriptor: property described by getter-setter pair of functions
that are set via the
get/setattributes- get: getter function, cannot be defined if
valueand/orwriteableare defined - set: setter function, cannot be defined if
valueand/orwritableare defined
- get: getter function, cannot be defined if
Property descriptors are defined and configured through the
Object.defineProperty static method.
For example, given ninja which is a Ninja instance, let’s add a color
property:
const ninja = new Ninja("Eve", "Beginner");
Object.defineProperty(ninja, "colour", {
enumerable: true,
configurable: false,
writable: false,
value: "black",
});
// fails
ninja.colour = "blue";
// fails
delete ninja.colour;
Back to .prototype
#
Back to .prototype, if we want all property names directly on an object
regardless of whether they’re enumerable or not, we can use
Object.getOwnPropertyNames to get them.
Let’s do so with Person.prototype:
console.log(Object.getOwnPropertyNames(Person.prototype));
// ["constructor"]
Person.prototype has a .constructor property. The value of this property is
the function Person itself i.e. it points back to Person. This means that
given any instance of Person we can always retrieve the function that was used
to construct it:
assert(Person.prototype.constructor === Person);
const bob = new Person("Bob");
assert(bob.constructor === Person);
Let’s get more details on the .constructor property:
function Person(name) {
this.name = name;
}
const desc = Object.getOwnPropertyDescriptor(Person.prototype, "constructor");
console.log(desc);
This prints:
{
value: [Function: Person],
writable: true,
enumerable: false,
configurable: true
}
Informally, we could say that the .constructor property is there so that
objects can remember which specific function was used to construct them. It’s
non-enumerable since we often don’t need to nor have to access it.
.prototype Prototype Chain
#
Now that we’ve seen what’s inside Person.prototype, let’s look at its
prototype chain. The prototype of Person.prototype is Object.prototype:
assert(Object.getPrototypeOf(Person.prototype) === Object.prototype);
And the prototype of Object.prototype is null
assert(Object.getPrototypeOf(Object.prototype) === null);
Therefore, the full prototype chain of an instance of Person is:
alice --> Person.prototype --> Object.prototype --> null
The function Person itself has the following prototype chain:
Person --> Function.prototype --> Object.prototype --> null
As for an instance of Ninja
(const alice = new Ninja("Alice", "intermediate")):
alice --> Ninja.prototype --> Person.prototype --> Object.prototype --> null
A Different Approach for OOP #
Another approach is to set the .prototype of the constructor to an instance of
the parent. This is what the book goes for. In our case:
function Ninja(name, skill) {
Person.call(this, name);
this.skill = skill;
}
// has to come before adding methods to the Person prototype
Ninja.prototype = new Person();
As expected, all Ninja instances automatically get access to the parent
Person methods:
const dan = new Ninja("Dan", "intermediate");
dan.greet();
dan.fight();
console.log("dan instanceof Ninja:", dan instanceof Ninja);
console.log("dan instanceof Person:", dan instanceof Person);
Which outputs:
Hello, this is Dan
Dan can fight at intermediate level
dan instanceof Ninja: true
dan instanceof Person: true
Btw, it’s probably a good idea to briefly mention how instanceof works. From
MDN:
the “instanceof operator tests to see if the prototype property of a constructor
appears anywhere in the prototype chain of an object”.
Back to the code, while it does work, it breaks one key expectation which is
that the prototype object of Ninja is expected to have a constructor property.
Let’s rectify that:
// has to come before adding methods to the Person prototype
Ninja.prototype = new Person();
Object.defineProperty(Ninja.prototype, "constructor", {
value: Ninja,
writable: true,
enumerable: false,
configurable: true,
});
All good. Now the prototype chain of an instance of Ninja is as follows:
dan --> person instance(Ninja.prototype) --> Person.prototype --> Object.prototype --> null
ES6 Classes and the extends Keyword
#
With ES6, we’ve now got the class keyword and extends for inheritance.
Should make transitioning from class-based OOP languages to the prototype-based JS much easier.
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, this is ${this.name}`);
}
}
class Ninja extends Person {
constructor(name, skill) {
super(name); // calls Person's constructor
this.skill = skill;
}
fight() {
console.log(`${this.name} can fight at ${this.skill} level`);
}
}
Also worth pointing out, it doesn’t use a instance of the parent for inheritance:
const desc = Object.getOwnPropertyDescriptor(Ninja.prototype, "constructor");
console.log(desc);
Which ouptuts:
{
value: [class Ninja extends Person],
writable: true,
enumerable: false,
configurable: true
}
Note that Ninja.prototype instanceof Person evaluates to true, which is
expected. But it would be misleading to conclude from this that
Ninja.prototype is an actual instance of Person. What instanceof checks is
whether Person.prototype appears somewhere in the prototype chain of
Ninja.prototype nothing else more.
Static Methods #
For extras, suppose we want to add a fight static method on Ninja. With ES6
classes, it’s as follows:
class Ninja extends Person {
...
static fight(ninja1, ninja2) {
console.log(`${ninja1.name} fights ${ninja2.name}`);
}
}
const alice = new Ninja("Alice", "advanced");
const dan = new Ninja("Dan", "intermediate");
Ninja.fight(alice, dan);
The equivalent of this when using functions as constructors:
function Ninja(...){...}
Ninja.fight = (ninja1, ninja2) => {
console.log(`${ninja1.name} fights ${ninja2.name}`);
};
Since functions are objects, we can just add properties directly on them. That’s all static methods really is: functions ’living’ on the constructor, not on its instances. That’s all for now.
Extras #
What happens if you try to set an object as its own prototype:
function Person(name) {
this.name = name;
Object.setPrototypeOf(this, this);
}
const p = new Person("Alice");
You get a TypeError: Cyclic __proto__ value ...
Also this gets you another TypeError: Cyclic __proto__ value ...:
function A() {}
function B() {}
Object.setPrototypeOf(A.prototype, B.prototype);
Object.setPrototypeOf(B.prototype, A.prototype);
Because of how instanceof works, you might get true even if an object is not
an instance of a constructor:
const someObj = Object.create(null);
function Foo() {}
Foo.prototype = someObj;
const bar = {};
Object.setPrototypeOf(bar, someObj);
console.log("bar instanceof Foo:", bar instanceof Foo); // true
And false even if an object is actually an instance of the constructor:
const someObj = Object.create(null);
function Foo() {}
const fooInstance = new Foo();
Object.setPrototypeOf(fooInstance, someObj);
console.log("fooInstance instanceof Foo:", fooInstance instanceof Foo); // false