[javascript] How does JavaScript .prototype work?

The seven Koans of prototype

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, ...)
  • properties of obj.__proto__
  • properties of obj.__proto__.__proto__, and so on
  • if some __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 it

The 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
  • when we call a function in Javascript as 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):