import {NOTE} from './_util'
About this and function invocation context
The this object represents the function's invocation context. This module
will try to explain what this is, and what 'invocation context' means.
NOTE: One of the main problems with this is that it is hard to talk about
it. When someone says 'this is the problem', you don't know whether they are
referring to the this object or trying to point something else out.
Unfortunately, it is too late to fix this, and this is here to stay. :)
Before you start reading this module, you should peruse the func
module and make sure you have a firm understanding of what functions are in
JavaScript, and how they work. If you really feel like you'd like to read this
module first, at least remember this: in JavaScript, you (can) assign function
expressions to object properties but that is no different from assigning values
like 1 or 'hello' to it, and has no special meaning. In other words it
doesn't automatically make them a method!
Functions in JavaScript can be invoked either directly or using dot notation, as a method on an object, and a few other ways. Here's a list of the various ways a function can be invoked.
const myFunc = () => {}
const myObj = {
myMeth: myFunc
}
- (1) We can invoke a function normally (directly)
myFunc()
- (2) We can invoke it as an object method
myObj.myMeth()
- (3) We can apply it to an object using
call()orapply()functions
myFunc.apply(myObj)
- (4) Invoked using
newkeyword as an object constructor
new myFunc
Cases 2 and 3 are more or less the same, only using different syntax (which has its uses). We won't talk much about the 4th case, which is discussed separately in the proto module.
When a non-arrow function is invoked as an object's method or applied to an
object (cases 2 and 3), then the object in question is accessible as this.
If it is invoked as a constructor, then the newly created object is accessible
as this within the function body. In all other cases, the function is not
going to have a this (it will be undefined).
The last sentence is only true in ECMAScript 5 using strict mode, and later versions of JavaScript. The diagram below illustrates this:
ES3 ---- ES5 --+-- ES5 strict ---- ES6 ---->
|
global <- | -> undefined
NOTE: global refers to window in browsers and global in NodeJS.
To ensure that you always get the correct behavior, include 'use strict';
(single-quoted string) on its own line at the top of your module or use a
transpiler that does (almost all of them do).
When the value of this is defined, we say that function 'binds this' or
'defines this'. This formulation is suggestive of something that may come as
a surprise to many developers coming from primarily OOP languages. The this
binding is dynamic, not static: it is determined on each invocation, and not
at compile time. If you are coming from other OOP languages, you would do
yourself a great favor by remembering that.
Let's first look at a typical usage of this (one that's going to be the most
useful to you).
const bear = {
name: 'Teddy',
own: function (what) {
console.log(this.name + ' owns ' + what)
}
}
NOTE('Invoke bear.own()')
bear.own('honey pot') // Teddy owns honey pot
When the bear.own() function is invoked as a method, using dot notation, the
value of this is the bear object. The same function can be assigned to a
variable and invoked stand-alone.
let own = bear.own
try {
own('honey pot') // in <ES5 w/o strict mode, logs 'undefined owns honey pot'
} catch (e) {
NOTE('Error when invoking own() stand-alone')
}
The bear.own can also be passed to another function as an argument and
invoked within it.
const callWithPot = (fn) => {
fn('honey pot')
}
try {
callWithPot(bear.own) // in <ES5 w/o strict mode, logs 'undefined owns...'
} catch (e) {
NOTE('Error when invoking callWithPot(bear.own)')
}
As you can see from the examples, we get a TypeError exception if we try to
invoke bear.own without using the dot notation, because this is undefined
and does not have a name property. This happens because by assigning or
passing bear.own we merely pass the value of the property, which is a
function, and when the function is invoked, it is invoked in a different
context.
As mentioned before, if we run the same code in ES5 without strict mode we are
not going to get a TypeError, because this is bound to the global object.
This property of this has been subject to much controversy (and even more
bugs in ES5 and older), to the point where some programmers consciously avoid
using this in any form.
It is probably clear by now, but except when calling bear.own(), the fact
that we refer to it the function as bear.own has no special meaning, and it
is treated as any other function. This is why I tend to call callable
properties of JavaScript objects just 'functions' and not 'methods', although I
may sometimes say 'invoke as a method' to emphasize the dot notation. I also
avoid using terms like 'static method' or 'instance method' for the same
reason.
The context can be affixed using the bind() function. For instance:
own = bear.own.bind(bear)
NOTE('Calling bound own()')
own('bound honey pot') // Teddy owns bound honey pot
In the above code, we have redefined the own variable to use a bound version
of bear.own which has this bound to bear. This binding allows us to
invoke own() stand-alone. The binding is permanent, and cannot be overridden
by additional calls to bind(). Although I haven't run into formal terminology
for functions that are bound, I like to call them 'bound functions'.
The this binding can also be changed temporarily using call() and apply()
functions which are properties on the function objects. These two functions are
similar, and serve the same purpose. The main difference is in how they treat
the arguments.
Let's define another object to work with.
const deer = {
name: 'Bambi'
}
The deer object we've defined does not have the own function, but we can
borrow one from the bear.
NOTE("Borrowing bear's own() function")
bear.own.call(deer, 'flower pot') // Bambi owns flower pot
The call() call invokes bear.own() and binds this to the first argument
(deer object), passing the rest of the arguments to bear.own().
If for some reason we don't know how many arguments we will have at runtime, we
can use apply(), which takes the arguments as an array.
NOTE('Borrowing own() using apply()')
bear.own.apply(deer, ['flower pot']) // Bambi owns flower pot
As I mentioned before, both call() and apply() do the same thing.
We have already mentioned that the this binding is dynamic. It just so
happens that you can use this to your advantage quite easily. We will show this
by splicing the bear's own() function into the deer object.
deer.own = bear.own
NOTE("Using deer's own() function")
deer.own('flower pot') // Bambi owns flower pot
Invoking the own() function as deer's method correctly uses the name
property on deer and not bear where the function was originally defined.
You can imagine that this type of borrowing can dramatically simplify code
where you would normally use inheritance in languages that statically bind
methods to objects.
We said that this is bound by a function. This includes any and all non-arrow
functions. This is also true when a function is defined within another one. A
common scenario is trying to access the outer function's this. A solution to
this issue is to assign this to a variable in the outer scope.
const alligator = {
name: 'Al',
bites: 12,
eat: function (food) {
const that = this;
(function bite() {
// `this` in `bite()` is undefined, but `that` points to
// `eat()`'s `this`
if (that.bites <= 0) {
console.log(that.name + ' is full')
return;
}
console.log(that.name + ' took a bite out of ' + food.name)
that.bites -= 1
return bite()
})()
console.log(this.name + ' is going to bed now')
}
}
alligator.eat(deer)
// Al took a bite out of Bambi
// ...
// Al took a bite out of Bambi
// Al is full
// Al is going to bed now
You will see JavaScript programmers assign this to variables of different
names, including the popular choices like that and self. In general, you
should stick to the existing convention in the code base, or use one of the
popular choices for clarity.