JavaScript Prototype - Motivation, Syntax
JavaScript is a prototype-based language. It achieves with prototype what Object-Oriented Language achieves with inheritance.
Motivation
Imagine you want to make car objects of various model
but all of them have move
method in common. It would be convenient to have a prototype object of a car, from which each car object can derive. Via prototypical inheritance, instances of the same prototype don't have to declare the same properties and methods repeatedly. Instead, they only have to refer to their prototype. It helps with conciseness and memory efficiency. Also, each object's responsibility is nicely separated, since the prototype object stores common properties and methods while instance objects only define data that differ among instances.
Syntax
The following code implements a simple car object and its prototype.
function Car (model) {
// const this = {}
this.model = model
// return this
}
Car.prototype.mileage = 0
Car.prototype.move = function () {
console.log('moving...')
}
const myCar = new Car('G63')
console.log(myCar) // Car { model: 'G63' }
console.log(myCar.mileage) // 0
myCar.move() // 'moving...'
Now, let's look at the different terminologies of JavaScript prototype.
new [FUNCTION]
When used with new
keyword, JavaScript function acts as a constructor that creates a new object. It returns an object, which has the function as its prototype.
🔗 this
Inside a function that is invoked as a constructor, this
refers to an instance object that the constructor creates. Using this
as a placeholder for the object being created, the function attaches properties and methods that will be the instance's own. Constructor works as if it internally declares an empty object named this
(const this = {}
), adds properties and methods to it, and returns it.
[FUNCTION].prototype
By default, every JavaScript function has a hidden property prototype
. When the constructor is invoked, it sets function's prototype
as prototype of objects to be made. Everything in prototype
is automatically available to instances. While properties added to this
become object's own properties, those attached to prototype
are common to all other objects that are made by the same constructor.
prototype
can either be a type of object
or null
.
[Function].prototype.constructor
Before anything is added to it, function's prototype
has only one method, constructor
. [FUNCTION].prototype.constructor
circularly refers to FUNCTION
itself.
console.log(Car.prototype.constructor === Car) // true
It lets an instance object trace back its constructor. The following two lines of code mean the same thing:
const myCar = new Car('G63')
const mySecondCar = new Car.prototype.constructor('RangeRover')
console.log(myCar) // Car { model: 'G63' }
console.log(mySecondCar) // Car { model: 'RangeRover' }
It helps when one wants to make another object of the same prototype as the existing one. This is especially useful when we. don't know the constructor of an object, as in 3rd party library.
[FUNCTION].[METHOD]
Attaching a method directly to a constructor function makes the method equivalent to a static [METHOD]
of ES6 Class syntax. The function can only be directly invoked from [FUNCTION]
, not by its instances.
function Car (model) {
this.model = model
}
Car.prototype.mileage = 0
Car.prototype.move = function () {
console.log('moving...')
}
const myCar = new Car('G63')
// adding static method to Car
Car.isCar = function (car) {
return Object.getPrototypeOf(car) === Car.prototype
}
Car.isCar(myCar) // true
myCar.isCar()
// ❌ Uncaught TypeError: myCar.isCar is not a function
[[Prototype]]
ECMAScript 5.1 refers to an object's prototype as [[Prototype]]
.
Internal Property | Value Type Domain | Description |
---|---|---|
[[Prototype]] | Object or Null | The prototype of this object. |
If you run console.dir(myCar)
on Chrome DevTools, you see [[Prototype]]
object with its constructor
method pointing at Car
function.
Internal properties that are wrapped in double square brackets, such as [[Prototype]]
, [[Scopes]]
, [[FunctionLocation]]
, are only visible in DevTools for debugging purposes. An attempt to access it directly via code will fail.
console.log(myCar['[[Prototype]]']) // undefined
[INSTANCE]._proto__
Instead of [[Prototype]]
, one can access to object's prototype using a setter/getter named __proto__
. While the constructor's prototype
property has been part of JavaScript from the early history of the language, __proto__
came along later. In the year of 2012, Object.create()
was added to the standard, which enables the creation of a new object of a prototype passed to as an argument. However, there was no standard way to access prototype objects. So browsers implemented the non-standard __proto__
as setter/getter of the object's prototype.
At the moment, __proto__
is considered deprecated and is only supported for backward compatibility with older browsers. It is recommended to replace them with standard Object.setPropertyOf()
and Object.getPrototypeOf()
.
Properties contained in the constructor's prototype
object are available to instances vie __proto__
. In JavaScript, __proto__
can be omitted. As a result, an object can use its prototype's properties as if they are its own.
function Car (model) {
// const this = {}
this.model = model
// return this
}
Car.prototype.mileage = 0
Car.prototype.move = function () {
console.log('moving...')
}
const myCar = new Car('G63')
myCar.model // 'G63'
myCar.__proto__.mileage // 0
myCar.mileage // 0
myCar.__proto.mileage === myCar.mileage // true
myCar.__proto__.move() // 'moving...'
myCar.move() // 'moving...'
myCar.__proto__.move === myCar.move // true
Object.getPrototypeOf([INSTANCE])
Object.setPrototypeOf([INSTANCE])
In 2015, 3 years later than __proto__
, Object.getPrototypeOf()
and Object.setPrototypeOf()
were introduced as the standard way to get and set [[Prototype]]
. They have the same functionality as __proto__
. Instead of non-standard and outdated __proto__
, Object.getPrototypeOf()
/Object.setPrototypeOf()
are recommended ways to go about an object's [[Prototype]]
.
function Car (model) {
this.model = model
}
Car.prototype.mileage = 0
Car.prototype.move = function () {
console.log('moving...')
}
const myCar = {}
Object.setPrototypeOf(myCar, new Car('G63'))
console.log(myCar.model) // 'G63'
console.log(myCar.mileage) // 0
myCar.move() // 'moving...'
myCar.__proto__ === Object.getPrototypeOf(myCar) // true
for ... in
construct iterates over enumerable properties that are either object's own or inherited regardless, some iteration methods like Object.keys()
only walk through object's own properties. For more, read the section of MDN, "Enumerability and ownership of properties"for (const prop in myCar) {
console.log(prop)
}
// model
// mileage
// move
console.log(Object.keys(myCar)) // ['model']
Just like [FUNCTION].prototype
, Object.getPrototypeOf([INSTANCE])([INSTANCE].__proto__
) should be either an object or null
. Other data types are ignored
const obj = {}
console.log(Object.getPrototypeOf(obj)) // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
Object.setPrototypeOf(obj, 'new proto')
// ❌ Uncaught TypeError: Object prototype may only be an Object or null
// ℹ️ obj.__proto__ = 'new proto' is also ignored, but doesn't throw error
Object.setPrototypeOf(obj, null)
console.log(Object.getPrototypeOf(obj)) // null
Object.setPrototypeOf(obj, { proto: true })
console.log(Object.getPrototypeOf(obj)) // { proto: true }
[INSTANCE](.__proto__).constructor
With the help of [INSTANCE](.__proto__).constructor
, we can trace back to an object's constructor and create another object of the same prototype. However, constructor
can be overwritten and therefore shouldn't be considered 100% accountable.
const NewConstructor = function () {
console.log('I\'m new constructor!')
}
myCar.constructor = NewConstructor
console.log(myCar.constructor)
// ƒ () {
// console.log('I\'m new constructor!')
// }
console.log(myCar instanceof NewConstructor) // false
console.log(myCar instanceof Car) // false
console.log(Car.prototype.constructor)
// ƒ Car (model) {
// this.model = model
// }
Note that it doesn't change [FUNCTION].prototype
and that existing objects are still considered instances of the original constructor.
2
, abc
, true
).Object.getOwnPropertyDescriptor(Object.getPrototypeOf(num), 'constructor')
on const num = 2
, it logs {writable: true, enumerable: false, configurable: true, value: ƒ}
, just like constructors of any other data type.const NewConstructor = function () {
console.log('I\'m new constructor!')
}
const dataTypes = [
2,
'abc',
true,
{},
[],
function () {},
/test/,
new Number(),
new String(),
new Boolean,
new Object(),
new Array(),
new Function(),
new Array(),
new RegExp(),
new Date(),
new Error()
]
dataTypes.forEach((data) => {
console.log(data)
const prevPrototype = Object.getPrototypeOf(data)
const prevConstructor = prevPrototype.constructor
console.log(`prev constructor: ${prevConstructor}`)
Object.setPrototypeOf(data, {
...prevPrototype,
constructor: NewConstructor
})
console.log(`curr constructor: ${data.constructor}`)
})
// 2
// prev constructor: function Number() { [native code] }
// curr constructor: function Number() { [native code] }
// 'abc'
// prev constructor: function String() { [native code] }
// curr constructor: function String() { [native code] }
// true
// prev constructor: function Boolean() { [native code] }
// curr constructor: function Boolean() { [native code] }
// {}
// prev constructor: function Object() { [native code] }
// curr constructor: function () {
// console.log('this is a string constructor')
// }
// new Number()
// prev constructor: function Number() { [native code] }
// curr constructor: function () {
// console.log('this is a string constructor')
// }
// new String()
// prev constructor: function Number() { [native code] }
// curr constructor: function () {
// console.log('this is a string constructor')
// }
// new Boolean()
// prev constructor: function Number() { [native code] }
// curr constructor: function () {
// console.log('this is a string constructor')
// }
It is noteworthy that the constructor
s of 2
, abc
, true
were not overwritten, while those of the same data type instantiated by a constructor function(Number
, String
, Boolean
) were.
Reference
Standard ECMAScript 5.1
코어 자바스크립트(Core JavaScript), 재남, 정. 위키북스, 2019. pp. 156-174.
"Prototype methods, objects without __proto__", The Modern JavaScript Tutorial
"F.prototype", The Modern JavaScript Tutorial
Enumerability and ownership of properties