JavaScript Objects - Constructors
Learning outcomes:
- What are constructors
- Creating and working with constructors
- Internals of a constructor — the
[[Construct]]
method - Constructors with parameters
- Adding methods to the instance
- Problem with adding methods inside constructors
- The
instanceof
operator - The
constructor
property - Calling the constructor of the parent class
- Static methods
- Calling constructors as functions
Introduction
JavaScript from day one was an OOP language. What exactly is meant by the term 'OOP language' would be covered step-by-step as we progress through this unit, but at the core, it refers to a language that could model a programming problem in terms of objects.
As we have already been able to appreciate in the prior segment of this course, everything in JavaScript is an object, obviously excluding the primitives. This is evident of the fact that JavaScript revolves around the notion of objects — it's based on an object-oriented model.
Amongst some defining characteristics of OOP languages is that they have things called constructors that initialize instances when created.
JavaScript also provides constructors but they are quite different from the constructors we'll encounter in classical OOP languages such as Java, C++, Python, PHP etc.
People coming to JavaScript from these languages would find the notion of constructors in JavaScript quite nebulous. We'll see all these distinctions in detail in this chapter.
This chapter aims to introduce us to the idea of constructors in JavaScript; how to create our very own constructors and thus emulate classes in classical OOP languages; how to instantiate objects using constructors; how constructors actually work in JavaScript internally; and much more.
Let's begin!
What are constructors?
A constructor in JavaScript is nothing more than a function i.e. there is no special syntax for a constructor that makes it a constructor.
This is in contrast to constructors in classical OOP languages such as Java, C++, Python etc. In these languages, a constructor is a special thing. Typically, it's part of a class definition, with a special syntax (in Python, it's given by the __init__()
method, while in Java, it's given by the name of the class itself) and is usually used to initialize an instance during the instantiation process governed by the class itself.
However, in JavaScript, as stated before, there is no notion of a class, hence the idea that a constructor is part of a class becomes meaningless right away.
A constructor in JavaScript is thus technically different than a constructor in a classical OOP language, but theoretically it's quite the same thing:
Let's define it the best we could:
One thing that's right away apparent from the discussion so far is that a constructor is a function. Likewise, we should expect all the concepts that apply to functions to apply to constructors as well.
Well, it turns out that almost all concepts native to functions in JavaScript are applicable to constructors as well, with the exception of only a few (we'll see these exceptional cases in this chapter).
Secondly, as the definition suggests, a constructor creates an object. Hence, when we are talking about a constructor, we are talking about some sort of object's creation, not the creation of any primitive.
Now, the definition given above for a constructor is a little incomplete. We've skipped on the idea of a prototype, because it's not been explained yet — including the notion of a prototype right now would only complicate things for you. In the next chapter, when we take a deep dive into prototypes, we'll review this definition in its complete form.
Anyways, coming back to understanding what a constructor really is, the ECMAScript 5.1 spec, section 4.2.1, itself defines constructors in a very beautiful way. It states that:
Now for you, this might not be a 'very beautiful' description — it might sound even more alien than our original definition.
Well, if this is the case (which would most probably be), forget about everything in the definition after the phrase 'create objects'. That is, just try to understand the following: "Objects in JavaScript may be created via constructors which create objects...". This should confirm the fact that constructors are meant to create objects.
By the end of this chapter, we guarantee to you that you'll be able to make complete sense of this definition of a constructor put forward by the ECMAScript spec. Believe it!
Moving on, a constructor function in JavaScript is supposed to be invoked with the new
keyword. This invocation with new
is by far the most crucial thing in the world of JavaScript OOP.
In the most simplest words, when a constructor function is invoked with the new
keyword, a new object is created — or in other words, constructed.
Calling a constructor function with new
has the consequence of performing a couple of internal operations before invoking the actual function itself. These internal operations, as we shall see them later on in this chapter, serve to create an empty object and then call the constructor function with this object provided to it.
With the empty instance in hand, the constructor can add properties and methods to it, and thus, in this way, initialize it.
Note that it's possible to call a constructor function without new
— after all it's a function. The same ECMAScript 5.1 spec beautifully states the consequence of doing so:
new
has consequences that depend on the constructor.That is, it might just depend on the constructor about what would happen when new
is omitted from the constructor's invocation expression.
Sometimes, it might be perfectly valid to call a constructor as a function (i.e. without the new
keyword) while other times it won't.
For instance, as we have seen in the previous chapters, both the invocations new Array()
and Array()
are valid and produce the same result. In this case, it's apparent that the Array()
constructor function doesn't impose any restriction on the context of its invocation — it could be called either as a constructor or an ordinary function.
Similarly, as stated before, it might not be valid to call a given constructor as a function. For instance, calling the IntersectionObserver()
constructor as an ordinary function would throw an exception, asking the programmer to explicitly call the function with new
.
It is an advanced concept that is detailed in our Advanced JavaScript course, in the unit JavaScript Intersection Observers — Introduction.
Conversely, just as it might be invalid to call a constructor as a function, sometimes it might be invalid to call a function as a constructor. For instance, the Symbol()
function can't be called as new Symbol()
.
To learn more about symbols in JavaScript, refer to Advanced JavaScript — Symbols.
In the following sections, we'll see how to check whether a given function is called as a function or as a constructor.
Anyways, so this was a good amount of discussion on the very primitive definition of a constructor function in JavaScript. Hopefully, at this point you have a basic idea of what a constructor is (recap: it's a function that's meant to create objects).
In the next sections, we dive right into exploring even more about this constructor function theory of JavaScript, and finally create constructor functions of our own.
Constructors vs. ordinary functions
As stated before, there is absolutely no syntactic difference between creating a constructor function and creating an ordinary function in JavaScript. Both could be created using any of the ways we discussed in the chapter JavaScript Functions Basics.
So how do we figure out what's a constructor and what's not?
Well, what really distinguishes a constructor from an ordinary function is its body.
We've been seeing ordinary functions throughout this course, and in a much more higher frequency in the JavaScript Functions unit.
An ordinary function might log some data to the console; take some arguments and then maybe print their sum, or maybe add them to a global array; perform a whole procedure such as asking the user for their name, then uppercasing it, and finally passing it to alert()
, which is guess what... another function; the list is just endless.
To go even far, an ordinary function might even create and return an object explicitly. We saw such functions in the JavaScript Functions Basics chapter, where we learnt that they're called factory functions, since they are factories that spawn new objects.
However, an ordinary function can't assert that it gets passed a new empty object out of thin air, and then initializes that object using the keyword this
, and then implicitly returns that object.
That's just NOT how an ordinary function would ever be set up on Earth (we don't know about Mars, or any other planet 🤔).
A constructor function, on the otherhand, gets an object out of thin air assigned to this
, then optionally initializes it, and finally implicitly returns that object.
A constructor function could be left empty as well. Now note that if the constructor's body is empty, then there is simply nothing for us to see to judge whether the function is a constructor or not.
It turns out that in this case, the way we call the function and especially name it determines whether it's a constructor or not i.e. we call it with the new
keyword and name it with an uppercase first letter (for e.g. Animal
instead of animal
).
Wait... Where does this uppercasing-the-first-letter convention come from? Why are constructors named this way?
Good question. It seems you're quite much following all of this discussion very carefully. Well, let's answer this question.
In JavaScript, a constructor function can be pretty much thought of as a class from the OOP world.
What is a class in object-oriented programming? At the core, it's something that defines characteristics and behavior of each of its instances.
That's quite what a constructor in JavaScript is. It defines characteristics and behavior of each the objects constructed from it — or better to say, instantiated from it. You won't be able understand this right now, but don't worry; it'll all become clear as you keep reading.
Now, since naming classes in OOP has the convention of uppercasing the first letter, we'll do the same for naming constructors in JavaScript.
Simple!
There is no class in JavaScript!
An important thing to keep in mind over here is that a constructor in JavaScript is only like a class from a pure class-based OOP language — it's NOTactually the same thing!
There are two categories of OOP languages: class-based and prototype-based. As mentioned before, Java, C++, Python, PHP fall in the first category i.e. class-based. JavaScript and the Self programming language fall in the second category i.e. prototype-based.
You'll often see people saying that a given object o
belongs to the class A
in JavaScript. The word 'class' here only conveniently implies that the constructor of that object is A()
, not that there actually exists a class construct in JavaScript.
The word 'class' is used because its rudimentary to any OOP talk. Devs and other people find it much intuitive to say that an object belongs to some class A
instead of saying that it is constructed using the constructor A()
.
In fact, in many OOP languages, the latter statement i.e. 'an object is constructed using the constructor A()
', isn't quite right. In these languages, an object can never be created using a constructor — it's created using a class whose constructor gets called to simply initialize the object after it has been created.
Anyways, if we see it very carefully, saying that an object o
belongs to the class A
in JavaScript is technically not wrong.
A class in OOP defines characteristics and behavior related to each of its instances, right? That's exactly the case with the constructor function A()
, as stated here.
In JavaScript, the constructor function is the 'thing' that defines characteristics and behavior of each of its instances (by virtue of being a constructor function which initializes its instance with various properties and methods, and by virtue of the prototype
property on itself.)
In even simpler words, all of the traits of the given object o
are nicely defined in A
alone, not in any other place. Hence, it's indeed not wrong to say that A
is the class of o
.
Creating a constructor
Recall the point
object we created in the previous to previous JavaScript Objects — Basics chapter, using the object literal syntax. It modeled a point on a 2D plane, and had two properties x
and y
corresponding to the x and y coordinates of the point, respectively.
A point on a graph is a really good starting point to experiment with constructor functions in JavaScript.
Let's create the same point object, this time using a constructor.
Before anything else, the first concern we ought to address is what name to give to the constructor.
Well, once again, we'll use the fact that a constructor in JavaScript is quite similar to the idea of a class from the OOP universe. Let's see how to use this fact...
We need to create point objects using our constructor, right? Each of these point objects could be said to belong to the class Point
, couldn't it? In other words, each object is a Point
.
Seems quite reasonable, doesn't it?
Hence, following from the fact that a point belongs to the class Point
and that a constructor in JavaScript is analogous to the concept of a class, we'll call the constructor Point
. Simple!
OK, so now let's finally define this constructor function Point()
:
function Point() {
console.log('A new point created.');
}
For now, we just have a simple log in here. Later on, in this chapter, we'll be adding more stuff in the constructor's body to initialize the newly-created instance.
Anyways, let's go on and create a Point
object.
Creating an object using a constructor is accomplished by invoking the constructor via new
, as we already know.
A constructor's invocation expression (with new
) creates and returns a new instance of the constructor. The overall process of creating the instance is called instantiation.
In the following code, we instantiate a Point
object (in line 5) and then log it to see how it's represented in the console:
function Point() {
console.log('A new point created.');
}
var p1 = new Point();
console.log(p1);
In line 5, when the constructor is invoked (via new Point()
), a handful of things happen.
We'll see all of these things in detail in the next section, but for now let's focus on the fact that amongst these things, the function Point()
is actually called.
This means that its body is executed, which explains the first 'A new point created'
log.
Once the constructor function completes, it returns the instance object created.
That is, the whole invocation expression new Point()
gets replaced with the created instance object. In the code above, this object is stored in the variable p1
. In line 6, we log this instance object.
The second log shows this instance object. It shows that the name of the constructor of the instance is displayed next to the instance's literal notation itself, which is empty in the case above.
Alright, all good uptil now!
We have a Point
instance called p1
. But wait... it doesn't have an x
or y
property.
What now?
Well, p1
is an object, right? And we studied in the previous chapters that objects could be assigned new properties at any time just by a simple assignment syntax together with bracket notation or dot notation. That's exactly what we could do here to add the properties x
and y
to our Point
instance.
In the code below, we add the two properties x
and y
one-by-one to p1
, set them to 0
, and then finally log them:
function Point() {
console.log('A new point created.');
}
var p1 = new Point();
p1.x = 0;
p1.y = 0;
console.log(p1);
Great!
This is a constructor in action in JavaScript.
At this point, you might be thinking that creating a point object using a constructor function is quite more tiring compared to using an object literal.
Well, yes for now when we haven't used any of the potential of constructors, this whole code is definitely less concise than the one we saw in the chapter JavaScript Objects — Basics.
However, once we get to the essence of constructors, it would become apparent that they are way more powerful and capable than the typical and monotonous object literal syntax in cases where we have to create multiple objects all belonging to one particular class (from OOP terminology).
Let's see some of this potential.
Initializing the instance
Assuming that when a point is created, it's placed on the origin, i.e. (0, 0), we could configure our Point()
constructor to do this automatically for us:
function Point() {
this.x = 0;
this.y = 0;
}
var p1 = new Point();
What's happening here is paramount to understand. Time to dive right in...
When Point()
is called in line 6, as a constructor, it's quite obvious what's happening in its body i.e. the properties x
and y
are being assigned on the this
object, but what's not that obvious is the value that this
resolves to.
So what is the object that this
resolves down to in the Point()
constructor above?
If we know the answer to this, making intuition of this Point()
constructor would be all peaches and cream — after all, it just adds two properties to that object.
It turns out that this
holds the real potential of OOP in JavaScript:
- It's what makes it possible to initialize an object upon construction inside the constructor function.
- Furthermore, as we shall see in the next chapter, it's what provides a method's body, defined on the prototype, with the instance object calling it.
We already have a good working experience of this
from the chapter JavaScript Objects — Basics. There, we learnt that this
, when used inside a function, resolves to the object calling that function.
Yes, indeed that is true, but at least not in the case of a constructor's invocation.
new
), internally a new empty object is created and then that object is assigned to this
, which can then be accessed inside the function.Hence, when we are assigning properties to this
inside a constructor's body, we are, in effect, assigning properties to this new empty object constructed by the constructor's call.
Assigning properties to an instance from within the constructor is commonly known as initialization. We say that we are initializing the instance.
this
in JavaScript, such as this = {}
. Only the internal engine could do this, and it actually does it all the time when entering the execution contexts of given functions.Coming back to the code above, this means that this
inside the constructor function Point()
points to the Point
object created internally by the call new Point()
.
Hence, this.x = 0
simply adds a property x
to this Point
object and sets it to 0
, while this.y = 0
adds a property y
and sets that to 0
as well.
Finally, as we know from the section above, this instance object is implicitly returned from the constructor function. In our case, this means that the return value of new Point()
is merely an object with two props x
and y
, both set to 0
.
Let's go on and instantiate two Point
objects using this constructor and inspect them:
function Point() {
this.x = 0;
this.y = 0;
}
var p1 = new Point();
var p2 = new Point();
console.log(p1);
console.log(p2);
As you can see, both the objects p1
and p2
are Point
instances with x
and y
properties, both equal to 0
.
Great!
Note that p1
and p2
defined here are two distinct objects. They have the same properties x
and y
and, at least for now, even the same values for those properties, but they are in no way identical to each other.
In other words, p1 === p2
would return false
, as both the objects hold different references.
While describing the code above, it won't be wrong to say that a constructor is a template-kind-of thing that defines what a Point
object looks like. We use this template to create two Point
instances p1
and p2
in our code, each having their own x
and y
properties set to 0
.
In other words, we don't have to manually go on and create each object using the same new literal syntax, merely copied and pasted again and again.
This doesn't only have the benefit of reducing repetition in rewriting an object literal for each new object, but also of allowing us to determine whether an arbitrary object is a Point
instance or not (which we'll see how in a while). This definitely can't be achieved such easily and neatly with an object literal.
Guess what, this is just the beginning of exploring the potential of constructors in JavaScript — there's a lot more to follow.
Now before we explore more of that, let's go over the internals of a constructor's invocation in the next section i.e where does this
gets its value from and what other hidden operations get performed behind-the-scenes while we instantiate an instance out of a constructor.
Internals of a constructor
We learnt in the chapter JavaScript Functions Basics that when a function is called, its internal [[Call]]
method is invoked. Now, this only happens when the function is called in the context of a function, i.e. without the new
keyword.
When a function is called in the context of a constructor, i.e. with the new
keyword, the internal [[Construct]]
method of the function object is called, if it exists.
So what happens inside [[Construct]]
?
Well, it's quite easy to search for the algorithm executed by [[Construct]]
in the latest ECMAScript spec, 12th edition at the time of this writing.
However, the thing is that the latest spec is quite big and complex, and with each algorithm assumes a lot of knowledge (really a lot!) about numerous concepts and internal function calls defined in the spec itself.
One has to be profound of all the primitive ideas discussed in the spec from its very beginning in order to be able to make any sense of most algorithms, like the one executed by the [[Construct]]
internal method of functions.
At this stage, (or even at an advanced stage as a JavaScript developer, given that we're not an engine developer for the language) all this detail in the spec is of little value to us, at least at this point.
Older ECMAScript specs were better in this regard. They didn't assume a lot on the reader's end. Each algorithm was extremely easy to follow and make intuition of, even if the end reader didn't start off the spec from its very beginning.
Likewise, we'll refer to an old ECMAScript spec to understand what happens in [[Construct]]
. Specifically, we'll be referring to the ECMAScript 5.1 spec.
Section 13.2.2, details the algorithm used in the [[Construct]]
method of function objects.
We'll abstract away even more details from it that are not really useful for our discussion.
Given a function F
, the expression new F()
executes the internal [[Construct]]
method on F
that performs the following steps:
- A new empty object is created.
- The prototype of this object is set to
F.prototype
. We'll understand this step much better once we learn about prototypes in the next chapter. - The
[[Call]]
internal method of the function is executed with itsthis
set to the object created in step 1. - The object created in step 1 is returned.
As you can see, the steps listed above are fairly easy to follow. Even if you were to inspect the actual algorithm as listed in Section 13.2.2 of the spec, you'll find that it's not difficult to make sense of either.
It's just the latest specs starting from the behemoth ES6 that really drive the mind crazy with tons and tons of detail.
Anyways, the most important steps to consider above are 1 and 3.
Step 1 states that calling new F()
results in an empty object (an object with no own properties) being created behind-the-scenes.
Step 3 states that once this object is created (and its prototype set), the function is called just as any other normal JavaScript function is. That is, its internal [[Call]]
method is invoked.
However, unlike an ordinary function, the this
of a constructor is set to the newly created object. This allows the constructor function's body to initialize the newly-created object.
Practically, this model is an exceptionally intelligent one. A new object is created and then the respective function is executed normally as any other function, with its this
configured to the newly-created object.
This is miraculous. We have an OOP principle being utilised, which is that of a constructor, yet what's powering it is a mere function, which is not an OOP idea.
Isn't this awesome?
[[Call]]
internal method is not invoked directly; rather, it's invoked from within the [[Construct]]
internal method.Now, knowing these internals won't make you a completely different JavaScript dev, but yes it would help you understand some other object-related behaviors in JavaScript much better (as we shall explore them in the next chapter) and make you much more confident about the knowledge you have for the language.
With the internals done, let's now resume with extracting more of that potential from our Point()
constructor.
Constructor with parameters
In the last code snippet that we saw above, we defined a Point()
constructor to create a point object set to the origin, with its x
and y
properties both assigned the value 0
.
Now what if we want to create a point for some other x and/or y coordinates?
Well, one way is to go beyond and reassign the properties x
and/or y
.
This is illustrated below:
function Point() {
this.x = 0;
this.y = 0;
}
var p1 = new Point();
console.log(p1);
p1.x = 5;
p1.y = 10;
console.log(p1);
Point { x: 0, y: 0 }
Point { x: 5, y: 10 }
This surely works. But it's redundant and tiring. That is, to change the point to some other set of coordinates or to create a point having different coordinates from the very beginning, we have to write two assignment expressions manually.
Well, the latter could be solved very easily by taking advantage of the fact that the constructor is just another function. So what, you ask?
Hmm. Think of it.
We could set up the Point()
constructor to accept the x
and y
coordinates of the point to-be-created as arguments.
Consider the code below:
function Point(x, y) {
this.x = x;
this.y = y;
}
// Create a point on (5, 10)
var p1 = new Point(5, 10);
console.log(p1);
Point { x: 5, y: 10 }
With this Point()
constructor in place, we could now very conveniently create a Point
object to represent any point on a graph, not just a point on the origin, without having to manually type in the property assignment expressions.
Let's go on and actually create a couple of points:
new Point(50, 1)
Point { x: 50, y: 1 }
new Point(-2, -3)
Point { x: -2, y: -3 }
new Point(6, 0)
Point { x: 6, y: 0 }
Amazing!
Although this constructor works flawlessly, there's a slight betterment oppurtunity in it that we could, and preferably should, avail.
Let's see that...
Currently, if we want to create a bunch of points pointing to the origin, we have to manually pass in the arguments 0
and 0
to the constructor's invocation, i.e. new Point(0, 0)
. Given the fact that the origin is a common location, we could simplify this a bit.
That is, we could allow the arguments x
and y
to be omitted, and in that case assumed to be 0
. In other words, we could make the parameters x
and y
optional and default them to 0
, if omitted.
Let's do this using ES6 default-valued parameters:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
var p1 = new Point();
console.log(p1);
var p2 = new Point(-2, 8);
console.log(p2);
Point { x: 0, y: 0 }
Point { x: -2, y: 8 }
Now our constructor is quite flexible. If we just write new Point()
, that's effectively the same as new Point(0, 0)
.
ES6 default-valued parameters aren't supported, as with all ES6 features, on old browsers. Hence, to get the code above to work properly on these old browsers, we could simply use the ?:
-style check.
Shown below is the code:
function Point(x, y) {
this.x = x !== undefined ? x : 0;
this.y = y !== undefined ? y : 0;
}
var p1 = new Point();
console.log(p1);
var p2 = new Point(-2, 8);
console.log(p2);
Point { x: 0, y: 0 }
Point { x: -2, y: 8 }
If we are 100% sure that the environment where our JavaScript program would run supports ES6, then without any second thought, we should go for ES6 default-valued parameters.
Otherwise, we should definitely use ?:
or an if...else
check, in order to support older browsers.
Adding methods to instances
So far, we are on the right track to building our Point()
constructor from scratch with the desired functionalities for creating a point on a 2D plane.
When called without any arguments, it creates a Point
object situated at the origin. Otherwise, it creates a Point
object situated at the given set of coordinates (provided as arguments).
Simple enough.
This parameter version of our Point()
constructor has surely kept us from having to manually assign values to the x
and y
properties of a Point
instance after it is created, if we want to create a point located at some place other than the origin.
However, even with this parameterized Point()
constructor, one thing is still not addressed. That is when we want to change the coordinates of a given point after it has been created.
For example, suppose that we create a new point object via the expression new Point(0, 2)
and then save the returned instance in the variable p1
. Now what if we want to change this point to (1, 5)?
Well, given the setup we have right now, there's only one way to do this, as shown below:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
var p1 = new Point(-2, 8);
// Reposition p1 to (1, 5)
p1.x = 1;
p1.y = 5;
console.log(p1);
Point { x: 1, y: 5 }
As can be seen, we have to manually change the respective properties one-by-one.
Once again, this manual work is tiring. The good news is that it could be prevented as well.
But how?
Well, recall the fact that inside a constructor, we initialize the instance being created by adding properties to it. Methods are simply special cases of properties so we could add them to the instance as well and thus describe its behavior.
In our case, we could define a method that serves to reposition a point to another set of coordinates. It would take in two args x
and y
, and then assign them to the x
and y
properties of the Point
object, respectively.
Let's call this method setTo()
. With this method, we would no longer need to manually write assignment statements to change the x
and y
properties of a given Point
.
In the code below, we define this method:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.setTo = function(x, y) {
this.x = x;
this.y = y;
}
}
Let's go on and use this method to do the same point repositioning that we did in the code above:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.setTo = function(x, y) {
this.x = x;
this.y = y;
}
}
var p1 = new Point(-2, 8);
// Reposition p1 to (1, 5)
p1.setTo(1, 5);
console.log(p1);
Point { x: 1, y: 5, setTo: f }
As is clear, this time we don't have to go through the manual work of reassigning both the properties x
and y
on the object p1
, separately. The repositioning is only one call away.
x
, y
and setTo
; not two properties. This is important to understand. setTo
is also a property at the end of the day — it's just a special case of a property, one that holds a function, called a methodGood!
Although this code works absolutely fine, there is now a completely different kind of a problem in it. This problem is discussed in the next section.
Problem with constructors
Any code that is written inside a constructor's body is executed for each instantiated object. In the case of mere properties, that anyways have to be maintained for each instance, this isn't a problem.
After all, each instance usually has distinct properties. But behavior is typically not local to instances.
For instance, there might be tons of different Tesla Model S cars on Earth right now, each with different properties, such as the name of the owner, the body color, etc, but they all have the same internal mechanism of braking, right?
In simple words, adding methods to instances from within the constructor in the way we add properties is usually not a good idea, since for each instance there is a separate method, yet every single one of them performs the same action.
Let's see this in more detail...
Consider the latest definition of Point()
from the last code snippet shown above:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.setTo = function(x, y) {
this.x = x;
this.y = y;
}
}
Let's create two instances p1
and p2
out of this Point()
constructor and log the type of the setTo
property of both these instances:
function Point(x = 0, y = 0) {
/* ... */
}
var p1 = new Point(-2, 8);
var p2 = new Point();
console.log(typeof p1.setTo);
console.log(typeof p2.setTo);
Quite trivially, as can be seen here, both instances p1
and p2
have a setTo
property available that points to a function.
Now, let's go on and compare the setTo()
method of p1
with that of p2
to see whether they both are the same:
No, they ain't the same!
p1
has its ownsetTo()
method while p2
has its ownsetTo()
method. Both these methods perform exactly the same task, yet they ain't the same.
This is the problem.
Our code is not DRY (Don't Repeat Yourself).
We are merely repeating stuff. Note that this repetition is not in terms of actual code — verbally, there isn't stuff that is being repeated (at least for the sake of argument). Rather, the repetition is in terms of the way the code is creating stuff in memory.
To be more precise, the code is creating a separatesetTo()
method for each instance. This separate method for each instance comes at a cost:
- Storing these different function objects assigned to
setTo
obviously consumes memory. - Storing keys on an object consumes memory as well. Here, we are referring to the key
'setTo'
. - Storing references (i.e. addresses) to function objects also takes up memory. Here, we are referring to the actual value stored inside a property.
In short, the code above is consuming a relatively large amount of memory without any purpose. To put it other way, the code above is simply wasting memory.
Obviously, for now (or practically, for most of the applications you'll make), that's not a significant amount of wastage.
But if we were to continue on with this memory-wasting coding practice to make intricately complex JavaScript applications, we would eventually end up with a performance bottleneck, due to high memory usage.
So this is the problem with constructors:
One obvious remedy to this is to create the function objects in the global scope and then create methods on the instance by means of passing a reference to these global functions.
Let's try this with our code:
function pointSetTo(x, y) {
this.x = x;
this.y = y;
}
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.setTo = pointSetTo;
}
var p1 = new Point(-2, 8);
var p2 = new Point();
console.log(p1.setTo === p2.setTo);
We have taken the definition of the function assigned to setTo
outside the Point()
constructor. In this way, it's not re-created each time Point()
is called. The log made also shows that both p1.setTo
and p2.setTo
point to the same function object.
In terms of memory consumption, this is definitely a better choice.
How?
Well, we've effectively cut down on the memory wastage that resulted from creating a new function object each time Point()
was called. Now the function has been declared once in the global scope, and just being referred to from within Point()
.
But, nonetheless, this still doesn't solve all of our problems...
Firsly, we are still defining setTo
on each instance, even though there is no need to. Ideally, stuff defined separately for each instance of a constructor should define traits of the instance (that are usually different for each instance) and in this way define its state. So that's problem number one.
Secondly, we have effectively lost encapsulation of the stuff related to a Point
object within the Point()
constructor. That is, we are now filling up the global scope with stuff that ideally should've been somehow defined as part of the Point
class only, and not anywhere else.
Imagine if we had five such global functions to be ultimately assigned to properties of a Point
instance inside the Point()
constructor. Shown below is a very simple illustration:
function pointFunction1() { /* ... */ }
function pointFunction2() { /* ... */ }
function pointFunction3() { /* ... */ }
function pointFunction4() { /* ... */ }
function pointFunction5() { /* ... */ }
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.method1 = pointFunction1;
this.method2 = pointFunction2;
this.method3 = pointFunction3;
this.method4 = pointFunction4;
this.method5 = pointFunction5;
}
This is quite a global mess, isn't it?
Plus, if the program defined some other global functions (in addition to these 5 functions) not related to the Point
class in any way, then it would've been even more of a mess.
Now, someone could go on and argue that to prevent global namespace pollution, we could wrap an IIFE around the whole code defining the Point()
constructor and its related functions.
This is possible, but much more headache than a solution. A solution ought to be one that solves a problem without imposing hundreds of problems of its own. Let's see the problems with using an IIFE here.
The code above, as encapsulated inside an IIFE, is demonstrated below:
var Point;
(function() {
function pointFunction1() { /* ... */ }
function pointFunction2() { /* ... */ }
function pointFunction3() { /* ... */ }
function pointFunction4() { /* ... */ }
function pointFunction5() { /* ... */ }
Point = function(x = 0, y = 0) {
this.x = x;
this.y = y;
this.method1 = pointFunction1;
this.method2 = pointFunction2;
this.method3 = pointFunction3;
this.method4 = pointFunction4;
this.method5 = pointFunction5;
}
})();
First of all, we have to make sure that Point
is declared outside the IIFE (line 1) in order to be accessible in the global scope.
Secondly, we have to change the Point
function declaration to a function expression (line 15) and assign that to the variable Point
.
Moreover, we also have to increase the indentation of the code inside the IIFE.
But still, we are creating separate properties method1
, method2
, ..., on each Point
instance, even though they all point to the exact same function objects internally.
Overall, the code looks extremely ugly and unmaintainable. We are creating IIFE overheads (putting them in memory, maintaining lexical environments, calling them) in our application completely unnecessarily.
If we look at the bigger picture from a distant view, everything is senseless here!
So an IIFE is clearly not a solution to our global-scope-pollution problem.
Another way could be to create an object literal holding all the functions that define behavior for a Point
object:
var pointMethods = {
function1: function() { /* ... */ },
function2: function() { /* ... */ },
function3: function() { /* ... */ },
function4: function() { /* ... */ },
function5: function() { /* ... */ }
}
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.method1 = pointMethods.function1;
this.method2 = pointMethods.function2;
this.method3 = pointMethods.function3;
this.method4 = pointMethods.function4;
this.method5 = pointMethods.function5;
}
But once again, we are creating an extra object pointMethods
here without any purpose at all, and shifting the logic of Point
s (i.e. the functions function1
, function2
, ...) out of the Point()
constructor.
Moreover, as you can see, we are again and again referring to pointMethods
in lines 19 - 23, which seems quite repetitive. Overall, the code looks even more uglier than the previous one.
So, using an object literal isn't a solution either.
Oh, wait! Let's put all the functions function1
, function2
, ..., on the Point
function object itself. After all, it's an object as well.
This is demonstrated below:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.method1 = Point.function1;
this.method2 = Point.function2;
this.method3 = Point.function3;
this.method4 = Point.function4;
this.method5 = Point.function5;
}
Point.function1 = function() { /* ... */ };
Point.function2 = function() { /* ... */ };
Point.function3 = function() { /* ... */ };
Point.function4 = function() { /* ... */ };
Point.function5 = function() { /* ... */ };
Compared to creating a new object literal to store all the given function objects, this way is slightly better in that we don't have to introduce a new identifier into the environment where Point()
exists, i.e. we don't have to create a separate object like pointMethods
.
However, regardless of any of these ways — of using an IIFE, or an object literal, or the Point()
constructor function itself to prevent global namespace pollution — we still haven't solved one long-lasting problem.
That is:
method1
, method2
, ..., method5
properties on itself that all together define behavior of the instance.As stated before, behavior of instances are common to all instances and likewise methods (that define the behavior) must not be created for each instance separately.
To boil all of this discussion down, defining methods inside a constructor function's definition is, in almost all cases, NOT a good idea.
But, then what's the good idea? How to define methods for the instances of a class?
There's one and only one answer to this — prototypes. We shall discover them in the next chapter.
The instanceof
operator
Recall that we said earlier in this chapter that creating objects using literals vs. creating them using constructor functions has the difference that we could easily distinguish between objects created using constructors.
It's now time to see how to do this...
instanceof
operator as the name suggests tells us whether a given object is an instance of a given constructor (which we could also think of as a class, remember?).It's a binary operator as might already be apparent from its definition.
Syntactically, here's how to use it:
objinstanceofF
obj
can be any valid JavaScript value while F
must be a constructor function. This expression returns true
if obj
is an instance of F
, or otherwise false
.
Let's try running it on our Point
objects p1
and p2
:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.setTo = function(x, y) {
this.x = x;
this.y = y;
};
}
var p1 = new Point(-2, 8);
var p2 = new Point();
console.log('p1 instanceof Point:', p1 instanceof Point);
console.log('p2 instanceof Point:', p2 instanceof Point);
As can be seen, p1
and p2
, as inspected using instanceof
to be instantiated by Point()
, both return true
.
Let's test in the same way some arbitrary object:
{ x: 0, y: 0 } instanceof Point
{} instanceof Point
As expected, we get false
returned in both cases, since { x: 0, y: 0 }
is not a Point
instance, and neither is {}
.
Note that as stated before, instanceof
returns true
only when the given object is an instance of the given constructor, not when the given object is the given constructor itself.
That is Point instanceof Point
won't return true
:
Point instanceof Point
To test whether a given object is a given constructor, we could very simply compare it with that constructor using ===
, quite obviously.
Point === Point
The constructor
property
When an instance is created using a constructor, there is a constructor
property available on the instance. This points to the constructor function used to create the object.
To mention it precisely, when an instance i
is created from a given constructor F()
, its prototype object is set to the prototype
property of the constructor function F
. This prototype object, that is common to all instances of F
, contains a constructor
property that points back to the constructor function.
Once again, we'll understand this much better once we are done with prototypes in the next chapter.
Anyways, let's bring into consideration our old Point()
constructor function and an instance p1
constructed out of it:
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
this.setTo = function(x, y) {
this.x = x;
this.y = y;
};
}
var p1 = new Point(-2, 8);
We'll see what does p1.constructor
have to return:
ƒ Point(x = 0, y = 0) { ... }
As can be seen, p1.constructor
returns back the constructor of p1
, i.e. the Point()
constructor.
This is exactly the same constructor Point()
, likewise we could use it to create another instance, although it's not sensible to do so if we have direct access to the constructor.
An illustration is shown below:
new p1.constructor(2, 5)
Point { x: 2, y: 5, setTo: ƒ }
constructor
property is non-enumerable, i.e. it doesn't show up in a for...in
loop. We learnt about property enumerability in the previous chapter JavaScript Objects — Property Attributes.Constructor stealing
Back in the OOP Introduction chapter, we learnt that a subclass is a class that inherits from a given superclass. For instance, the Cat
class is a subclass of the Animal
class. Every Cat
is an Animal
.
In classical OOP languages, we could create classes that inherit from given classes. The constructor method in the subclasses could call the constructor methods of the superclasses.
In JavaScript, this idea of calling the parent class' constructor from within the constructor of the subclass is possible, but not as straightforward as it might seem.
Let's first see what's the problem in going the straightforward way.
Consider the task of creating a shape drawing application in JavaScript. At the forefront, this is a big challenge to undertake. It requires us to know about a ton of things — a ton literally means a 'ton'. Obviously, at this stage, we couldn't create this. But, at least, we could make some things to be ultimately used in the application.
Let's assume that each shape is denoted by the Shape
class. A Shape
could have a fill color given as a hexadecimal value (including the hash symbol), and a position on the canvas (the main drawing area) given as two properties x
and y
representing the x and y coordinates on the canvas, respectively (in addition to numerous other properties).
The most abstracted Shape
constructor would look something like this:
function Shape(fill, x, y) {
this.fill = fill;
this.x = x;
this.y = y;
}
On its own, the Shape
class doesn't seem very useful. And, indeed, it isn't. It doesn't define the geometry of the underlying shape.
Since there can be multiple kinds of shapes, such as rectangles, circles, triangles, polygons, stars, and so on, and each of these would have a different geometry, it seems sensible to create subclasses of Shape
to represent these shapes in the application.
The subclass we'll construct right now is Rect
to denote rectangles.
Rect
could have a length
property and a height
property to determine the area spanned by the rectangle.
Let's create this Rect
class as well:
function Rect(length, height) {
this.length = length;
this.height = height;
}
Now, as we said before, Rect
is a subclass of Shape
. This simply means that every Rect
is a Shape
as well. Consequently, every Rect
object must have all the properties a Shape
object has.
Let's go on and define those properties in the Rect()
constructor:
function Rect(length, height, fill, x, y) {
// Initialize Shape properties
this.fill = fill;this.x = x;this.y = y;
this.length = length;
this.height = height;
}
Note that we had to modify the parameter list of Rect
here as well — previously, it had two parameters; now it has five of them.
But hey, wait. Isn't this repetition of code? We are redefining assignment statements for a Shape
object again inside the Rect()
constructor.
Yes, you guessed it right.
It sure is repetition of code and so we should avoid it. We could do much better than this.
One straightforward solution that might come in the mind of the novice developer (this is not you because you are quite much aware of many aspects of JavaScript at this point in this course) is to call the Shape()
constructor from within the Rect()
constructor as new Shape()
.
The thinking of calling Shape()
is perfectly alright — after all, to prevent repeating the code written inside Shape()
, we ought to call it. However, this invocation couldn't be a straightforward invocation with new
.
Let's see the problem...
Below we call the Shape()
constructor from within Rect()
in line 3, passing it all the desired arguments:
function Rect(length, height, fill, x, y) {
// Initialize this Shape
new Shape(fill, x, y);
this.length = length;
this.height = height;
}
Time to test this code. First, we'll instantiate a Rect
instance by providing some dummy arguments to Rect()
and then log each of the five properties of this instance one-by-one:
var r1 = new Rect(20, 10, '#000', 0, 0)
20
10
Can you see the problem here?
The object doesn't seem to have any of the properties x
, y
or fill
. length
and height
, though, are present. One thing is clear — there is some logical error in the code above.
Now before we go on and debug that error, take a break for a few seconds and think about the reason of this logical problem.
r1
object have properties of a Shape
?Alright, it's answer time...
In the code above, when we call new Shape()
, a new object is created and the properties fill
, x
and y
added to that object. This is just the problem.
The Shape()
constructor's invocation as a constructor simply creates a new binding for this
, that is a new Shape
instance. This instance is what receives all the initialization code of Shape()
.
Even if we were to assign Shape
to this.parent
inside Rect()
and then go like new this.parent()
, there would be absolutely no difference in the final result — the properties fill
, x
and y
would be undefined on the Rect
instance.
This can be seen as follows:
function Rect(length, height, fill, x, y) {
// Initialize this Shape
this.parent = Shape;
new this.parent(fill, x, y);
this.length = length;
this.height = height;
}
That's because a constructor's invocation creates its own binding for this
, regardless of the calling object (in this case, the object this
inside Rect()
). In layman terms, a constructor call hijacks the function's normal this
binding.
this
inside a function, refer to JavaScript Functions — this
.Any solutions?
Well, we have basically two of them:
One is already shown above, though in a slightly different way. It is to define a method on the instance inside Rect()
and point it to the Shape()
constructor, and then call this method, without using new
.
This would automatically set this
of the Shape()
function to the calling object, i.e. the Rect
instance.
Shown below is a demonstration:
function Rect(length, height, fill, x, y) {
// Initialize this Shape
this.parent = Shape;
this.parent(fill, x, y);
this.length = length;
this.height = height;
}
As before, let's test this:
var r1 = new Rect(20, 10, '#000', 0, 0)
20
10
'#000'
0
0
Yeah, it works!
The other way is to use the power of the function method call()
to configure this
for the constructor function.
This is much better than the previous way, as we don't have to create a new property (parent
in the case above) on the instance to hold the parent constructor.
Remember how to use the call()
function method? We covered it in the chapter JavaScript Function Methods — call()
.
The first argument to call()
specifies the value to be used as this
in the calling function; the rest of the arguments to call()
specify arguments to be passed into the calling function.
Therefore, in our case, first of all we need to pass in this
of the Rect()
constructor to the invocation Shape.call()
and then pass in the arguments x
, y
and fill
.
Simple?
Let's implement this:
function Rect(length, height, fill, x, y) {
// Initialize this Shape
Shape.call(this, fill, x, y);
this.length = length;
this.height = height;
}
Time for the testing:
var r1 = new Rect(20, 10, '#000', 0, 0)
20
10
'#000'
0
0
Amazing!
You'll find this second way used very frequently out there — this is because it's the simplest way to emulate parent class constructor invocation in JavaScript.
It's good to be aware of the first way in case you ever encounter a script that uses it and not the call()
function method.
So, at this point, we have successfully prevented repetition of the code within the parent constructor Shape()
by reusing it inside Rect()
.
This technique is commonly referred to as constructor stealing, or constructor borrowing, following from the fact that we merely steal the constructor's code to be used in the context of another constructor.
Static methods
In the OOP world, given a property/method, it could either be an instance property/method or a static property/method.
Instance property/method is one that is directly accessible on an instance.
In contrast to this, a static property/method is one that is directly accessible on the underlying class — there's no way to access it directly via an instance of the class.
Static properties/methods are used when some characteristics/behavior ought to be defined that aren't related to instances, but rather related to the class.
For example, the Array
class defines a static method called isArray()
. That is, the method is accessible as Array.isArray()
. This static method helps us determine whether a given value is an array or not.
The reason why it's static is because it isn't meant to be run on an Array
instance (logically that would've been senseless, as if isArray()
was an instance method, then it would've always returned true
).
The method isArray()
is meant to take any arbitrary argument and determine if that's an Array
or not.
Note that this method Array.isArray()
could've been provided as a global function (directly accessible on window
as window.isArray()
or simply as isArray()
).
The benefit of adding it to the Array
class is that the global namespace doesn't get overcrowded, and all array-related functionality sits nicely within the Array
class (where it should ideally be).
To even better understand this concept, let's add a static method to our Point
class that determines the distance between two points each given as an array of two elements, specifying the x and y coordinates of the point, respectively.
The most important thing to note here is that by the static method, we don't enforce the points to be represented as Point
objects. They could be two-element arrays, or any other sequence whose first and second items give us the x and y coordinates of the underlying point.
Consider the following code:
function Point(x = 0, y = 0) {
/* ... */
}
Point.distance = function(p1, p2) {
// Calculate distance between p1 and p2.
return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2);
}
var p1 = [0, 0];
var p2 = [3, 4];
console.log(Point.distance(p1, p2));
See how we are entertaining two arrays representing points and not enforcing them to be Point
objects first.
Stating it once again, this distance()
method on Point
could've been a distance()
function in the global scope. The benefit of using Point
is that it prevents pollution of the global namespace and nicely keeps everything related to points within Point
.
Great!
Calling constructors as functions
In the previous chapters, we've seen it numerous times that a predefined constructor is called as a function, yet it works exactly like a constructor.
For instance, if we call Array()
or new Array()
, we get exactly the same thing returned back.
How could this be accomplished for our Point()
constructor?
Well, if you think about it, it's quite simple: if the constructor is invoked as a function, we reinvoke it as a constructor (i.e. using new
). Otherwise, it means that it was invoked as a constructor and so, likewise, we proceed with the initialization code for the instance created.
But how do we check whether a constructor is called as a normal function or vice versa?
Hmm. This is a good question.
There are mainly two ways to accomplish this:
- Use the ES6-style expression
new.target
- Use the
instanceof
operator
We start by considering the latter.
We'll take the example of our very own constructor Point()
. First let's reason about how could instanceof
be used.
If Point()
is called as a constructor, what would its this
resolve to? We just learnt about this in the sections above. It would resolve to the Point
instance created.
On the otherhand, if Point()
is called as a function or as a method then what would this
resolve to? Well, if it's called as a function, this would clearly resolve to the global object window
. Otherwise, if it's called as a method, this
would resolve to the calling object.
This discussion has already laid the solution. Let's see it...
The thing is that we could use instanceof
to check whether this
inside Point()
is an instance of the Point
constructor. If this returns true
, this means that we are on the right track, i.e. Point()
was called as a constructor function (with new
).
Otherwise, it means that Point()
was called as a normal function/method (i.e. in a non-constructor context).
Easy? Let's finally implement this.
In the code below, we throw an error if the Point
constructor is called as a function, or else proceed with the initializations of the instance.
function Point(x = 0, y = 0) {
// If called as a function, throw an error.
if (!(this instanceof Point)) {
throw new Error('Illegal function call Point().');
}
// Otherwise, proceed as usual.
this.x = x;
this.y = y;
this.setTo = function(x, y) {
this.x = x;
this.y = y;
}
}
Let's try this:
new Point(2, 3)
Point { x: 2, y: 3, setTo: ƒ }
Point(2, 3)
As you can see, the second prompt here throws an error, since it calls Point()
without new
.
Note that instead of throwing an error, we could also configure Point()
to make the correct call itself. That is, when we call Point()
as a function, the function could call itself as new Point()
.
This is accomplished below:
function Point(x = 0, y = 0) {
// If called as a function,
// recall as a constructor and return the instance.
if (!(this instanceof Point)) {
return new Point(x, y);
}
// Otherwise, proceed as usual.
this.x = x;
this.y = y;
this.setTo = function(x, y) {
this.x = x;
this.y = y;
}
}
Let's try this the same way we tried the constructor above:
new Point(2, 3)
Point { x: 2, y: 3, setTo: ƒ }
Point(2, 3)
Point { x: 2, y: 3, setTo: ƒ }
Now, Point()
vs. new Point()
holds absolutely no distinction in terms of the final return value.
Spread the word
Think that the content was awesome? Share it with your friends!
Join the community
Can't understand something related to the content? Get help from the community.