Prototype
TL;DR
Prototype is a feature of Javascript objects' that can mimic inheritence concept in object-oriented languages but is instead just simple behavior delegations
.
Below images visualizes how prototype
concept forms the relationship between JavaScript objects.
Train of Thoughts
At the time when Brendan Eich was designing the language, object-oriented programming languages such as C++ and Java were gaining it's popularity.Though affected by OOP and everything were treated as object, JavaScript, however, was intended to be a script language to provide interactive abilities on the web.
Implementing concepts such as class and inheritance might be too complicated and makes it less easier to pick up. Then how was JavaScript designed to mimic the inheritance concept?
JavaScript does have a class
syntax introduced in ECMAScript (ES6), but is essentially a syntax sugar over the prototype-based model.
Creating object with object literal
To create an object in simplest term, we can use the object literal
syntax to create an instance and encapuslate properties related to it:
var borderoCollie = {}
borderCollie.type = 'Dog'
borderCollie.breed = 'Border Collie'
var pug = {}
pug.type = 'Dog'
pug.breed = 'Pug'
The down side is obvious, it gets tedious if multiple instances need to be instantiated, nor do the instances provide information indicating their reltationship with their prototype (in this case, Dog
, since both border collie and pug are different breeds of dog).
We can reduce the duplicated codes by creating an instance through function:
function Dog (breed) {
return {
type: 'Dog'
breed: breed
}
}
var borderCollie = Dog('Border Collie')
var pug = Dog('Pug')
Instantiate with new
keyword
Although we may infer through code that both instances might resemble to each other since they are both created through same function, it doesn't implies the relationship that both instances represent a breed of dog.
Taking page of OOP langugaes, new
keyword and function constructor
are introduced into JavaScript. Meaning we can instantiate an instance with following snippet, where the this
keyword binds to the created instance.
One important thing to notice is that a constructor
property is automatically added to the created instances, which points to the function Dog
, indicating that instances are created by the constructor function.
An operator, instanceof
, is added to test whether if the prototype property of a constructor appears anywhere in the prototype chain of an object.
// A function constructor
function Dog(breed){
this.type = 'Dog'
this.breed = breed
}
var border = new Dog('Border Collie');
console.log(border.breed) // Border Collie
console.log(border.constructor === Dog) //true
console.log(border instanceof Dog) //true
var pug = new Dog('Pug');
console.log(pug.breed) // Pug
console.log(pug.constructor === Dog) //true
console.log(pug instanceof Dog) //true
Introducing prototype
Desipte the syntax resembles other OOP language, its hard to share mutual properties of funtionalities between instances through this approach. As shown in below example, each instance will have its own memory space for each method, which increases memory usage and is slower when instantiating a new instance.
function Dog(breed){
this.type = 'Dog'
this.breed = breed
this.Bark = function(){
console.log(`I'm a ${this.breed}`)
}
}
var border = new Dog('Border Collie')
var pug = new Dog('Pug')
console.log(border.type) // Dog
console.log(pug.type) // Dog
console.log(border.Bark === pug.Bark) //false
To solve this problem, a property prototype
is automatically added to the function which points to another object which instances will inherite all of its properties from.
prototype
is a function-specific property, object.prototype
returns undefined.
let a = { b: 1 }
a.prototype // undefined
Defined in ECMA-262, function objects
(e.g function a () {}) are created by by calling a CreateDynamicFunction
operation on the Function
object, which defines a prototype
property on any function it creates whose kind is not ASYNC to provide for the possibility that the function will be used as a constructor.
function Dog(breed){
this.breed = breed
}
console.log(Dog.prototype) // {constructor: ƒ}
Dog.prototype.type = 'Dog'
Dog.prototype.Bark = function () {
console.log(`I'm a ${this.breed}`)
}
console.log(Dog.prototype) // {type: 'Dog', Bark: ƒ, constructor: ƒ}
var border = new Dog('Border Collie')
var pug = new Dog('Pug')
console.log(`${border.breed} is a ${border.type} breed`) // Border Collie is a Dog breed
console.log(`${pug.breed} is a ${pug.type} breed`) // Pug is a Dog breed
console.log(border.constructor) // ƒ Dog(breed){ this.breed = breed }
console.log(border.constructor === Dog) // true
console.log(pug.constructor) // ƒ Dog(breed){ this.breed = breed }
console.log(pug.constructor === Dog) // true
console.log(border.constructor === pug.constructor) // true
console.log(border.Bark === pug.Bark) // true
border.Bark() // I'm a Border Collie
pug.Bark() // I'm a Pug
Dog.prototype.type = 'Mammal'
Dog.prototype.Bark = function () {
console.log(`I'm a ${this.type} that don't bark`)
}
border.Bark() // I'm a Mammal that don't bark
pug.Bark() // I'm a Mammal that don't bark
Let's break down above code a bit:
- Line #1~#3: A constructor function is declared.
- Line #5: A
prototype
property is automatically added to the constructor function, which is an object instance that only has aconstructor
property. - Line #7~#12: Adding shared properties and methods to the
prototype
object directly. - Line #14 ~ #15: Instantiate two
Dog
instances:border
andpug
. - Line #17 ~ #31: Checking properties of two instances, what is worth noting is that:
- Both instances immediately have accessible
breed
andtype
properties after instantiate. - Instances have a
constructor
property that points to the same construction functionDog
. - Both instances have access to the same Bark function while able to access their own properties with
this
keyword.
- Both instances immediately have accessible
- Line #33 ~ #39: Directly modifying the prototype object affects both instances' behaviour
This is the prototype
model that JavaScript uses to mimic the inheritance behaviour of OOP languages.
Accessing object instances' prototype
Even though we now know both instances have some sort of connection with the constructor function, is there an explict way to check it? That's where static methods Object.getPrototypeOf()
and Reflect.getPrototypeOf()
come in, where both returns the prototype (i.e. the value of the internal [[Prototype]]
property) of the specified object.
You're almost guaranteed to come across accessing the prototype of an object with the __proto__
syntax when browsing articles about JavaScript prototype, which points to the prototype object of the creator function (the function used to create any instance of that type).
Basically, it is equivalent to Object.getPrototypeOf()
for object instances, meaning Object.getPrototypeOf(border) === border.__proto__
.
Though browsers might still support it for legacy reasons, such syntax is deprecated and no longer recommended.
In this note, __proto__
will be used for simplicity and verifying concept. One should gradually remove such syntax in production code.
function Dog(breed){
this.breed = breed
}
Dog.prototype.type = 'Dog'
Dog.prototype.Bark = function(){
console.log(`I'm a ${this.breed}`)
}
var border = new Dog('Border Collie')
var pug = new Dog('Pug')
console.log(Reflect.getPrototypeOf(border)) // {type: 'Dog', Bark: ƒ, constructor: ƒ}
console.log(Object.getPrototypeOf(pug)) // {type: 'Dog', Bark: ƒ, constructor: ƒ}
console.log(Reflect.getPrototypeOf(border) === Object.getPrototypeOf(border)) // true
console.log(Object.getPrototypeOf(border) === Object.getPrototypeOf(pug)) // true
console.log(Object.getPrototypeOf(border) === Dog.prototype) // true
Note that both border
and pug
instance has a prototype object, that contains type
property and Bark
method defined through our code. More importantly, line 16 and 17 confirms what we concluded in previous section that both returned prototypes are actually the same object instance, which is the constructor function's prototype
object !
And as expected, modifying the prototype object will change behaviours of all inherited
// Directly modify prototype properties
Object.getPrototypeOf(border).type = 'Mammal'
Object.getPrototypeOf(border).Bark = function () {
console.log(`${this.breed} is a ${this.type} that doesn\'t bark`)
}
console.log(Reflect.getPrototypeOf(border)) // {type: 'Mammal', Bark: ƒ, constructor: ƒ}
console.log(Object.getPrototypeOf(pug)) // {type: 'Mammal', Bark: ƒ, constructor: ƒ}
border.Bark() // Border Collie is a Mammal that doesn't bark
pug.Bark() // Pug is a Mammal that doesn't bark
This is why it is strongly discouraged to directly modifying prototype of third-party object. It will affect globally and cause unexpected behaviour, making it hard to trace the source of modification in large codebase. Only do so with a good reason.
// Overrides Array.concat method with Array.pop method
Array.prototype.concat = Array.prototype.pop
let a = [1, 2, 3, 4, 5]
a.pop() // 5
a.concat() //4
console.log(a) // [1, 2, 3]
Understanding prototype chain
In previous code examples, we've called type
and Bark
function on instances without second thought, but we will find that both object instances itself does not include both properties if we log them directly.
Instead, both properties exist in the prototype object of the instance.
let border = new Dog('Border Collie')
console.log(border)
// Dog {
// breed: 'Border Collie'
// [[Prototype]]: Object
// }
console.log(border.__proto__) // {type: 'Dog', Bark: ƒ, constructor: ƒ}
console.log(border.type) // Dog
border.Bark() // I'm a Border Collie
What if we add a property type
directly to the instance ?
border.type = 'Animal'
console.log(border)
// Dog {
// breed: 'Border Collie',
// type: 'Animal'
// [[Prototype]]: Object
// }
console.log(border.type) // Animal
border.Bark() // I'm a Border Collie
Calling border.type
now returns Animal
instead of Dog
. This demonstrates how JavaScript works : it will first search for properties on the called instance itself, and continues to search into prototype
object if not found in instance itself.
The behaviour of local property will prevent searching for same property in the prototype object is called property shadowing. And the mechanism will continue to find the target property through object instance's prototype, prototype of object instance's prototype, and so on. It continues to search and stop ultimately when a certain prototype
object has a null prototype, meaning the search has comes to end.
The searching path, essentially a chain of prototypes, is what is commonly known as the prototype chain
.
What exactly is the prototype
object that has a null prototype ({someObject}.proto === null) then? Since everything is treated as object in JavaScript, prototype chains of all objects eventually points to a same object: Object.prototype
.
Since Object.prototype
servers as the inheritance root of all object, it has a null prototype.
function Animal () {}
Animal.prototype.kingdom = 'Animalia'
function Dog(breed){
this.breed = breed
}
Dog.prototype.Bark = function () {
console.log(`I'm a ${this.breed}`)
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
let border = new Dog('Border Collie')
console.log(border.kingdom) // Animalia
border.Bark() // I'm a Border Collie
console.log(border.__proto__ === Dog.prototype) // true
console.log(border.__proto__.__proto__ === Animal.prototype) // true
console.log(border.__proto__.__proto__.__proto__ === Object.prototype) // true
console.log(border.__proto__.__proto__.__proto__.__proto__ === null) // true
console.log(Object.prototype.__proto__ === null) // true
// prototype chain:
// border -.__proto__-> Dog.prototype -.__proto__-> Animal.prototype -.__proto__-> Object.prototype -.__proto__-> null
Verifying prototype of an object
We now know what prototype chain is, how can we verify if a certain object involes in a prototype chain ? A few approaches can come in handy.
instanceof
operator is the earliest approach introduced to check the prototype chaining.
The instanceof operator tests the presence of
constructor.prototype
in object's prototype chain. This usually (though not always) means object was constructed with constructor.-- Mdn Web Docs
function Animal () {}
Animal.prototype.kingdom = 'Animalia'
function Dog(breed){
this.breed = breed
}
Dog.prototype.Bark = function () {
console.log(`I'm a ${this.breed}`)
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
let border = new Dog('Border Collie');
console.log(border.kingdom) // Aminalia
border.Bark() // I'm a Border Collie
console.log(border instanceof Dog) // true, because Object.getPrototypeOf(border) === Dog.prototype
console.log(border instanceof Animal) // true, because Object.getPrototypeOf(Dog.prototype) === Animal.prototype
However, instanceof
operator might fail on some circumstances, such as when an object is generated through Object.create
with a particular prototype. Some edge case examples can be found on Mdn Web Docs. In short, instanceof
operator is somewhat limited.
let animalProto = {
kingdom: 'Animalia'
}
let dog = Object.create(animalProto)
dog.breed = 'Border Collie'
dog.Bark = function () {
console.log(`I'm a ${this.breed}`)
}
dog.Bark() // I'm a Border Collie
console.log(dog.kingdom) // Aminalia
console.log(dog instanceof animalProto) // Uncaught TypeError: Right-hand side of 'instanceof' is not callable
Contray to the instanceof
operator, Object.isPrototypeOf
is a built-in method available on the Object.prototype
object and is much more generally used.
isPrototypeOf()
differs from the instanceof operator. In the expressionobject instanceof AFunction
, object's prototype chain is checked againstAFunction.prototype
, not againstAFunction
itself.-- Mdn Web Docs
function Animal () {}
Animal.prototype.kingdom = 'Animalia'
function Dog(breed){
this.breed = breed
}
Dog.prototype.Bark = function () {
console.log(`I'm a ${this.breed}`)
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
let border = new Dog('Border Collie');
console.log(border.kingdom) // Aminalia
border.Bark() // I'm a Border Collie
console.log(Dog.isPrototypeOf(border)) // false
console.log(Dog.prototype.isPrototypeOf(border)) // true
console.log(Animal.isPrototypeOf(border)) // false
console.log(Animal.prototype.isPrototypeOf(border)) // true
Deep Dive
Object instanceof Function === true ?
If you run below code in browser console, you'll find the result a bit surprising. Is this a chicken-or-egg problem ?
Object instanceof Function // true
Function instanceof Object // true
In fact, both Object
and Function
are constructor functions. And according ECMA-262 Specification:
The Object constructor has a internal slot
[[Prototype]]
whose value isFunction.prototype
. (link)The Function constructor is itself a built-in function object, which has a internal slot
[[Prototype]]
whose value isFunction.prototype
. (link)
That is to say: Function.__proto__
points to Function.prototype
. Function
is it's own constructor!
Function.constructor === Function // true
Object.__proto__ === Function.prototype // true
References
- Javascript – How Prototypal Inheritance really works
- Object prototypes
- JS Objects Series
- mollypages - JavaScript Object Layout
- 該來理解 JavaScript 的原型鍊了
- Working with objects -- Mdn Web Docs