Understanding prototypes, constructors, and prototype objects in JavaScript (walk-through using examples)
This is a companion article to the first 40 minutes of the following video lecture on various OOP concepts in JavaScript: https://www.youtube.com/watch?v=-N9tBvlO9Bo.
Both the video lecture and the contents of this article draw upon concepts covered in JS 120 and JS 225 as part of Launch School’s curriculum on the fundamentals of software engineering.
Example 1
We’ll start by defining a simple object literal obj
on lines 2–5. We can confirm that it is indeed of type Object
using typeof
, and validate that it has the properties that we defined on it, namely, color
and amount
, using the hasOwnProperty
method.
Now, the first question that arises is how did obj
get access to the hasOwnProperty
method in the first place? When we manually created obj
on lines 2–5, we defined just the two properties on it; nonetheless, we were able to invoke hasOwnProperty
as if it were defined on obj
.
In fact, we can invoke this very method to verify that it does not come from obj
, i.e., obj.hasOwnProperty('hasOwnProperty')
logs false
(see line 14 in the code snippet).
The answer to this lies in the schematic shown in Fig. 1 which serves as our first model of how inheritance works in JavaScript.
As illustrated by the schematic, because we’ve manually created obj
using the object literal syntax, behind the scenes, JavaScript uses the Object
constructor to create the new object. What’s more, it sets the [[Prototype]]
property of obj
to reference the constructor’s prototype object, Object.prototype
. It is from this object that obj
inherits the hasOwnProperty
method. In the code snippet above (line 24), this can be verified by logging Object.prototype.hasOwnProperty('hasOwnProperty')
which evaluates to true
.
The [[Prototype]]
property is a hidden property which captures the connection between the object literal, obj
, and its immediate parent in the prototype chain. Almost all objects in JavaScript have a [[Prototype]]
property. It is hidden in the sense that it can only be accessed indirectly using either the Object.getPrototypeOf()
static method (see line 19), or the .__proto__
property (see line 20), although this latter approach is now deprecated and not recommended for use in production code (we’ll still use .__proto__
in this article as an analog for [[Prototype]]
).
So that solves the riddle of the source of hasOwnProperty
.
Next, we look at another property, constructor
. This property allows us to check the origin of a given object, i.e., how was it created and if it represents data of a particular type. As obj
was manually created using object literal syntax, its constructor will be Object
. As briefly alluded to earlier, Object
is a built-in constructor function that JavaScript uses to create all such objects by setting the [[Prototype]]
of the newly-created object to reference the prototype object of Object
, Object.prototype
.
Furthermore, all objects created using the object literal syntax will inherit the constructor
property from the prototype object rather than holding their own copy (see lines 34–35) (unless we manually assign a value to the constructor
property of the new object). Indeed, manually setting the value of any property of an object will create a new copy of it on that particular object instance, shadowing any other value stored by an object further up in the prototype chain.
As a final point, note that only functions have a prototype object referenced by the prototype
property, so obj
will not have a default prototype
property (see line 39). We will explore this point further in the next example.
Example 2
Let’s now repeat the analysis using a function literal, func
. It’s a fairly simple function that simply logs “hello world!” to the console when invoked.
As was the case with the object literal obj
in the previous example, when we defined a new function literal func
, JavaScript will create it using a particular constructor function, Function
. Furthermore, the newly-created function object will have a [[Prototype]]
property that points to Function.prototype
from which func
can inherit methods and properties such as call()
(see line 21) and constructor
(see line 15). The code snippet on line 10 confirms that the particular object referenced as the prototype of func
is indeed Function.prototype
.
Similar to how the Object
constructor function had a prototype
property that pointed to a prototype object, the constructor Function
also has its own prototype object referenced by Function.prototype
. On line 11, we further validate that func
has been created by the constructor Function
using the constructor
property.
The primary difference between the object literal obj
studied in the previous example and the function literal func
is that, now, there is a new prototype
property that has been defined on func
.
That means func
can be also used as a constructor by invoking it using the new
keyword (line 26), and this will create a new object, e.g. newObj
, whose [[Prototype]]
will point back to func.prototype
(see lines 27–28).
All object instances created by func
will, therefore, inherit from func.prototype
. In particular, the constructor
property that each instance has access to will point back to the original function func
, serving as a marker of how it was created. This can be useful later when we want to investigate, perhaps for debugging purposes, the origin or ‘type’ of a given object as demonstrated on lines 29–30. Thus, in this example, newObj
is an instance of type func
.
The Function
constructor is ‘special’ in that it creates new objects of type ‘function’. Thus, function objects created by the Function
constructor are themselves constructors that can create other objects. Indeed, its own prototype object, Function.protoype
, is itself a function as can be validated by the code snippet typeof Function.prototype
(see lines 33–34) which duly returns “function”.
Interestingly, the Function
constructor is itself an instance of its own prototype object, i.e., Object.getPrototypeOf(Function) === Function.prototype
returns true
(see lines 38–41). Even the constructor Object
which we encountered in the previous example, and the Array
constructor that we’ll see in the next example, point to Function.prototype
as their [[Prototype]]
(see lines 46–47).
In fact, all functions in JavaScript, whether they are ordinary functions or constructors, will be ‘descendants’ of Function.prototype
, i.e., they will have Function.prototype
as a prototype somewhere in their prototype chain and Function.prototype.isPrototypeOf(<someFunction>)
always returns true
.
Example 3
With this next example, we have an array literal, arr
, with two elements, as well as the length
property necessary for all array-like objects. As with the object literal and the function literal, JavaScript uses a built-in constructor to create the new array object behind the scenes. Thus, the [[Prototype]]
of arr
points to the prototype object of Array
, Array.prototype
, from which it can inherit the array instance methods such as forEach()
, map()
, filter()
, reduce()
, join()
, and so on.
In the code snippet above, on line 27, we further confirm that the array object does not have a prototype
property that points to any prototype object. Only function objects will have this property by default, and which is used to create a new object when the function is invoked as a constructor using the new
keyword.
Finally, what about the [[Prototype]]
property of the Array
constructor, and the [[Prototype]]
property of Array
’s prototype object, Array.prototype
? What objects do these two properties point to? This will be explored in the next example.
Example 4
With this next example, we get the entire picture of the prototype chain of the array literal arr
, and that of its constructor, Array
.
The arr
object inherits from its prototype, Array.prototype
which, in turn, inherits from its prototype, Object.prototype
. We can verify this relation between the objects by following the links along the prototype chain, i.e., the following the dotted lines from the [[Prototype]]
property of arr
all the way to Object.prototype
. This can be confirmed programmatically as well as shown in lines 7–8 in the code snippet above.
We also see that Object.prototype
is the last object in the prototype chain as it does not inherit from any other objects, and its [[Prototype]]
explicitly points to null
(see line 14). Note the distinction between a property evaluating to null
which represents an intentional absence of value versus what JavaScript returns when there is a non-existent property, namely, undefined
(see line 15).
That clarifies the entire prototype chain of arr
. How about the constructor Array
? What does its [[Prototype]]
point to?
First, note that all three constructors Array
, Object
, and Function
have a type of ‘function’ (lines 20–22). Already, we can hazard a guess what their prototype object will be as only the Function
constructor can create objects of type ‘function’ (see line 31). Indeed, the code on lines 24–26 confirms that all three constructors reference Function.prototype
as their prototype.
Furthermore, Function.prototype
is an object of type ‘function’ that has its prototype, Object.prototype
(see line 34). This rather circular and tightly-coupled relationship between the Object
and Function
constructors is illustrated in the Fig. 5 below.
A more thorough exploration of these two constructors and the objects that are created by each, namely, function literals and object literals, is provided in this article.