Function binding

When using setTimeout with object methods or passing object methods along, there’s a known problem: "losing this".

Suddenly, this just stops working right. The situation is typical for novice developers, but happens with experienced ones as well.

Losing “this”

We already know that in JavaScript it’s easy to lose this. Once a method is passed somewhere separately from the object – this is lost.

Here’s how it may happen with setTimeout:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(user.sayHi, 1000); // Hello, undefined!

As we can see, the output shows not “John” as this.firstName, but undefined!

That’s because setTimeout got the function user.sayHi, separately from the object. The last line can be rewritten as:

let f = user.sayHi;
setTimeout(f, 1000); // lost user context

The method setTimeout in-browser is a little special: it sets this=window for the function call (for Node.JS, this becomes the timer object, but doesn’t really matter here). So for this.firstName it tries to get window.firstName, which does not exist. In other similar cases as we’ll see, usually this just becomes undefined.

The task is quite typical – we want to pass an object method somewhere else (here – to the scheduler) where it will be called. How to make sure that it will be called in the right context?

Solution 1: a wrapper

The simplest solution is to use an wrapping function:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(function() {
  user.sayHi(); // Hello, John!
}, 1000);

Now it works, because it receives user from the outer lexical environment, and then calls the method normally.

The same, but shorter:

setTimeout(() => user.sayHi(), 1000); // Hello, John!

Looks fine, but a slight vulnerability appears in our code structure.

What if before setTimeout triggers (there’s one second delay!) user changes value? Then, suddenly, it will call the wrong object!

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(() => user.sayHi(), 1000);

// ...within 1 second
user = { sayHi() { alert("Another user in setTimeout!"); } };

// Another user in setTimeout?!?

The next solution guarantees that such thing won’t happen.

Solution 2: bind

Functions provide a built-in method bind that allows to fix this.

The basic syntax is:

// more complex syntax will be little later
let boundFunc = func.bind(context);

The result of func.bind(context) is a special function-like “exotic object”, that is callable as function and transparently passes the call to func setting this=context.

In other words, calling boundFunc is like func with fixed this.

For instance, here funcUser passes a call to func with this=user:

let user = {
  firstName: "John"
};

function func() {
  alert(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // John

Here func.bind(user) as a “bound variant” of func, with fixed this=user.

All arguments are passed to the original func “as is”, for instance:

let user = {
  firstName: "John"
};

function func(phrase) {
  alert(phrase + ', ' + this.firstName);
}

// bind this to user
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user)

Now let’s try with an object method:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

let sayHi = user.sayHi.bind(user); // (*)

sayHi(); // Hello, John!

setTimeout(sayHi, 1000); // Hello, John!

In the line (*) we take the method user.sayHi and bind it to user. The sayHi is a “bound” function, that can be called alone or passed to setTimeout – doesn’t matter, the context will be right.

Here we can see that arguments are passed “as is”, only this is fixed by bind:

let user = {
  firstName: "John",
  say(phrase) {
    alert(`${phrase}, ${this.firstName}!`);
  }
};

let say = user.say.bind(user);

say("Hello"); // Hello, John ("Hello" argument is passed to say)
say("Bye"); // Bye, John ("Bye" is passed to say)
Convenience method: bindAll

If an object has many methods and we plan to actively pass it around, then we could bind them all in a loop:

for (let key in user) {
  if (typeof user[key] == 'function') {
    user[key] = user[key].bind(user);
  }
}

JavaScript libraries also provide functions for convenient mass binding , e.g. _.bindAll(obj) in lodash.

Summary

Method func.bind(context, ...args) returns a “bound variant” of function func that fixes the context this and first arguments if given.

Usually we apply bind to fix this in an object method, so that we can pass it somewhere. For example, to setTimeout. There are more reasons to bind in the modern development, we’ll meet them later.

Tasks

importance: 5

What will be the output?

function f() {
  alert( this ); // ?
}

let user = {
  g: f.bind(null)
};

user.g();

The answer: null.

function f() {
  alert( this ); // null
}

let user = {
  g: f.bind(null)
};

user.g();

The context of a bound function is hard-fixed. There’s just no way to further change it.

So even while we run user.g(), the original function is called with this=null.

importance: 5

Can we change this by additional binding?

What will be the output?

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Ann" } );

f();

The answer: John.

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Pete"} );

f(); // John

The exotic bound function object returned by f.bind(...) remembers the context (and arguments if provided) only at creation time.

A function cannot be re-bound.

importance: 5

There’s a value in the property of a function. Will it change after bind? Why, elaborate?

function sayHi() {
  alert( this.name );
}
sayHi.test = 5;

let bound = sayHi.bind({
  name: "John"
});

alert( bound.test ); // what will be the output? why?

The answer: undefined.

The result of bind is another object. It does not have the test property.

importance: 5

The call to askPassword() in the code below should check the password and then call user.loginOk/loginFail depending on the answer.

But it leads to an error. Why?

Fix the highlighted line for everything to start working right (other lines are not to be changed).

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  loginOk() {
    alert(`${this.name} logged in`);
  },

  loginFail() {
    alert(`${this.name} failed to log in`);
  },

};

askPassword(user.loginOk, user.loginFail);

The error occurs because ask gets functions loginOk/loginFail without the object.

When it calls them, they naturally assume this=undefined.

Let’s bind the context:

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  loginOk() {
    alert(`${this.name} logged in`);
  },

  loginFail() {
    alert(`${this.name} failed to log in`);
  },

};

askPassword(user.loginOk.bind(user), user.loginFail.bind(user));

Now it works.

An alternative solution could be:

//...
askPassword(() => user.loginOk(), () => user.loginFail());

Usually that also works, but may fail in more complex situations where user has a chance of being overwritten between the moments of asking and running () => user.loginOk().

Tutorial map

Comments

read this before commenting…
  • You're welcome to post additions, questions to the articles and answers to them.
  • To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
  • If you can't understand something in the article – please elaborate.