JavaScript’s Prototypal Inheritance: Level Zero
This is not meant to be authoritative in any way. These are simply my notes distilled and organized. I’m looking for feedback and corrections.
These are the five questions that help me understand the prototypal inheritance of JavaScript.
- What is an
Object
? - What is a
Function
? - What happens when a constructor is invoked?
- What happens when a constructor is defined?
- What is the prototype chain?
-
An instance of
Object
is the base type for all non-primitive data types in JavaScript. The instance of an object type is like a lump of clay. Properties and methods can be attached to it at any point during its lifetime (except whenObject.freeze()
is applied). It can be instantiated in multiple ways.- A pair of curly braces creates an “object literal” which can be directly assigned or returned via factory function.
- The Object.create() method returns an object instance.
- The
new
operator tells a function to return a “constructed’” (instantiated and modified) object.
For these notes, objects are instantiated via constructors. When referring to object properties in general, it is assumed that methods are also included.
-
An instance of
Function
is a type that inherits directly from the base object. It is different from other objects in that it can be called to execute a subroutine of JavaScript code. When called with thenew
operator, the given function becomes an object “constructor”. All Function objects have a.prototype
property for this purpose. -
A constructor instantiates and modifies an object. The constructed object inherits properties from the object referenced by
.prototype
. An executedreturn
statement within the constructor’s body will override the “construction” of a new object in which case the constructor executes only the return statement. -
When the constructor is defined, it creates an empty instance of the base object and references it with the .prototype property. Though this prototype object begins life without any unique properties, its properties can be modified at any point after the constructor is defined. This particular object instance is meant to serve as a set of properties to be shared by all object instances constructed by the function.
-
An object referenced by
.prototype
becomes the prototype for objects instantiated by the constructor, also known as “descendants”.The prototype chain is like a singly-linked list of prototypes and descendants. The properties of the prototype object are inherited by the descendants via a prototype-chain look-up. If the descendant is assigned a property with a name shared by the prototype object, the descendant’s version of the property shadows the prototype’s version and the look-up is completed. If the prototype is a descendant of another prototype, the look-ups can continue until either the sought property is finally found, or the lookup reaches the base Object type. If the constructor’s.prototype
is reassigned to a different object, previously constructed objects are unaffected, but subsequently constructed objects perform property look-ups on the prototype chain from the substituted prototype.
Examples in Node REPL
Can base object properties be added and modified after creation?
> clay = new Object()
{}
> clay.arms = 2
2
> clay.legs = 2
2
> clay.head = 2
2
> clay
{ arms: 2, legs: 2, head: 2 }
> clay.head = 1
1
> clay
{ arms: 2, legs: 2, head: 1 }
Is a function object really constructed from the base object?
> riddle_obj = new Object()
{}
> riddle_func = new Function()
[Function: anonymous]
// Before we start modifying, let's be certain
// that neither object has the "question" property
> riddle_obj.question
undefined
> riddle_func.question
undefined
// The base object's prototype object is like an ancestor from which
// all base object instances inherit their properties. We'll use the
// Object.getPrototypeOf() method to access the prototype object.
// We'll also attach a question property to the base object's prototype
// to see if it propagates to both descendant base
// objects descendant functions.
> Object.getPrototypeOf(riddle_obj).question = "Why was 6 afraid of 7?"
'Why was 6 afraid of 7?'
> riddle_obj.question
'Why was 6 afraid of 7?'
> riddle_func.question
'Why was 6 afraid of 7?'
// That worked. Both types reflect the change to the prototype of the base
// object. Now we'll see if changing the prototype of function propagates to
// both the descendant function object and the descendant base object.
> Object.getPrototypeOf(riddle_func).answer = "Because 7 8 9!"
'Because 7 8 9!'
// The change propagates to the function,
> riddle_func.answer
'Because 7 8 9!'
// but the change does not propagate to the base object.
> riddle_obj.answer
undefined
// This is one way to understand how the base object type is the primary
// type, or prototype, from which all other non-primitive JavaScript
// datatypes inherit their properties.
Is a new
operator ignored with return statement in the constructor?
// Which gets returned from a constructor, the Chicken or the Egg?
// Make Egg and Chicken type constructors.
> let Egg = function () { this.greeting = "The Egg has landed!"; }
undefined
> let Chicken = function () { this.greeting = "The Chicken has landed!"; }
undefined
// Create Chicken and Egg instances.
> let eggietype = new Egg()
Egg { greeting: 'The Egg has landed!' }
> let chickietype = new Chicken()
Chicken { greeting: 'The Chicken has landed!' }
// What does a constructor return if there is no return statement?
> let ChickenEgg_noreturn = function () {}
undefined
// set the prototype to Chicken object
> ChickenEgg_noreturn.prototype = chickietype
Chicken { greeting: 'The Chicken has landed!' }
// constructed object inherits from prototype,
// (it does not inherit from the constructor itself)
> new ChickenEgg_noreturn()
Chicken {}
> Object.getPrototypeOf( new ChickenEgg_noreturn() )
Chicken { greeting: 'The Chicken has landed!' }
// As expected, plain function call with no return statement
> ChickenEgg_noreturn()
undefined
// What results from a function containing a return statement if it is
// invoked with the "new" operator? (this is a "factory function" which
// uses Object.create() to return a descendant of the eggietype object)
> let ChickenEgg_yesreturn = function () { return Object.create(eggietype) }
undefined
// set the prototype to Chicken object
> ChickenEgg_yesreturn.prototype = chickietype;
Chicken { greeting: 'The Chicken has landed!' }
// "return" statement ignores "new" operator
> new ChickenEgg_yesreturn()
Egg {}
// As expected, the function returns the object created from the "return" statement
> ChickenEgg_yesreturn()
Egg {}
// This demonstrates that a constructor cannot execute a return statement
// because the return statement overrides "construction" from a prototype.
Is the default constructor prototype an empty base object?
> let Shoes = function (fastener) {
... this.fastener = fastener;
... }
undefined
// Here's our empty prototype.
> Shoes.prototype
Shoes {}
// These shoes have their own unique fasteners
> ruby_slippers = new Shoes("buckle");
Shoes { fastener: 'buckle' }
> clown_shoes = new Shoes("buttons")
Shoes { fastener: 'buttons' }
// buttoned clown shoes are out of style.
// take off those buttons!
> delete clown_shoes.fastener
true
// Hey, Bozo has no way to fasten his shoes!
> clown_shoes.fastener
undefined
// When in doubt lace your shoes.
> Shoes.prototype.fastener = "laces"
'laces'
> Shoes.prototype
Shoes { fastener: 'laces' }
// Now Bozo can tie his shoes.
> clown_shoes.fastener
'laces'
// Dorothy wants laces too. Just click your heels and...
> delete ruby_slippers.fastener
true
// ...voila!
> ruby_slippers.fastener
'laces'
// This is a silly demonstration that the constructor's default
// prototype is an empty base object. The changes to the base
// object are visible to descendants lacking properties to shadow
// the base object's properties
Example in Script
What happens to an object’s prototype chain when prototypes are reassigned?
#!/usr/bin/env node
// This is our prototype chain printer
const protoChain = (obj, objname) => {
console.log(`>>>protoChain: ${objname}`);
while (obj) {
console.log(obj);
let spaces = " ";
for(let i = 0; i < 4; i++) {
if(i === 3) {
console.log(spaces +"V");
} else if(i === 2) {
console.log(" \\ /");
} else {
console.log(spaces + "|");
}
}
obj = Object.getPrototypeOf(obj);
}
console.log(obj);
}
// Here's the inheritance hierarchy for our life forms
// Animalia -> Cnidaria -> Anthozoa -> SeaAnemone
// Chromista -> BrownAlgae -> Kelp -> seaBamboo
// Chromista -> BrownAlgae -> Kelp -> Coral
// reassign Coral prototype
// Animalia -> Cnidaria -> Anthozoa -> Coral
/*--------Animalia--------*/
function Animalia () {
this.kingdom = "animalia";
}
function Cnidaria () {
this.phylum = "cnidaria";
}
Cnidaria.prototype = new Animalia();
function Anthozoa () {
this.class = "anthozoa";
}
Anthozoa.prototype = new Cnidaria();
function SeaAnemone () {
this.order = "sea anemone";
}
SeaAnemone.prototype = new Anthozoa();
/*-------Chromista---------*/
function Chromista () {
this.kingdom = "chromista";
}
function BrownAlgae () {
this.division = "brown algae";
}
BrownAlgae.prototype = new Chromista();
function Kelp () {
this.family = "kelp";
}
Kelp.prototype = new BrownAlgae();
function SeaBamboo () {
this.species = "sea bamboo";
}
SeaBamboo.prototype = new Kelp();
/*---------What is Coral?---------*/
function BlueCoral () {
this.species = "blue coral";
}
BlueCoral.prototype = new Kelp();
console.log("\n>>> Here's the prototype reassignment demo");
console.log("Our first Coral leads back to the Chromista type");
console.log("> let coral = new BlueCoral():\n");
let coral = new BlueCoral();
protoChain(coral, "coral");
console.log("\n>>> Oops... coral should descend from Animal, not Chromista!");
console.log("Let's change that prototype of the constructor and check our " +
" first coral's prototype chain");
console.log("> BlueCoral.prototype = new Anthozoa()\n");
BlueCoral.prototype = new Anthozoa();
protoChain(coral, "coral with .constructor.prototype reassigned");
console.log("\nThat BlueCoral constructor's reassigned prototype had no effect" +
" on our existing coral. The reassignment will only apply to newly" +
"instantiated corals. Let's go back and fix our existing coral with...");
console.log("> Object.setPrototypeOf(coral, new Anthozoa):\n");
Object.setPrototypeOf(coral, new Anthozoa());
protoChain(coral, "coral with Object.setPrototypeOf() reassignment");
And here’s the output…
>>> Here's the prototype reassignment demo
Our first Coral leads back to the Chromista type
> let coral = new BlueCoral():
>>>protoChain: coral
Chromista { species: 'blue coral' }
|
|
\ /
V
Chromista { family: 'kelp' }
|
|
\ /
V
Chromista { division: 'brown algae' }
|
|
\ /
V
Chromista { kingdom: 'chromista' }
|
|
\ /
V
Chromista {}
|
|
\ /
V
{}
|
|
\ /
V
null
>>> Oops... coral should descend from Animal, not Chromista!
Let's change that prototype of the constructor and check our first coral's prototype chain
> BlueCoral.prototype = new Anthozoa()
>>>protoChain: coral with .constructor.prototype reassigned
Chromista { species: 'blue coral' }
|
|
\ /
V
Chromista { family: 'kelp' }
|
|
\ /
V
Chromista { division: 'brown algae' }
|
|
\ /
V
Chromista { kingdom: 'chromista' }
|
|
\ /
V
Chromista {}
|
|
\ /
V
{}
|
|
\ /
V
null
That BlueCoral constructor's reassigned prototype had no effect on our existing coral. The reassignment will only apply to newlyinstantiated corals. Let's go back and fix our existing coral with...
> Object.setPrototypeOf(coral, new Anthozoa):
>>>protoChain: coral with Object.setPrototypeOf() reassignment
Animalia { species: 'blue coral' }
|
|
\ /
V
Animalia { class: 'anthozoa' }
|
|
\ /
V
Animalia { phylum: 'cnidaria' }
|
|
\ /
V
Animalia { kingdom: 'animalia' }
|
|
\ /
V
Animalia {}
|
|
\ /
V
{}
|
|
\ /
V
null