DEV Community

Igor Irianto
Igor Irianto

Posted on • Edited on • Originally published at irian.to

Javascript bind 101

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let's bind it with sushi:

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

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

lunchie('miso soup') // miso soup and sushi
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And it would have worked just fine :)

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

More Readings/ Resources

Top comments (0)