javascript: apply vs. bind vs. call

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.

Seeing Contexts

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:

example.js
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.

Apply, Bind, and Call vs. Arrow Functions

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.

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”).

arrowExample.js
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.

arrow-function-scope

Apply (And Call)

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.

applyExample.js
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.

Differences Between Apply and Call

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.,

restExample.js
function myFunc(...args) {
  return Math.min.apply(null, args)
}

Bind

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).

bindExample.js
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()

Considerations and Parting Thoughts

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!