Classical Inheritance in Javascript
2018 Edition
Who is Thanasis
Professional
Community
Open Source
- Eng Mgr at Waldo
- CTO at QallOut
- CTO at Insight Replay
- Founder
- Over 40 NPM packages
- Contributor to major Node.js packages
- Avid OSS author
- Local Node.js Meetup organizer (Greece)
- skgtech.io founder
- DEVit Conference organizer
- Software Engineer, CTO, founder
- Recently moved to London from Greece
- Available for hire
Inheritance in Javascript
- Lots of ways
- It's complicated
- We'll talk about Classical / Prototypical
Why Classical / Prototypical?
Why Classical / Prototypical?
var Animal = function() {};
var cat = new Animal();
cat instanceof Animal; // true
- Forces use of a Constructor
- Uses the "new" keyword
- "instanceof" works
Utilizes all the language features provided for inheritance
Why Classical / Prototypical?
class Animal {}
var cat = new Animal();
cat instanceof Animal; // true
ES6 "class" keyword is prototypical inheritance
Why Classical / Prototypical?
var util = requite('util');
// Parent Ctor
var Animal = function() {};
var Cat = function() {
Animal.call(this);
};
util.inherits(Cat, Animal);
For ES5 Node.js Provides helpers built in the language (versions 6 and bellow)
Why Classical / Prototypical?
Alternatives to Classical Inheritance
- A bunch of variations on how to do Classical (Object.create, Object.assign)
- Composition
- Object literals and the rest...
Why Classical / Prototypical?
Closing points onto WHY
- It is the language's intended way for inheritance.
- Generally accepted as the way to do inheritance in Javascript.
- Signal / Noise ratio in JS land is a problem, stay confident & focused.
How to Inherit
// Parent Ctor
class AnimalES6 {
constructor(name) {
this.name = name;
}
}
class Cat extends AnimalES6 {
constructor(color) {
super('cat'); //call the parent method with super
this.color = color;
}
}
ES6 and beyond
How to Inherit
var util = require('util');
// Parent Ctor
var Animal = function() {};
var Cat = function() {
Animal.call(this);
};
util.inherits(Cat, Animal);
ES5 in Node.js
How to Inherit
var inherits = function(ChildCtor, ParentCtor) {
ChildCtor.prototype = Object.create(ParentCtor.prototype);
ChildCtor.prototype.constructor = ParentCtor;
};
// Parent Ctor
var Animal = function() {};
var Cat = function() {
Animal.call(this);
};
inherits(Cat, Animal);
How "util.inherits()" works
How to Inherit
var inherits = function(ChildCtor, ParentCtor) {
function TempCtor() {};
TempCtor.prototype = ParentCtor.prototype;
ChildCtor.prototype = new TempCtor();
ChildCtor.prototype.constructor = ChildCtor;
};
// Parent Ctor
var Animal = function() {};
var Cat = function() {
Animal.call(this);
};
inherits(Cat, Animal);
Vanilla Javascript ES3
What did just happen?
... lets talk about the prototype
The Javascript Prototype
Javascript is a prototypical language
- All Objects have a prototype
- Everything in JS is an Object
-
var str = new String();
-
var bool = new Boolean();
-
var num = new Number();
-
- The Prototype is the blueprint for creating objects and instances
- The Instances or Objects do not have a prototype
- Functions have a prototype
The Javascript Prototype
The Prototype is the blueprint for creating objects and instances
var Animal = function(name) {
this.name = name;
};
Animal.prototype.getName = function() {
return this.name;
};
var pony = new Animal('pony');
pony.getName(); // "pony"
The instance
Invokes the Constructor
The Constructor
Instance local variable
A method
The Javascript Prototype
Going down the rabbit hole...
var fn = function() {}
fn.prototype
// fn {}
// -> constructor: function()
// -> __proto__: Object
All prototypes have at least two properties:
- constructor: A Function that gets invoked when constructing the instance
- __proto__: A reference to the parent prototype
The Javascript Prototype
Going downer the rabbit hole...
var str = 'a string';
str.__proto__;
// Outputs the Object that was used to construct
// the "str" instance:
// String {length: 0, [[PrimitiveValue]]: ""}
All variables of any type in Javascript have the __proto__ property!
The Javascript Prototype
All variables of any type in Javascript have the __proto__ property!
Remember:
- __proto__ is a reference
- Points to the Object that constructed the instance
- Instances using the "new" keyword have a __proto__ that points to the Ctor
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
function TempCtor() {};
TempCtor.prototype = ParentCtor.prototype;
ChildCtor.prototype = new TempCtor();
ChildCtor.prototype.constructor = ChildCtor;
};
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
function TempCtor() {};
TempCtor.prototype = ParentCtor.prototype;
ChildCtor.prototype = new TempCtor();
};
ChildCtor.prototype.constructor = ChildCtor;
Create a temporary Constructor to copy the Parent Prototype on.
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
function TempCtor() {};
TempCtor.prototype = ParentCtor.prototype;
ChildCtor.prototype = new TempCtor();
};
ChildCtor.prototype.constructor = ChildCtor;
Copy the Parent's Prototype to the temporary Constructor's prototype
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
function TempCtor() {};
TempCtor.prototype = ParentCtor.prototype;
ChildCtor.prototype = new TempCtor();
};
ChildCtor.prototype.constructor = ChildCtor;
Instanciate the temporary Ctor and assign it by overwriting the Child's prototype.
This is where inheritance happens.
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
function TempCtor() {};
TempCtor.prototype = ParentCtor.prototype;
ChildCtor.prototype = new TempCtor();
};
ChildCtor.prototype.constructor = ChildCtor;
Let's break this out, too much happened here:
-
"var inst = new TempCtor()" we created an instance
-
The instance has a __proto__ referencing the TempCtor
-
Which in our case is the Parent's Prototype.
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
function TempCtor() {};
TempCtor.prototype = ParentCtor.prototype;
ChildCtor.prototype = new TempCtor();
};
ChildCtor.prototype.constructor = ChildCtor;
-
We overwrite the prototype's special property "constructor"
-
Using the actual Child Ctor
-
Remember, the Ctor is a function
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
function TempCtor() {};
TempCtor.prototype = ParentCtor.prototype;
ChildCtor.prototype = new TempCtor();
};
ChildCtor.prototype.constructor = ChildCtor;
-
So essentially we discard the default Child's prototype
-
Overwriting it with the instance of the Parent Ctor
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
ClildCtor.prototype = ParentCtor.prototype;
};
-
This isn't a "copy", this is "assignment by reference"
-
Every change you perform on the Child's prototype will directly change the Parent's prototype, they are the same
Why not:
Inheritance Explained
var inherits = function(ChildCtor, ParentCtor) {
ClildCtor.prototype = new ParentCtor()
};
-
This is a "copy"
-
BUT you are also invoking the Parent's Ctor which can have severe side-effects
Why not:
We are not finished yet!
The Inheritance
So...
We copied the Parent's prototype to the Child...
... now we need to make sure that all the Constructors from our Parents will be invoked!
... in the right Scope!
The Inheritance
The Scope....
The Inheritance
var util = require('util');
var Animal = function(name) {
this.name = name;
};
Animal.prototype.getName = function() {
return this.name;
};
// Inherits from Animal
var Cat = function(name, color) {
Animal.call(this, name);
this.color = color;
};
util.inherits(Cat, Animal);
Cat.prototype.getColor = function() {
return this.color;
};
That's the key Part right there!
The Scope
99% of your problems will come from Scope
SO PAY ATTENTION
The Scope
Scope refers to where variables and functions are accessible, and in what context it is being executed.
In the case of an Instance, scope is critical in maintaining access to methods and local properties
The Scope
Base Definition
var Animal = function(name) {
this.name = name;
};
Animal.prototype.getName = function() {
return this.name;
};
The Scope
Retains Scope - Vanilla
var animal = new Animal('cat');
animal.getName();
// "cat"
Looses Scope
var animal = new Animal('cat');
var getName = animal.getName;
getName();
// undefined
Retains Scope - bind
var animal = new Animal('cat');
var getName = animal.getName.bind(animal);
getName();
// "cat"
The Scope
Retains Scope - call
var animal = new Animal('cat');
var getName = animal.getName;
getName.call(animal);
// "cat"
Retains Scope - apply
var animal = new Animal('cat');
var getName = animal.getName;
getName.apply(animal);
// "cat"
The Scope
In Practice...
var Animal = function(name) {
this.name = name;
// this will fail
this.on('some event', this.doSomething);
// this is ok
this.on('some event', this.doSomething.bind(this));
};
var animal = new Animal('cat');
// this will fail
onEvent(animal.onEventHandler);
// this is ok
onEvent(animal.onEventHandler.bind(animal));
// With Arrow function
onEvent(() => animal.onEventHandler);
So, back to inheritance
Inheritance Ctor
// Inherits from Animal
var Cat = function(name, color) {
Animal.call(this, name);
this.color = color;
};
util.inherits(Cat, Animal);
Cat.prototype.getColor = function() {
return this.color;
};
- First thing you do in your Ctor is to call your Parent's Ctor using your own, new, context (scope)
- Then, right after the Ctor, invoke the inherits function
- Afterwards define your methods. Can even overwrite the parent's
1
3
2
Inheritance Ctor
// Inherits from Animal
Class Cat extends Animal {
constructor (name, color) {
super(name);
this.color = color;
}
getColor() {
return this.color;
}
}
- First thing you do in your Ctor is to call your Parent's Ctor with super
- Afterwards define your methods. Can even overwrite the parent's
1
2
From Node 8
The Prototype Chain
The Prototype Chain
- The more you extend a Ctor creating childs the longer the chain gets
- Js will lookup serially for a method all the way up to the last prototype
- This can result in performance hits in some particular cases
Common Gotchas
- Only define methods on the prototype
- Everything else on the Ctor
var Animal = function(name) {
this.name = name;
};
// Never do this
Animal.prototype.name = '';
Common Gotchas
- Never invoke the inheritance method after your methods
var util = require('util');
var Animal = function(name) {
this.name = name;
};
// Inherits from Animal
var Cat = function(name, color) {
Animal.call(this, name);
this.color = color;
};
Cat.prototype.getColor = function() {
return this.color;
};
// Will overwrite and DELETE getColor()
util.inherits(Cat, Animal);
Common Gotchas
Scope on callbacks
Animal.prototype.getName = function(cb) {
this.db.getName(function(name) {
// NEW SCOPE HERE, "this" REFERS TO
// THIS FUNCTION'S CONTEXT
cb(name);
});
};
- Use .bind(this) at end of the anonymous function expression used as a callback
- Use the Arrow Function Expression ( fat arrow => )
Trivia & Best Practises
- We call Constructors "Constructors" and not "Classes" because they do not behave like Classes
- They resemble a Class, thus the term "Classical Inheritance"
- But in reality, this is "Prototypical Inheritance"
- We signify a Ctor by capitalizing the first letter
var Animal = function() {}
Trivia & Best Practises
- Use the constructor strictly to construct the instance
- Asynchronous operations in Ctors are forbidden!
- Define any and all properties in the constructor
- Use null to initialize properties with no value
-
Never return any value from the Ctor
- It will screw up everything
var Animal = function() {
this.name = null;
this.color = null;
}
Trivia & Best Practises
- You may define a method on the Ctor directly, that is considered a Static function and will not be inherited
var Animal = function() {
this.name = null;
this.color = null;
}
Animal.staticFn = function() {
// Will not be inherited
};
Thank you
Questions
¿
Classical Inheritance in JS Pt2
By thanpolas
Classical Inheritance in JS Pt2
How to perform classical / prototypical inheritance in Node.js and Javascript
- 914