Making sense out of context
Published on December 30, 2016 under the category JavaScriptThe this
keyword is probably one of the most misunderstood aspects of JavaScript. At the root of it for me, it allows us to reuse
functions within different contexts, or in other words, it allows us to decide which objects should be focal when invoking a function.
Whenever you're trying to figure out what the value of this is, you need to look at where the function is being invoked. Not when it was defined, but specifically when it was called.
Let's say we have a function called sayHi
which takes in a name and logs to the console Hi, ${name}
, name
being whatever argument name
we pass in.
function sayHi(name) {
console.log('Hello, my name is ' + name);
}
If I were to ask you right now what name this last function is logging to the console, you wouldn't have a clue because you can't possibly
know what name
is until the function is invoked.
The exact same idea holds for the this
keyword. We can't know what the value of this
is until that function gets run.
Context Bindings
There are four different types of bindings in JavaScript:
- Implicit Binding
- Explicit Binding
new
Bindingwindow
Binding
Implicit Binding
TL;DR: look at the immediate left of the dot at call time.
Whenever you call a function that's attached to an object, look to the left of the dot, and that is what the this
keyword is going to be.
var Person = function (name) {
return {
name: name,
sayHi: function () {
console.log('Hi, my name is ' + this.name);
},
};
};
var homer = new Person('Homer Simpson');
homer.sayHi(); // "Hi, my name is Homer Simpson"
Now, what if we had a nested object within this homer
object called daughter
which also had a name
property and a sayHi
method?
var homer = {
name: 'Homer Simpson',
sayHi: function () {
console.log('Hi, my name is ' + this.name);
},
daughter: {
name: 'Lisa Simpson',
sayName: function () {
console.log('Hi, my name is ' + this.name);
},
},
};
homer.sayHi(); // Hi, my name is Homer Simpson
homer.daughter.sayHi(); // Hi, my name is Lisa Simpson
Well, this doesn't really matter as we always look at what's to the left of the dot, in this case, homer.daughter.sayHi()
is going to be
bound to the daughter
object. The object which is immediately to the left to the dot is always the context used.
Explicit Binding
TL;DR: we explicitly tell a function what the context (= value of the
this
keyword) is by using one of these prototype methods:call
,apply
orbind
.
This is about explicitly setting the value of this
to a function upon execution.
Let's say we have a getCountryDetails
function which is not a method on any object we've created, rather a function attached to the global
scope:
function getCountryDetails() {
console.log(
this.country +
"'s capital city is " +
this.capital +
' and as of ' +
arguments[0] +
' the country has a population of ' +
this.population +
' according to ' +
arguments[1],
);
}
Note that getCountryDetails
looks like a free flowing function, but there are actually no free flowing functions in JavaScript. They're
all attached to some object. In this case, getCountryDetails
is attached to the global window
object.
Now, what we want to do is somehow call this function in the context of these two countries:
var au = {
country: 'Australia',
capital: 'Canberra',
population: '23M',
};
var nz = {
country: 'New Zealand',
capital: 'Wellington',
population: '4M',
};
var today = '30/12/2016';
var source = 'Wikipedia';
We've got a number of ways to achieve this.
Function.prototype.call
Function.prototype.apply
Function.prototype.bind
What this means is every function (that is, every instance of Function
) has a .call
method, an .apply
method and a .bind
method that
allows us to do what we want to do.
i. Call
Here's the signature for call:
fun.call(thisArg[, arg1[, arg2[, ...]]])
In layman's terms this means:
- The very first argument
call
takes in is the context you want to use. - Afterwards, you can pass in any number of comma-separated values which will be treated as arguments for the function
fun
.
You can remember this because: call starts with "c" as in "comma-separated arguments" 💥
Here's our country details example using call
:
getCountryDetails.call(nz, today, source);
// "New Zealand's capital city is Wellington and as of 30/12/2016 the country has a population of 4M according to Wikipedia"
ii. Apply
The signature for apply is as follows:
fun.apply(thisArg, [argsArray]);
As opposed to the call
method, this one takes only two arguments:
- First argument is the context we want to use.
- Second argument is an array of values which are gonna be used as the arguments passed in to the function
fun
.
You can remember this because: apply starts with "a" as in "array of arguments" 💥
Again, here's our country details example using apply
:
getCountryDetails.apply(au, [today, source]);
// "Australia's capital city is Canberra and as of 30/12/2016 the country has a population of 23M according to Wikipedia"
iii. Bind
bind
is almost the exact same thing as call
, except it defers the function invocation, i.e.: bind
returns a whole new function instead
of invoking the original one.
This new function is bound to the scope we pass in and also keeps track of the comma-separated values we've provided as arguments.
var getAustraliaDetails = getCountryDetails.bind(au, today, source);
// nothing happens just yet...
getAustraliaDetails(); // we need to manually run the resulting function
// "Australia's capital city is Canberra and as of 30/12/2016 the country has a population of 23M according to Wikipedia"
If we were to recreate Function#bind
, this is what it'd look like:
Function.prototype.bind = function (context) {
var fn = this;
var outerArgs = [].slice.call(arguments, 1);
return function () {
var innerArgs = [].slice.call(arguments, 0);
fn.apply(context, [].concat(outterArgs, innerArgs));
};
};
One more thing about bind
. This is a simple example of a functional programming technique called partial application which allows the
remaining arguments to be passed in when the new bound function is called:
var missingSource = getCountryDetails.bind(au, today);
var missingAllArgs = getCountryDetails.bind(au);
missingSource(source);
missingAllArgs(today, source);
Let's recap:
call
,apply
andbind
they all allow us to explicitly set the value ofthis
when calling a function.call
andapply
both invoke the bound function immediately.bind
doesn't invoke the bound function immediately, rather returns a new function we can run later.call
andapply
differ only in the format they take extra arguments in: use comma-separated values forcall
, and an array of values forapply
.bind
takes extra arguments as comma-separated values, just ascall
does.
new
Binding
TL;DR: whenever we've got a function invoked with the
new
operator, thethis
keyword here is bound to the new object being built by the function constructor.
Let's say we've got this Dog
function constructor which takes in a name, a breed and a colour. By the way, we have capitalised the first
letter to express that this is function constructor, meaning it should be instantiated (i.e.: called with the new
operator).
var Dog = function (name, breed, color) {
// this = {} --> `this` equals to a brand new object
// being created by the `new` operator whenever the
// function gets instantiated.
this.name = name;
this.breed = breed;
this.color = color;
this.sayHi = function () {
console.log('My name is ' + this.name + ', woof!');
};
};
Let's now instantiate a puppy:
var puppy = new Animal('Rufus', 'German Shepherd', 'Black');
puppy.sayHi();
As we're invoking the function constructor with the new
keyword, JavaScript is (under the hood) creating a new object for us and saving it
as our this
, thus generating a brand new context.
This means, whenever we are instantiating a function constructor, we don't need to worry about binding the new instance to any particular context as it already has its own.
Window Binding
TL;DR: if none of the other rules apply, then the
this
keyword defaults to thewindow
object. This holds true as long as you are not in strict mode, otherwisethis
isundefined
.
Let's go back to our sayHi
function:
function sayHi() {
console.log('Hi, my name is ' + this.name);
}
What if we just tried to invoke sayHi
without specifying any context? Here we're not using call
/apply
/bind
, there's also nothing to
the left of the dot, nor we are using the new
operator.
sayHi(); // undefined
Well, in this case we'll just get undefined
back, because what's happening here is if you run a function that uses the this
keyword but
it's not bound to anything else (that is, doesn't meet any of the previous binding rules), then this
is going to automatically default to
the window
object.
As a result, if we add a name
property to the global window
object we'll no longer get undefined
:
window.name = 'Homer'; // could also be `var name = 'Homer';`
sayHi(); // we now get "Hi, my name is Homer"
What's interesting though, is that if we run this function in strict mode, we get an error because strict mode is smart enough to know this
might not be what we want. It kind of says: "Hey, you probably don't want to do what you're doing. You might now want the this
keyword to
reference the window
object, so I won't let you do that".
function sayAge() {
'use strict';
console.log(this.age);
}
sayAge(); // bummer! type error thrown
ES6 Goodness: Arrow Functions
Arrow functions are automatically bound to their lexical scope, i.e.: this
gets passed in from the parent into the arrow function.
function () {
this.greeting = "G'day!";
// If we don't bind this function, it'll echo out `undefined`
setTimeout(function () {
console.log(this.greeting);
}.bind(this));
// Binding here happens automagically using the parent scope
setTimeout(() => {
console.log(this.greeting);
});
}
Some credits: I've first seen the terms implicit/explicit/new/window binding on this Egghead lesson by Tyler McGinnis.