All functions in JavaScript are objects of type Function
. Most Function
instances (there are exceptions) have a .prototype
property. However, there’s a difference between a Function
instance’s .prototype
property and the [[Prototype]]
internal slot which is common to all objects.
The [[Prototype]]
internal slot
Every object has a [[Prototype]]
internal slot which refers to its prototype object. To obtain a reference to this object, you can invoke the static Object.getPrototypeOf()
method.
Object.getPrototypeOf({}); // Object.prototype
Object.getPrototypeOf([]); // Array.prototype
Object.getPrototypeOf(new Date()); // Date.prototype
Object.getPrototypeOf(/hello world/); // RegExp.prototype
Object.getPrototypeOf(function () {}); // Function.prototype
The prototype chain
When you try to access a property of an object, the JavaScript interpreter will search the object’s own properties. If it can’t find the property, it will search the object’s prototype. If it still can’t find the property, it will search the prototype of the prototype. This continues until either the property is found or the end of the prototype chain is reached (Object.getPrototypeOf(Object.prototype) === null
, marking the end of the chain), in which case undefined
will be used.
Object.getPrototypeOf([]); // Array.prototype
Object.getPrototypeOf(Array.prototype); // Object.prototype
Object.getPrototypeOf(Object.prototype); // null
You can traverse an object’s prototype chain with the following function. You can even traverse the prototype chain of the traverse
function itself! This is because it’s an object of type Function
. Note that there are no parentheses after the inner traverse
. This is because we want to use the reference to the Function
object rather than invoke the function and use its return value.
function traverse(obj) {
let prototype = Object.getPrototypeOf(obj);
while (prototype) {
console.log(prototype);
prototype = Object.getPrototypeOf(prototype);
}
console.log(prototype);
}
traverse(traverse); // Function.prototype -> Object.prototype -> null
You can visualize the prototype chain as a linked list. In this example, the node with the Array
instance as its value is the head of the list, while the node with Object.prototype
as its value is the tail.
let prototypeChain = {
value: [],
next: {
value: Array.prototype,
next: {
value: Object.prototype,
next: null,
},
},
};
Example: An array’s prototype chain
Consider an array of strings that represents a few of The Avengers. An array is a special type of object with a correlation between its numbered own properties and its Array.prototype.length
property.
let avengers = ["captain america", "iron man", "thor"];
When you access an element in an array by its index, you are really accessing the value of a property. That’s right: the numbered indices are just properties of the Array
instance. We can prove this by invoking the static Object.keys()
method.
Object.keys(avengers); // ["0", "1", "2"]
Property names have to be strings or symbols, which is why you have to use bracket notation with array indices. If you try to use dot notation, the JavaScript interpreter will throw a SyntaxError
because the property name has to be a valid identifier, which cannot start with a number. Bracket notation accepts an expression, so the JavaScript interpreter is able to coerce the number inside the brackets to a string.
avengers[1]; // "iron man"
avengers[Object(1).toString()]; // what really happens is something like this
The point here is that the numbered indices are the Array
instance’s own properties. When you access an element in an array by its index, the JavaScript interpeter doesn’t have to search Array.prototype
, because the property exists directly on the Array
instance.
However, when you try to invoke an array instance method, such as Array.prototype.forEach()
, the JavaScript interpreter won’t find it among the Array
instance’s own properties. It has to traverse the object’s prototype chain. This is because the method exists on the object’s prototype, i.e. Array.prototype
.
Object.hasOwn(avengers, "forEach"); // false -- not an own property
"forEach" in avengers; // true -- exists in prototype chain
What if you try to access a property that doesn’t exist on Array.prototype
either? The JavaScript interpreter will check the prototype of Array.prototype
, which is Object.prototype
(Object.getPrototypeOf(Array.prototype) === Object.prototype
).
avengers.constructor; // Array
The .constructor
property is inherited from Object.prototype
, which is the prototype of Array.prototype
. The value of the array’s .constructor
property is Array
, which tells us that the Array
function was used to construct this object.
The typeof
operator says the type of Array
is "function"
. Although functions are a type of object—not a primitive value—the specification for the typeof
operator says it should return "function"
if its operand implements an internal [[Call]]
method (in other words, if the object is callable, i.e. a function). It returns "object"
for all other objects.
We can also check the prototype of Array
, which should be Function.prototype
for a Function
instance. Note that we’re checking the prototype of the Array
function itself, not the prototype of Array.prototype
.
typeof Array; // "function"
Object.getPrototypeOf(Array); // Function.prototype
Now that we know Array
is an object of type Function
, we can investigate its .prototype
property.
The .prototype
property
If the value of the Array
function’s [[Prototype]]
internal slot is Function.prototype
, then what is Array.prototype
? What does it refer to if not the function’s own prototype?
Every Function
instance which is constructible (has an internal [[Construct]]
method) has a .prototype
property. The value of this property is an object which is created at the same time the Function
instance is created. This object becomes the prototype of the newly constructed object when the function is invoked with the new
operator.
Or, as the specification for the .prototype
property puts it:
Function
instances that can be used as a constructor have a “prototype” property. Whenever such aFunction
instance is created, another ordinary object is also created and is the initial value of the function’s “prototype” property. Unless otherwise specified, the value of the “prototype” property is used to initialize the[[Prototype]]
internal slot of the object created when that function is invoked as a constructor.
The prototype of the Array
function is Function.prototype
. This means the Array
function inherits from Function.prototype
, so it has access to methods like Function.prototype.bind()
and Function.prototype.call()
.
The prototype of an Array
instance is Array.prototype
. This means an Array
instance inherits from Array.prototype
, so it has access to methods like Array.prototype.at()
and Array.prototype.concat()
.
Object.getPrototypeOf(Array); // Function.prototype
Object.getPrototypeOf([]); // Array.prototype
Constructor functions
Let’s demonstrate this with an example. We’ll declare a Person
function (an object of type Function
). When invoked with the new
operator, it will act as a constructor function. The newly constructed Person
instance will have its own .name
property.
function Person(name) {
this.name = name;
}
let ironMan = new Person("tony stark");
Object.keys(ironMan); // ["name"]
Let’s check the value of the Person
function’s [[Prototype]]
internal slot. It should be Function.prototype
.
Object.getPrototypeOf(Person); // Function.prototype
By default, a function’s .prototype
property points to an object with a single property: .constructor
. The value of this property is a reference to the function itself, so that instances of the function can tell which function constructed them.
Person.prototype; // { constructor: Person }
We can add custom properties and methods to the object referenced by the .prototype
property. All instances of the Person
function will inherit them. Let’s add a .species
property and a .greet()
method.
Person.prototype.species = "human";
Person.prototype.greet = function () {
return `hi, my name is ${this.name}`;
};
The prototype of the Person
function is Function.prototype
, while the prototype of a Person
instance is Person.prototype
. A Person
instance does not have a .prototype
property, because it is not a Function
instance, and therefore has no internal [[Construct]]
method. A Person
instance’s only own property is .name
. It inherits its .species
property and .greet()
method from Person.prototype
.
let ironMan = new Person("tony stark");
Object.getPrototypeOf(Person); // Function.prototype
Object.getPrototypeOf(ironMan); // Person.prototype
ironMan.prototype; // undefined
ironMan.name; // own property
ironMan.species; // inherited property
ironMan.greet(); // inherited method
Summary
All functions are objects of type Function
. However, there’s a difference between a Function
instance’s .prototype
property and the [[Prototype]]
internal slot which is common to all objects.
An object’s [[Prototype]]
internal slot holds a link to its prototype object, which is the object it inherits from. You can obtain a reference to this object by invoking the static Object.getPrototypeOf()
method.
A Function
instance’s .prototype
property refers to a different object. By default, this object has one property, .constructor
, which holds a reference to the Function
instance itself. You can add properties and methods to this object, and they will become available to all instances of the function when it is invoked with the new
operator.