Understanding `this` in JavaScript
What better topic to start the blog with. Let’s look at how this
works in JavaScript.
Level 0: The Basics
At the root scope, this
refers to the global object or undefined
if in strict mode. In browsers, the global object is window
.
console.log(this);
// logs the `window` object
(function(){
'use strict';
console.log(this);
})()
// logs `undefined`
Level 0 🔑
Within a function, what
this
refers to depends on how the function is invoked, NOT how it’s defined.
You should also keep in mind that you can’t directly set the value of this
from within a function.
function foo(){
this = 'trying to set this!';
}
foo(); // Uncaught ReferenceError: Invalid left-hand side in assignment
We’ll be working with the following function for the remainder of this article.
function printThis(){
console.log(this);
}
By mere inspection, I couldn’t tell you what this will log. I need to know how the function will be invoked.
And this leads us to …
Level 1: The Four Invocation Patterns
There are four ways to execute a function in JavaScript
The function call
This is the regular way of calling a function
printThis()
As mentioned above, the output of our function depends on whether we’re in strict mode or not. If not in strict mode, this
will refer to the global object, window
in browsers. If we are in strict mode, then this
will be undefined.
The method call
Suppose we instead had an object like this:
var obj = {
print: printThis
}
And we invoked the function like this obj.print()
.
Our output will be the object obj
. In this case, this
refers to the object the method is attached to. This rule is often summarized as “that which is to the left of the dot”.
Note that the following is not the same thing:
var printB = obj.print;
printB();
Remember Level 0, this
depends on how the function is called. The above code puts us back into the regular function call case, which will then log the global object or undefined.
The constructor
Suppose we had the following:
function Obj(){
this.x = 'foo';
}
var object = new Obj();
We’ve invoked the Obj
constructor function using the new
keyword. In this case, this
now refers to the newly created object. This object’s prototype is equal to Obj.prototype
.
Using apply/call
We can change the value of this
to anything we like using call
or apply
. The first argument to either method is the value that this
will take within the function.
A useful mnemonic is that apply
accepts an array as it’s second argument (they both start with a).
call
accepts each argument separately.
var obj = {prop: 'a property'});
printThis.call(obj)
// logs `obj`
This really drives home the point that this
can be anything since the caller can use call
or apply
to change it to anything they desire.
Many libraries tend to set the value of this
to something useful when you pass a callback to a library function. For example, in jQuery, this
will often be set to the DOM element where a bound event is taking place:
$('#foo').click(function(){
console.log(this);
// logs the `#foo` element
})
Using bind
We can use the native bind
method to “hard bind” the value of this
. No matter how this bound function is invoked, the value of this
will stay the same.
var obj {
foo: function(){console.log(this)}
}
var method = obj.foo;
var boundMethod = obj.foo.bind(obj);
obj.foo();
// logs `obj`
method();
// logs `window` object
boundMethod();
// logs `obj`
One caveat is that when using a bound function as a constructor, the bound value of this
is ignored.
function MyObj {
this.x = 'foo';
}
var o = {y: 'hello'}
var boundMyObj = MyObj.bind(o);
boundMyObj();
console.log(o);
// logs {x: 'foo', y: 'hello'}
var bar = new boundMyObj();
console.log(bar);
// logs {x: 'foo'}
Level 2: Arrow Functions
In addition to being a more concise syntax, arrow functions also differ in how this
is bound. The value of this
within an arrow function body is equal to whatever this
was in the surrounding scope (“lexical this
”).
function foo() {
this.x = 'hello';
[1,2,3].forEach(()=>{
console.log(this.x);
});
}
new foo();
The above snippet logs hello
3 times. If we were to use a non-arrow function, we get undefined
logged 3 times. This is because the value of this
within the forEach
callback would refer to window
rather than our newly created foo
object.
Level 3: Advanced
Levels 0 through 2 will probably account for 99% of the cases you’ll encounter in real life. But there are some esoteric edge cases that aren’t covered.
This section is based on the material found in this fantastic blog post.
Consider the following lines, what would you expect the value of this
to be?
var foo = {bar: function(){console.log(this)}};
(1, foo.bar)();
(g = foo.bar)();
(foo.bar)();
The first two lines log the window
object but the last line logs foo
. It’s a bit surprising isn’t it?
To understand what’s happening, we have to understand references.
A reference is essentially an internal data structure that the JavaScript compiler uses to “reference” a function. It can be thought of as an abstract object that looks like this:
var Reference = {
base: EnvironmentRecord, // determines the value that `this` will take
name: "foo", // the function name
strict: false // whether we're in strict mode
};
So a reference isn’t an actual function.
The comma operator and simple assignment don’t create references. This means that the first two examples above are more like doing (function(){console.log(this)})()
.
The grouping operator does create a reference. That’s why we still log foo
in the last example.
If you want to learn more about references, check out this blog post.