Javascript Bind 101

November 22, 2019

bind is a common JS keyword. If you read enough JS code, you probably have seen it (this.something = this.something.bind(this) in React, anyone?). Whenever I see bind, I would get really confused. If this is you, by the end of this article you are bound (see what I did there 😎?) to understand bind much better!

Warning: knowledge of this is a prerequisite to understand bind. I wrote an article about this, check it out!

Bind has many applications and it would be impossible to cover in a short article like this, but I think the essentials are:

  1. bind in a regular function
  2. bind's 2nd argument
  3. bind in arrow function
  4. bind in callback function

So what is bind?

JS bind "binds" a function's this method with your value. It can also "bind" arguments.

Bind in a regular function

What does it mean to bind a function's this method?

Recall that this, when called from inside a function, refer to an object where that function is called from.

const obj = {
  breakfast: 'pancakes',
  sayBreakfast(){
    console.log(`I had ${this.breakfast} for breakfast`)
  }
}

obj.sayBreakfast() // pancakes

With bind, we can bind sayBreakfast's this value to anything we want.

const waffles = obj.sayBreakfast.bind({breakfast: "waffles"})
waffles() // waffles
obj.sayBreakfast() // pancakes

Bind's 2nd argument

Let's use an example of a regular function, inside an object, that takes an argument:

const food = {
  entree: 'fried rice',
  sayLunch(appetizer) {
    console.log(`I  had ${appetizer} and ${this.entree} for lunch`)
  }
}

food.sayLunch('dumplings') // dumplings and fried rice

Let's bind it with sushi:

const lunchie = food.sayLunch.bind({entree: 'sushi'})
lunchie() // undefined and sushi

Woops, we still need to pass an argument - otherwise it returns undefined, so let's do that:

lunchie('miso soup') // miso soup and sushi

Bind's 2nd argument can "lock" argument(s) values - giving it pre-specified values.

const lunchy = food.sayLunch.bind({entree: 'tacos'}, 'chips')
lunchy() // chips and tacos
lunchy('burritos') // still chips and tacos

If you want to give a function pre-specified values but don't want to bind anything, put null as first argument.

const addStuff = function(first, second) {
  return first + second
}

addStuff(10,5) // 15
const addHundred = addStuff.bind(null, 100) // first is now 100
addHundred(3) // 103
addHundred(1,5) // 101

We bound the first argument to 100. Once bound, first will always be 100. Hence addHundred(1,5) is returning 101 and not 6. 1 becomes second argument and 5 is technically the third argument now.

Bind in arrow function

This section assumes a lot of this knowledge.

From my previous article, I mentioned:

  1. Only regular function and global function can have this.
  2. Arrow function does not have this on its own
  3. When this is referred to inside an arrow function, it will look up the scope to find this value. It behaves like lexical scope.

We will keep these in mind as we go through the last 2 sections.

Let's start by binding {a: "rock"} into sayThis arrow function.

const sayThis = () => console.log(this);
sayThis() // window obj
const imnotarock = sayThis.bind({a: "rock"})
imnotarock() // still window

It still returns window because arrow function does not have its own this. It looks up lexically for either regular function or global object's this.

This would have worked fine on regular function:

const sayThisAgain = function(){console.log(this)}
const imarock = sayThisAgain.bind({a: "rock"})
imarock() // {a: "rock"}

Although we can't bind this, we can give an arrow function pre-specified values.

const addFive = (x) => x + 5
addFive(10) // 15
const gimmeSeven = addFive.bind(null, 2)
gimmeSeven() // 7
gimmeSeven(10) // still 7

Bind in callback function

Let's say we have a sayDinner() regular function, inside dinner object, using reduce():

let dinner = {
  meals: ['pizza', 'pie', 'tea'],
  sayDinner() {
    let food = this.meals.reduce(function(acc, currentEl){
      if(currentEl === this.meals.slice(-1)[0]){
        return `${acc} ${currentEl}!`
      }
      return `${acc} ${currentEl},`
    }.bind(this), "")
    console.log(food)
  }
}

dinner.sayDinner() // pizza, pie, tea!

(Btw, if you are not familiar with reduce, check this out)

Pay attention at .bind(this) at the end of our reducer function. The bind is necessary to give this.meals context.

Let me explain:

When the callback function is called, it has no idea what this.meals (the one inside reducer function that is being sliced) is. It does not even know that dinner object exists. It only knows acc and currentEl. When we do .bind(this), we are telling the reducer, "Hey, if you see this.meal inside yourself, you can use dinner's meals."

Try the above again without .bind(this)

dinner = {
  meals: ['pizza', 'pie', 'tea'],
  sayDinner() {
    let food = this.meals.reduce(function(acc, currentEl){
      if(currentEl === this.meals.slice(-1)[0]){ // does not know about this.meals if we don't bind it
        return `${acc} ${currentEl}!`
      }
      return `${acc} ${currentEl},`
    }, "")
    console.log(food)
  }
}
dinner.sayDinner() // error

This gives an error "Cannot read property 'slice' of undefined" because this.meals inside our callback function is undefined if not bound.

Retrospectively, let's replace our callback function from regular function into arrow function. It works perfectly without bind:

dinner = {
  meals: ['pizza', 'pie', 'tea'],
  sayDinner() {
    let food = this.meals.reduce((acc, currentEl) => {
      if(currentEl === this.meals.slice(-1)[0]){ 
        return `${acc} ${currentEl}!`
      }
      return `${acc} ${currentEl},`
    }, "")
    console.log(food)
  }
}

dinner.sayDinner() // pizza, pie, tea!

Recall three things about this and arrow function mentioned above.

In this case, our reducer arrow function, seeing this.meals inside itself and does not know what it means, lexically looks up at sayDinner() function. Since sayDinner() is a regular function, it does have this.meals context.

What if we change sayDinner() from regular to arrow function?

Something like this:

dinner = {
  meals: ['pizza', 'pie', 'tea'],
  sayDinner: () => {
    let food = this.meals.reduce((acc, currentEl) => {
      if(currentEl === this.meals.slice(-1)[0]){
        return `${acc} ${currentEl}!`
      }
      return `${acc} ${currentEl},`
    }, "")
    console.log(food)
  }
}

Let's think: we are inside reducer function trying to get this.meals. Recall our three rules above, rule #2: arrow function does not have this on its own, so it will look for regular function or object that has this.meals. The next object it checks is global window object, but it finds none. It throws an error: "Uncaught TypeError: Cannot read property 'reduce' of undefined"

Of course, we can always define a global meals, like

window.meals = ['Hamburger', 'Fries']
const dinner = {...}

dinner.sayDinner() // hamburger, fries

And it would have worked just fine :)

There you have it folks! Javascript bind. Now go bind stuff!!!

More Readings/ Resources

© Copyright 2021 Igor Irianto