3 Techniques for Maintaining Your Sanity Using "This" in JavaScript
Of JavaScript's many confusing aspects, the keyword this
can be one of the most complicated -- Here's a joke about the troublesome keyword:
this
is frustrating. Every time you think you have it, another weird case shows up - it should be simple, so why does it never seem to work like you want it to?
Why "this" is confusing
In other programming languages, this
always refers to the current instance of an object. It's a very consistent keyword which will only ever hold two values: the current object, or nothing.
In JavaScript, this
refers to what is known as the execution context. In practical contexts, this is deceptively similar to other languages version of this
, but contains a fundament difference: the execution context is different based on how a function is called.
This means that JavaScript's version of this
may have different values depending on how you called the function.
class Foo {
text = "string";
trigger() {
// Because of how the function is being called, `this` can have
// several different values
this.text = this.text + "a";
}
brokenTrigger() {
// `this` will refer to the current object, so it will act as we expect
this.trigger();
// setTimeout resets `this` to the global context object - in web
// browsers, it is the Window object
setTimeout(this.trigger, 500);
// When we refer to the function directly (without the object)
// `this` refers to the global context object (window)
const unboundFunction = this.trigger;
unboundFunction();
// Event listeners replace "this" with the target element
// `this` will refer to the clicked ".triggerButton"
let button = document.querySelector(".triggerButton");
button.addEventListener('click', this.trigger);
}
}
How to use this
safely
When you see all the ways that this
can go wrong, it seems like the easiest option is to throw your hands in the air, become a hermit and start a small potato farm.
In practice, this
tends to be far less problematic than these examples make it appear. Most of the weird behaviours of this
are easy to avoid by restricting your use of this
to object functions, where it is the most consistent
As I said in the intro, using this
with an object is almost always going to refer the object instance, but you do have to watch for two major exceptions:
setTimeout
addEventListener
In these cases, we have several techniques at our disposal to control the value of this
, and to make sure that it works how we want.
Technique 1: Use Fat Arrow Functions
Fat Arrow Functions, aside from being a quick way of declaring functions, differ slightly from other function declarations in that they won't allow anything to overwrite this
. Instead, it keeps the value from where the function is declared (its lexical scope).
What this means is that we can use them as wrappers, or directly as event listener function calls to preserve our this
reference.
class Foo {
listen() {
// `this` still refers to Foo
document.querySelector('.class').addEventListener('click', (e) => {
this.handler(e);
// or
this.val = 1;
});
}
handler(e) {
this.val = 1;
}
}
Technique 2: Assign this
to a variable
Before ES6, a popular pattern was to copy the value of this
when we knew it referred to our object and used the new variable instead.
var foo = {
listen: function() {
// These are both common names for our new `this`
var that = this;
var self = this;
document.querySelector('.class').addEventListener('click', function() {
self.val = 1;
});
}
}
Technique 3: Explicitly set this
with Function.bind
Functions come with several tools to set the value of this
explicitly so you can guarantee the value of this
.
Function.bind
Function.apply
Function.call
In practice, Function.bind
is the most useful of the three, since it doesn't immediately call the function, instead returning a new version with a pre-set this
, and any parameters you pass - you can use this new function directly in setTimeout
or addEventListener
function and keep your value of this
.
class Foo {
listen() {
// The first paramter of `bind` is the new `this` value
document.querySelector('.class').addEventListener('click', this.handleEvent.bind(this));
}
handleEvent() {
this.val = 1;
}
}
Bonus Technique: Use Strict Mode
JavaScript's Strict Mode slightly changes the behaviour of this
. Instead of implicitly setting this
to the global context outsides of objects, it causes it to be undefined
instead.
In practical terms, this is a fairly minor change, but it prevents several incorrect usages of this
, and cause a would-be-hidden bug to throw an error instead:
'use strict';
let obj = {
update(val) {
// Normally this will create an `x` property on the global object and
// continue running, but in strict mode this will throw an error
this.x = val;
}
}
// Breaking the reference to `obj` causes `this` to be undefined instead
// of referring to the global object
let func = obj.update;
func();
Don't overcomplicate it
If you regularly read my blog, you'll know this is basically my slogan.
There's no denying that this
is strange, but that doesn't mean that you need to worry about all the edge cases that it presents - More often than not, they don't come up.
I've been a Web Developer for coming on 8 years now, and I learned some new edge cases about this
when preparing this post that I have never encountered before.
Instead of spending your time memorising every way this
can go wrong, start learning a framework, or get out there are create some practice projects to get a better sense of how it works.
If you want to read more about some JS fundamentals, you can check out this guide to closures.