JavaScript Prototype - Prototype Chain

Prototype Chain

Searching Data in Prototype Chain

When we access a property in an object, JavaScript engine first searches for an object's own property. If it can't find one, it goes on to look search the object's __proto__ . If there's no match and if there is another __proto__ inside __proto__, it continues to search __proto__, until there is no more __proto__ to resort to. This chain of prototypes in which a property is looked for is called the "prototype chain." When properties and methods become automatically available via prototype chain, they are said to be “inherited.” To be precise, what looks like inheritance in object-oriented languages is, under the hood, reference in prototype chain. Even JavaScript class that was introduced in ES6 is syntactic sugar of prototype.

Writing and Deleting Data in Prototype Chain

The prototype chain is only traversed when reading properties. Writing and deleting works directly on instance object and don't affect the prototype, even if there are properties with the same name.

function Car (model) {
	this.model = model
}

Car.prototype.mileage = 0
Car.prototype.move = function () {
	console.log('moving...')
}

const myCar = new Car('G63')

delete myCar.mileage
myCar.mileage // 0

delete myCar.model
myCar.model // undefined
⚠️
When there are accessor properties defined in the prototype, the prototype's methods are invoked to set/get properties of the instance. In this case too, however, only instances' properties are modified, since this in [INSTANCE].[SETTER] points to instance, not the prototype.
const car = {
  _mileage: 0,
  get mileage () {
    console.log('getting mileage...')
    return this._mileage 
  },
  set mileage (mileage) {
    console.log('setting mileage...')
    this._mileage = mileage
  } 
}

const myCar = { model: 'GW6' }
Object.setPrototypeOf(myCar, car)

console.log(myCar.mileage)
// getting mileage...
// 0

myCar.mileage = 'string mileage'
// setting mileage ...

console.log(myCar.mileage)
// getting mileage...
// 'string mileage'

Object.getPrototypeOf(myCar).mileage
// 0

Keep in mind that the prototype chain can't go in circles. If so, JavaScript will throw an error.

function Vehicle (energySource) {
    this.energySource = energySource
}

function Car (model) {
	this.model = model
}

Object.setPrototypeOf(Car, Vehicle)

Object.setPrototypeOf(Vehicle, Car)
// ❌ Uncaught TypeError: Cyclic __proto__ value
[Figure1]

Property / Method Override

When traversing the prototype chain, object's own properties and methods are prioritized. If an instance has properties or methods with the same name as its prototype, those of the instance are returned.

function Car (model) {
  this.model = model
}

Car.prototype.mileage = 0
Car.prototype.move = function () {
  console.log('moving...')
}

const myCar = new Car('G63')
myCar.move() // 'moving...'

myCar.move = function () {
  console.log('myCar is moving, Yay!')
}
myCar.move() // 'myCar is moving, Yay!'

Object.getPrototypeOf(myCar) // f () { console.log('moving...) } 

In this case, prototype's move method was overriden.

Here, we should be careful not to confuse 'override' and 'overwrite.' To overwrite is to replace something completely and the thing overwritten is no longer available. On the other hand, to override means to make something operate instead, in specific cases, without destroying the thing overridden. In this case, the prototype's method is intact and we can still access it by Object.getPrototypeOf(myCar). It's just that myCar's own move was found first when invoking move method in myCar.move() .

Prototype Chain of Native Object

The following code

const myArr = [1, 2]

is equivalent to the following:

const myArr = new Array(1, 2)

When we run console.dir() on myArr, we see that its prototype is JavaScript's native object Array.

[Figure2]

Then, as we toggle [[Prototype]] and go far down the line, we see that Array 's [[Prototype]] references JavaScript's native object Object.

[Figre3]

This is because __proto__ is itself an object and is therefore an instance of Object.

A visual representation:

[Figure4]

In JavaScript, Array, Function, Number, String, Boolean are all objects, meaning that their prototypes are Object. Therefore, they have direct access to Object.prototype's methods, such as hasOwnProperty(), isPrototypeOf, etc.

myArr.hasOwnProperty('length') // true

const myNum = 2, myStr = 'abc', myBool = true
// Note code doesn't break
myNum.hasOwnProperty('length') // false 
myStr.hasOwnProperty('length') // true
myBool.hasOwnProperty('length') // false 
⚠️
Calling Object's static method directly from primitive value throws an error.
2.hasOwnProperty('length')
// ❌ Uncaught SyntaxError: Invalid or unexpected token

While numbers, strings, and booleans are primitive data types, not objects, we can still invoke methods. According to the specification, such is possible since internally, even when we assign value literally(2, abc, true), built-in constructors are invoked(String, Number, Boolean) and wrapper objects are created. Those wrapper objects provide methods and disappear. This way we have access to properties in String.prototype, Number.prototype, Boolean.prototype and ultimately to Object.prototype.

💡
undefined and null do not have wrapper objects. Therefore, we cannot invoke any property or method.

For this reason, unlike Array.prototype, Number.prototype, and so on, Object.prototype contains methods that can be shared by all other data types, not those restricted to Object  in a narrow sense. Methods that only pertain to Object are contained in Object directly as static methods(Object.create(), Object.freeze(), etc.), instead of in Object.prototype. Also, since they are not contained in prototype, they can't be invoked directly from an object. An object is passed to the static methods as  an argument(e.g. Object.freeze(obj) 🟢, obj.freeze() ❌)

💡
Object.getPrototypeOf(Object.prototype) logs null, from which we cannot trace back prototype any further.
💡
Object.create(null) creates an object whose prototype is null, instead of Object.prototype. An object created this way is lighter for the price of not having access to built-in object methods. Such a trick works best when we want to make a pure dictionary, simply mapping keys to values, and are sure that we don't have to make use of inheritance or native methods. 

References

Subscribe to go-kahlo-lee

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe