As Ciro San descended Mount Fire Fox after deep meditation, his mind was clear and peaceful.
His hand however, was restless, and by itself grabbed a brush and jotted down the following notes.
0) Two different things can be called "prototype":
the prototype property, as in obj.prototype
the prototype internal property, denoted as [[Prototype]]
in ES5.
It can be retrieved via the ES5 Object.getPrototypeOf()
.
Firefox makes it accessible through the __proto__
property as an extension. ES6 now mentions some optional requirements for __proto__
.
1) Those concepts exist to answer the question:
When I do
obj.property
, where does JS look for.property
?
Intuitively, classical inheritance should affect property lookup.
2)
__proto__
is used for the dot .
property lookup as in obj.property
. .prototype
is not used for lookup directly, only indirectly as it determines __proto__
at object creation with new
.Lookup order is:
obj
properties added with obj.p = ...
or Object.defineProperty(obj, ...)
obj.__proto__
obj.__proto__.__proto__
, and so on__proto__
is null
, return undefined
.This is the so-called prototype chain.
You can avoid .
lookup with obj.hasOwnProperty('key')
and Object.getOwnPropertyNames(f)
3) There are two main ways to set obj.__proto__
:
new
:
var F = function() {}
var f = new F()
then new
has set:
f.__proto__ === F.prototype
This is where .prototype
gets used.
Object.create
:
f = Object.create(proto)
sets:
f.__proto__ === proto
4) The code:
var F = function(i) { this.i = i }
var f = new F(1)
Corresponds to the following diagram (some Number
stuff is omitted):
(Function) ( F ) (f)----->(1)
| ^ | | ^ | i |
| | | | | | |
| | | | +-------------------------+ | |
| |constructor | | | | |
| | | +--------------+ | | |
| | | | | | |
| | | | | | |
|[[Prototype]] |[[Prototype]] |prototype |constructor |[[Prototype]]
| | | | | | |
| | | | | | |
| | | | +----------+ | |
| | | | | | |
| | | | | +-----------------------+ |
| | | | | | |
v | v v | v |
(Function.prototype) (F.prototype) |
| | |
| | |
|[[Prototype]] |[[Prototype]] [[Prototype]]|
| | |
| | |
| +-------------------------------+ |
| | |
v v v
(Object.prototype) (Number.prototype)
| | ^
| | |
| | +---------------------------+
| | |
| +--------------+ |
| | |
| | |
|[[Prototype]] |constructor |prototype
| | |
| | |
| | -------------+
| | |
v v |
(null) (Object)
This diagram shows many language predefined object nodes:
null
Object
Object.prototype
Function
Function.prototype
1
Number.prototype
(can be found with (1).__proto__
, parenthesis mandatory to satisfy syntax)Our 2 lines of code only created the following new objects:
f
F
F.prototype
i
is now a property of f
because when you do:
var f = new F(1)
it evaluates F
with this
being the value that new
will return, which then gets assigned to f
.
5) .constructor
normally comes from F.prototype
through the .
lookup:
f.constructor === F
!f.hasOwnProperty('constructor')
Object.getPrototypeOf(f) === F.prototype
F.prototype.hasOwnProperty('constructor')
F.prototype.constructor === f.constructor
When we write f.constructor
, JavaScript does the .
lookup as:
f
does not have .constructor
f.__proto__ === F.prototype
has .constructor === F
, so take itThe result f.constructor == F
is intuitively correct, since F
is used to construct f
, e.g. set fields, much like in classic OOP languages.
6) Classical inheritance syntax can be achieved by manipulating prototypes chains.
ES6 adds the class
and extends
keywords, which are mostly syntax sugar for previously possible prototype manipulation madness.
class C {
constructor(i) {
this.i = i
}
inc() {
return this.i + 1
}
}
class D extends C {
constructor(i) {
super(i)
}
inc2() {
return this.i + 2
}
}
// Inheritance syntax works as expected.
c = new C(1)
c.inc() === 2
(new D(1)).inc() === 2
(new D(1)).inc2() === 3
// "Classes" are just function objects.
C.constructor === Function
C.__proto__ === Function.prototype
D.constructor === Function
// D is a function "indirectly" through the chain.
D.__proto__ === C
D.__proto__.__proto__ === Function.prototype
// "extends" sets up the prototype chain so that base class
// lookups will work as expected
var d = new D(1)
d.__proto__ === D.prototype
D.prototype.__proto__ === C.prototype
// This is what `d.inc` actually does.
d.__proto__.__proto__.inc === C.prototype.inc
// Class variables
// No ES6 syntax sugar apparently:
// http://stackoverflow.com/questions/22528967/es6-class-variable-alternatives
C.c = 1
C.c === 1
// Because `D.__proto__ === C`.
D.c === 1
// Nothing makes this work.
d.c === undefined
Simplified diagram without all predefined objects:
(c)----->(1)
| i
|
|
|[[Prototype]]
|
|
v __proto__
(C)<--------------(D) (d)
| | | |
| | | |
| |prototype |prototype |[[Prototype]]
| | | |
| | | |
| | | +---------+
| | | |
| | | |
| | v v
|[[Prototype]] (D.prototype)--------> (inc2 function object)
| | | inc2
| | |
| | |[[Prototype]]
| | |
| | |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)------->(inc function object)
| inc
v
Function.prototype
Let's take a moment to study how the following works:
c = new C(1)
c.inc() === 2
The first line sets c.i
to 1
as explained in "4)".
On the second line, when we do:
c.inc()
.inc
is found through the [[Prototype]]
chain: c
-> C
-> C.prototype
-> inc
X.Y()
, JavaScript automatically sets this
to equal X
inside the Y()
function call! The exact same logic also explains d.inc
and d.inc2
.
This article https://javascript.info/class#not-just-a-syntax-sugar mentions further effects of class
worth knowing. Some of them may not be achievable without the class
keyword (TODO check which):
[[FunctionKind]]:"classConstructor"
, which forces the constructor to be called with new: What is the reason ES6 class constructors can't be called as normal functions?Object.defineProperty
.use strict
. Can be done with an explicit use strict
for every function, which is admittedly tedious.