2020-12-30
|~5 min read
|964 words
Javascript contexts are a concept that continues to cause me confusion. What is this
and how does it work?
For the most part, I can avoid issues by using functional programming or remembering some of the lessons I’ve learned in the past about using classes in Javascript.
I’d prefer to actually understand, however, so I spent some time recently reviewing Javascript’s Lexical Scope, which I described ”Closure & Lexical Scope” this way:
When we say that Javascript is lexically scoped, what we mean is that a function will have access to the variables in the context in which it was defined not in which it is called (as far as those details are relevant — which is a garbage collection optimization).
How do Arrow functions fit into this? What about apply, bind, and call? When would I use them vs. an Arrow function? How do they differ from one another?
In this post, my aim is to answer these questions.
Before we get into the specifics about these functions, I thought it’d be useful to see the problem.
In MDN’s article on Arrow Functions, they have the following example to show a function declaration’s native behavior to default this
to the window’s scope:
window.age = 10 // <-- notice me?
function Person() {
this.age = 42 // <-- notice me?
setTimeout(function () {
// <-- Traditional function is executing on the window scope
console.log("this.age", this.age) // yields "10" because the function executes on the window scope
}, 100)
}
var p = new Person()
This example is perfect for my purposes, because it demonstrates how confusing this
can be - particularly because if we remember what I said earlier about lexical scope!
A function should have access to the variables in which it was defined, not in which it was called, and yet, in this case, it seems like that’s not the case since the console.log
prints 10
, not 42
.
“Fixing” this (quotes as it’s a bug only in so far as it is unexpected) relies on understanding the context of the function and the tools at our disposal.
Javascript provides several tools adjust the context of function invocations.
There are the three functions on the function prototype - apply
, bind
, and call
. Then, there’s the Arrow function.
Let’s start with the “modern” approach: arrow functions.
The solution that arrow functions provide to the problem of confusing contexts is actually beautiful in its elegance. It’s not that there’s magic occurring, Arrow functions simply do not have their own this
.
When the function refers to this
then, which is not present in the current scope, the runtime engine will look up the chain (following normal inheritance lookup rules) and, in our example, find a this
in the enclosing lexical scope (remember, “a function will have access to the variables in the context in which it was defined”).
window.age = 10 // <-- notice me?
function Person() {
this.age = 42 // <-- notice me?
setTimeout(() => {
// <-- Traditional function is executing on the window scope
console.log("this.age", this.age) // yields "10" because the function executes on the window scope
}, 100)
}
var p = new Person()
Running this in the browser, we can see that by the time we get to the setTimeout
, this.age
takes the value of 42
.
The apply method on the function prototype enables specifying the a given context (this
) with which to call a function. It’s optional second parameter is an array of arguments to call the function with.
Let’s apply this to our example.
window.age = 10 // <-- notice me?
function Person() {
const func = function () {
// <-- Traditional function is executing on the window scope
console.log("this.age", this.age) // _would_ yield "10" because the function executes on the window scope
}
this.age = 42 // <-- notice me?
setTimeout(func.apply(this), 100) // <-- specifying `this` overrides the behavior and ultimately yields "42"
}
var p = new Person()
Note: Because of how similar apply
and call
are, in this case, apply
could be replaced with call
to yield a similar result.
Since we’re applying this
within the context of Person
where this.age
is 42
, that’s what is ultimately yielded.
apply
and call
are nearly identical. The primary difference is in the second parameter. Where call
accepts a list of arguments, apply
takes an array of arguments.
This feature makes apply
quite useful in cases where arguments are collected via a rest operation, e.g.,
function myFunc(...args) {
return Math.min.apply(null, args)
}
Unlike apply
and call
, bind
returns a function to be invoked later with a specified context. This is particularly useful when currying (which we’re not doing in this example).
window.age = 10 // <-- notice me?
function Person() {
function greeting() {
// <-- Traditional function is executing on the window scope
console.log("this.age", this.age) // yields "10" because the function executes on the window scope
}
this.age = 42 // <-- notice me?
greeting() // <-- unbound version will yield "10"
setTimeout(greeting.bind(this), 100) // <-- bound the `greeting` function to the `Person` context, and so when invoked here will yield "42"
}
var p = new Person()
While understanding how apply
, call
, and bind
work is extremely helpful, I still vastly prefer the directness of Arrow functions and not worrying as much about this
.
Of course, there are situations where classes, object oriented programming, and this
are the right tool for the job. It’s for those cases that I’m glad I know a bit more about these tools and how they work.
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!