JS gotchas with this and new
Note
This was originally posted on Medium.com on December 23, 2015, but I’m reposting it on my personal blog on January 5, 2017.
Introduction
Love it or hate it, you’ve probably used JavaScript at some point. JavaScript
has two keywords to aid in writing
object-oriented
code: this
and new
. Despite the Java namesake for JavaScript, these keywords
work completely differently in JavaScript, and are frequently misunderstood or
used improperly on accident. This post explains the behavior of this
and
new
, and explores common mistakes with using them, as well as how to write
more resilient code without them.
Strict mode behavior of this and new
The current version of ECMAScript (the official standard name for JavaScript) is
ES2015, but back with the
release of ES5, a feature was added called strict
mode.
Strict mode
is used by starting a file or function with the string literal "use strict"
(including quotes). Strict mode changes the semantics of many areas of the
language, so I will be covering how it affects this
and new
as well.
I will start with an explanation of how this
works in strict mode, since it’s
easier to follow. You can think of this
as an implicit parameter to every
JavaScript function. You do not declare it in the parameter list and you can’t
pass it like a normal argument.
This with function calls
function add(x, y) {
console.log("this =", this);
return x + y;
}
add(3, 4); // Prints "this = undefined"
In the example above, this
is implicitly a parameter of add
, like it is with
every function, and it’s implicitly passed to the function in the console.log
line. When a function is invoked in the default manner like foo(x)
, this
is
set to undefined
.
This with method calls
There is another case where this
is passed implicitly: foo.bar()
and
foo["bar"]()
. When the function itself is being referenced directly from an
object, the parent object itself is passed as this
, which would be foo
in
this case.
var janelle = {
name: "Janelle",
getName: function () {
return this.name;
},
};
console.log(janelle.getName());
This will log “Janelle” to the console, as we wanted. But let’s make a slight change and see how this works:
Gotcha: Forgetting this
var janelle = {
name: "Janelle",
getName: function () {
return this.name;
},
};
function greet(getName) {
console.log("Hello, ", getName());
}
var getName1 = function () {
return "Lukas";
};
var getName2 = janelle.getName;
greet(getName1); // "Hello, Lukas"
greet(getName2); // Error, cannot get property "name" of undefined
Ok, so what happened here? Notice that even though we get the function from
janelle.getName
, we invoke it using the plain style as getName()
, so
undefined
is passed as the value of this
. JavaScript has a built-in method
for dealing with this—bind
—used like janelle.getName.bind(janelle)
. This is
a bit wordy, and incredibly easy to forget, but it does give you a new function
which will pass janelle
as the value of this
, even if the function is
invoked in the plain style like getName()
with no object.
Functions which accept functions as parameters (known as
higher-order functions)
are all over the place in JavaScript:
setTimeout,
Array.prototype.map,
Array.prototype.forEach,
Promise.prototype.then,
and many more. Any of these functions are potential places for errors when
passing functions using this
from an object as an argument.
An infuriating real life example
A practical example you may have run into before involves promises and
console.log
. Unfortunately, the console
object is not governed by any
standard, so any JavaScript implementation can omit it or change the behavior
however they want. In the past, the methods on console
did not use this
, so
they were resilient in the face of plain invocation. That means this code sample
used to work just fine:
Promise.resolve("hello world").then(console.log);
Unfortunately now calling console.log
with an incorrect this
value causes an
illegal invocation error. Couple this with promises swallowing all errors, and
you have a line of code that will print nothing and report no errors. There are
two common solutions to this problem:
Promise.resolve("hello world").then(function (x) {
console.log(x);
});
// or
Promise.resolve("hello world").then(console.log.bind(console));
Neither of those are particularly nice, and could be avoided by using a function
that never used this
in the first place.
Explicitly passing this
There are also two explicit ways to pass this
to a function:
.call
and
.apply.
They are invoked as methods from a function, as follows:
console.log.call(console, 1, 2, 3, 4);
console.log.apply(console, [1, 2, 3, 4]);
Both lines are equivalent. The first parameter in both is the value of this
to
send, and then call
takes the arguments normally afterward, whereas apply
takes an array of arguments as its second and final parameter.
Another common workaround
The method call
is used in functions like map
and forEach
which have
optional “context” parameters (the value of this
to use in the callback).
var selectors = ["p", "a", "img"];
var elems1 = selectors.map(document.querySelector, document);
var elems2 = selectors.map(document.querySelector.bind(document));
Both elems1
and elems2
contain the same values and don’t throw any errors
because the value of this
has been preserved correctly, by passing a value for
this
that the map
method can use explicitly with the method call
. But with
elems2
we can see that context parameters are unnecessary when we have the
method bind
.
Strict mode behavior: New
The keyword new
does quite a few things in the expression new Foo(x)
.
-
Let
obj
be a new object with no properties. -
Set the prototype of
obj
toFoo.prototype
. -
Let
res
be the result ofFoo.call(obj, x)
. -
If
res
is an object, returnres
. Else returnobj
.
So first of all that’s quite a bit different than just calling Foo(x).
This
set of steps allows for code like:
function Person(name) {
this.name = name;
}
Person.prototype.speak = function () {
return "Hello, I am " + this.name;
};
var anika = new Person("Anika");
anika.speak(); // "Hello, I am Anika"
Notice how the function Person
implicitly returns undefined
(not an object)
so that “else” branch of step 4 will be executed. Functions intended to be used
only with new
are called constructor functions.
Gotcha: Forgetting new
Most functions are called without using the keyword new
in JavaScript, so it’s
easy to forget. If you call a function made like the above Person
without
new
, the value of this
will be set to undefined
, and this.name
will
throw an error.
Workaround: Not using a constructor function
Luckily, it’s actually easier to just not use new
or constructor functions at
all.
function createPerson(name) {
function speak() {
return "I am " + name;
}
return {
name: name,
speak: speak,
};
}
var anika = createPerson("Anika");
anika.speak(); // "Hello, I am Anika"
Notice how we didn’t use this
or new
, so we avoid several kinds of errors
for consumers of our person API.
Alternatively, we can just avoid methods all together and make a simple module that operates on plain data (such as that returned from JSON APIs):
var Person = {
create: function (name) {
return {
name: name,
};
},
speak: function (p) {
return "I am " + p.name;
},
};
var anika = Person.create("Anika");
Person.speak(anika); // "Hello, I am Anika"
Non-strict mode
When not in strict mode, this
behaves a little differently. If you pass a
non-object value as this
, it is converted to an object. If the value is a
number, string, or boolean, it’s converted to a wrapped object version. If it’s
null
or undefined
, it’s converted to the global object (known as window
or
global
). This makes constructor functions even more dangerous, as forgetting
to use new
can make them accidentally set properties on the global object
(i.e. set global variables).
Note that it doesn’t matter if your code uses strict mode, it matters if the function you’re calling was written using strict mode. And because it’s opt-in, most code doesn’t use it.
Other considerations
My primary concerns with code are correctness and readability. There are
performance benefits in most JavaScript engines to using particular patterns
along with this
and new
, but that is simply not as important to me as
avoiding all of its potential problems. If you are making a video game in
JavaScript, this performance difference might matter, but it’s likely not your
bottleneck in any other kind of application.
And if you’re thinking about prototypal inheritance, it’s equally awkward
whether you use new
or not, but not featured in this article for brevity.
Some say avoiding this
and new
is not idiomatic JavaScript. I think
generally those kind of comments just mean “I don’t like it”, but in this case
it’s just plain wrong. Consider document.createElement("div")
or
$("div.foo")
, both plain functions that hide away any internal details about
new
.
Safer, easier JavaScript
As you’ve seen, the behavior of this
and new
is nothing magical, but both
both constructs are more error prone than they need to be. Luckily, JavaScript
is a powerful enough language that we do not need either of those features to do
object-oriented programming. Don’t let anyone shame you into thinking you’re
doing JavaScript “wrong” or you’re not “harnessing the power of prototypes” if
you want to write your code this way. Good luck, and have fun writing JavaScript
with two dangerous tools out of your way.