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...'
[Figure1]

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 PropertyValue Type DomainDescription
[[Prototype]]Object or NullThe 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.

[Figure2]

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
💡
While inherited properties can be accessed as if they are object's own, the behavior of inherited and own properties defer in iteration. While 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.

💡
The constructor of any data type is replaceable, except that of primitive values that are assigned literally( 2, abc, true).
However, the reason why certain constructors are not overwritten is unclear. In Core JavaScript, it says it's because the constructors of literal primitive values are read-only properties. However, when one returns 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

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