Skip to content

Instantly share code, notes, and snippets.

@spion
Last active June 17, 2019 21:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save spion/840584f65e96ae4edfd0656aa5fbf03a to your computer and use it in GitHub Desktop.
Save spion/840584f65e96ae4edfd0656aa5fbf03a to your computer and use it in GitHub Desktop.
Understanding JS `this` via bind operator

In JavaScript, functions have arguments. We can name those arguments so that we can access the values we pass to those functions

function add(a, b) {
  return a + b;
}

We can call these functions normally:

add(5, 3) // result: 8

However, JS functions also have a hidden argument that is not listed. This argument's name is this

function addToThis(val) {
  return this + val;
}

When you call a function normally, the this argument is undefined. To assign a value to the this argument, you can use a special operator called the bind operator ::. The argument is placed on the left of the operator, and spaces are not allowed around it:

5::addToThis(3) // result: 8

Objects allow you to group multiple values in one place by giving them labels. Some of these values can be functions

let obj = {
  a: 1,
  f: function(x) { return x + 1 }
}

Functions attached to objects are special. If you simply access their value:

let myFunction = obj.f

you get the function stored in a new value.

However, if you call the function while its attached to the object:

let val = obj.f();

an additional argument this will be passed to the function, which is the object left of the dot. Effectively this is the same as calling the function with the bind operator!

let myFn = obj.f;
let val = obj::myFn();

This only applies to the calling syntax, which means obj.f acts very differently from obj.f()

We can use this to extend records with functionality that acesses their data, creating objects:

let myCounter = {
  a: 1,
  increment() { this.a = this.a + 1; }
  decrement() { this.a = this.a - 1; }
  getValue() { return this.a; }
}

myCounter.increment();
myCounter.increment();
myCounter.decrement()
console.log(myCounter.getValue()); // 2

Specific cases:

DOM Events

When you pass a callback to a dom event e.g.

myButton.addEventListener('click', myFn);

The listener will call the function binding this with the bind operator to the element:

myButton::myFn(eventValue);

You can therefore access the element via the this argument

setTimeout

Once you start using objects, you have to be careful when passing their functions around to other functions. The object will not travel with them.

For example, if you call setTimeout with an object's function:

setTimeout(obj.fn, 1000)

setTimeout receives the function value, but not the obj object. It cannot pass the right this argument via the bind operator, and will therefore call it with this as undefined

function setTimeout(fn, delay) {
  // setTimeout only has "fn", not the object. It cannot pass it as a `this` argument
  afterDelay fn(); // `this` will be `undefined`
}

How can we fix this? (Assuming user understands concept of closures).

We can create a closure where we make a version of the function that remembers the this value by another name, so its not overridden with undefined

  setTimeout(function() { obj.fn() })

Now it no longer matters which this argument was passed to this newly created function, since its ignored - it will call fn with obj as the this argument, regardless.

However, what if we want to send a function from within an object?

let myCounter = {
  a: 1,
  increment() { this.a = this.a + 1; }
  incrementAfterSecond() {
    setTimeout(???, 1000)
  }
}

What can we put in the ??? position? Lets try a regular function

let myCounter = {
  a: 1,
  increment() { this.a = this.a + 1; }
  incrementAfterSecond() {
    setTimeout(function() { this.increment() },  1000)
  }
}

This wont work as the new function also has a this argument which will shadow (over-ride) the original one.

Instead, we need to remember the argument under another name

let myCounter = {
  a: 1,
  increment() { this.a = this.a + 1; }
  incrementAfterSecond() {
    let self = this, increment = this.increment;
    setTimeout(function() { self::increment() },  1000)
  }
}

Alternatively, we can write a small utility function that automatically does this for us:

function bind(obj, fn) {
  let self = obj;
  return function(...args) { return self::fn(args); }
}

and then use it:

let myCounter = {
  a: 1,
  increment() { this.a = this.a + 1; }
  incrementAfterSecond() {
    let obj = this, fn = this.increment;
    setTimeout(bind(obj, fn), 1000);
  }
}

The bind operator can also do this for us. When used like this:

::obj.fn

it will "pre-bind" the function to the object, just like bind(obj, obj.fn)

let myCounter = {
  a: 1,
  increment() { this.a = this.a + 1; }
  incrementAfterSecond() {
    setTimeout(::obj.fn, 1000);
  }
}

This applies to functions that are already attached to the object. But lets say we want to implement incrementTwiceLater, which calls increment twice after one second. We can do it by remembering the self object manually:

let myCounter = {
  a: 1,
  increment() { this.a = this.a + 1; }
  incrementAfterSecond() {
    let self = this;
    setTimeout(function() {
      self.increment();
      self.increment();
    }, 1000);
  }
}

Or as a short-hand we can immediately create an annonimous pre-bound function by using the arrow syntax:

let myCounter = {
  a: 1,
  increment() { this.a = this.a + 1; }
  incrementAfterSecond() {
    setTimeout(() => {
      this.increment();
      this.increment();
    }, 1000);
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment