Modermo
Modermo

Reputation: 1992

Confused about when to use `bind` in an Event Handler

The following successfully prints 'foo'.

var obj = {
    name: 'foo',
    printName: function printName() {
      console.log(this.name);
    }
  };

var printButton= document.getElementById('printIt');

printButton.addEventListener('click', function(){
  obj.printName(); 
});

The following doesn't, however:

printButton.addEventListener('click', obj.printName() );

I know the solution... simply use bind so that we're referencing the obj object. i.e:

printButton.addEventListener('click', obj.printName.bind(obj) );

Why then don't we need to use bind in the first example. I don't know why wrapping obj.printName() function call in the anonymous function results in the console.log correctly referencing and printing this properly, but when called directly after click, you needs to use bind

Upvotes: 1

Views: 92

Answers (2)

Rico Kahler
Rico Kahler

Reputation: 19222

Alright, I commented with some good information on this question so I might as well answer!

Functions are first class

Okay, let's starts with some fundamentals of javascript that is very dissimilar to some other programming languages: in javascript functions are first class citizens--which is just a fancy way of saying that you can save functions into variables and you can pass functions into other functions.

const myFunction = function () { return 'whoa a function'; }
array.map(function () { return x + 1; });

And because of this wonderful feature, there is a big difference between the expressions:

Expression 1

obj.printName

and

Expression 2

obj.printName();

In expression 1: the function isn't being invoked so the value of the expression is of type function

In expression 2: the function is being invoked so the value of the expression is what the function returns. In your case, that's undefined


addEventListener

The method addEventListener takes in two arguments:

  1. a string of the type of event
  2. a function that will be run when the event fires.

Alight, so what does that mean?

When you call

// doesn't work
printButton.addEventListener('click', obj.printName() );

you're not passing a value of type function to the addEventListener method, you're actually passing undefined.

// works
printButton.addEventListener('click', obj.printName.bind(obj) );

then works (for one reason) because the second argument is actually of type function.


What does bind do? Why does it return a function?

Now we need to discuss what bind actually does. It related to the pointer* this.

*by pointer, I mean a reference identifier to some object

bind is a method that exists on every function object that simply binds the this pointer of a desired object to the function

This is best shown by an example:

Say you have a class Fruit that has a method printName. Now that we know that you can save a method into a variable, let's try that. In the example below we're assigning two things:

  1. boundMethod which used bind
  2. unboundMethod that didn't use bind

class Fruit {
  constructor() {
    this.name = 'apple';
  }
  
  printName() {
    console.log(this.name);
  }
}

const myFruit = new Fruit();

// take the method `printName`
const boundMethod = myFruit.printName.bind(myFruit);
const unboundMethod = myFruit.printName;

boundMethod(); // works
unboundMethod(); // doesn't work

So what happens when you don't call bind? Why doesn't that work?

If you don't call bind in this case, the value of the function that gets stored into the identifier unboundMethod can be thought to be:

// doens't work
const unboundMethod = function() {
    console.log(this.name);
}

where the contents of the function is the same contents of the method printName from the Fruit class. Do you see why this is an issue?

Because the this pointer is still there but the object it was intended to refer to is no longer in scope. When you try to invoke the unboundMethod, you'll get an error because it couldn't find name in this.

So what happens when you do use bind?

Loosely bind can be thought of as replacing the this value of function with the object you're passing into bind.

So if I assign: myFruit.printName.bind(myFruit) to boundMethod then you can think of the assignment like this:

// works
const boundMethod = function() {
    console.log(myFruit.name);
}

where this is replaced with myFruit

The bottom-line/TL;DR

when to use bind in an Event Handler

You need to use Function.prototype.bind when you want to replace the thises inside the function with another object/pointer. If your function doesn't ever use this, then you don't need to use bind.

Why then don't we need to use bind in the first example?

If you don't need to "take the method" (i.e. taking the value of type of function), then you don't need to use bind either Another way to word that is: if you invoke the method directly from the object, you don't need bind that same object.

In the wrapper function, you're directly invoking the method of the object (as in expression 2). Because you're invoking the method without "taking the method" (we "took" the methods into variables in the Fruit example), you don't need to use bind.

printButton.addEventListener('click', function(){
  // directly invoke the function
  // no method "taking" here
  obj.printName();
});

Hope this helps :D

Upvotes: 1

Pankaj Shukla
Pankaj Shukla

Reputation: 2672

Note: You need to call printButton.addEventListener('click', obj.printName() ); without parenthesis in obj.printName() since you want to pass the function.

The answer lies in the way this is bound in Javascript. In JS, the way a function is called decides how this is bound. So when you provide the callback function like below:

printButton.addEventListener('click', function(){
  obj.printName(); 
});

Notice, printName is being called via dot notation. This is called implicit binding rule when this is bound to an object before dot, in this case obj. Clearly in this case, you get the expected output.

However, when you call it like this:

printButton.addEventListener('click', obj.printName );

Notice that, all you are passing is the address of the function that is inside obj. So in this case info about obj is lost. In other words, the code that calls back the function doesn't have the info about obj that could have been used to set this. All it has is the address of the function to call.

Hope this helps!

EDIT: Look at this crude implementation I call bind2 that mimics native bind. This is just to illustrate how native bind function returns a new function.

Function.prototype.bind2 = function (context) {
        var callBackFunction = this;//Store the function to call later
        return function () {        //return a new function
            callBackFunction.call(context);//Later when called, apply 
                                           //context, this is `obj` passed 
                                           //in bind2()
        }
    };

    function hello() {
        alert(this.name);
    }
    obj = {
        name:'ABC'
    };
    var f = hello.bind2(obj);


    f();

Notice: How function f() is hard bound here. f() has hard bound this with obj. You cannot change this to other than obj now. This is another thing with bind that probably will help you knowing.

Upvotes: 0

Related Questions