Skip to content

Instantly share code, notes, and snippets.

@visar
Forked from spion/understanding-js-this.md
Last active August 29, 2015 14:13
Show Gist options
  • Save visar/30aefe752b7fb7bbd3c8 to your computer and use it in GitHub Desktop.
Save visar/30aefe752b7fb7bbd3c8 to your computer and use it in GitHub Desktop.

Understanding this in JavaScript

It's easy to trip up on the meaning of this in JavaScript. The behavior is very different from other languages, which means we have to throw most preconceptions and intuition out the window.

The best way to think of this in JS is as a hidden argument which is passed in a slightly awkward way. Instead of the normal passing of arguments:

fn(arg1, arg2, arg3)

this is passed as whatever is left of the dot when the function is called.

leftOfTheDot.fn(arg1, arg2) 

In this last example this will become leftOfTheDot inside the body of the function.

But what if nothing is left of the dot?

fn(arg1, arg2);

Then this refers to the global object inside the body of the function (global in node, window in browsers). (undefined in strict mode)

Understanding via desugaring.

Every function has its own method named call which also allows you to specify the value of the this argument. When call is used, the first argument becomes this, the 2nd becomes the 1st argument and so on i.e. fn.call(thisArg, firstArg, secondArg, ...).

This means we can translate (desugar) all of the above examples to the call method:

leftOfTheDot.fn(arg1, arg2)

desugars to the following call version:

leftOfTheDot.fn.call(leftOfTeDot, arg1, arg2)

On the other hand, the next example

fn(arg1, arg2);

desugars to

  1. fn.call(window, arg1, arg2) - in browsers
  2. fn.call(global, arg1, arg2) - in node
  3. fn.call(undefined, arg1, arg2) - everywhere when in strict mode ("use strict")

Some extra examples

Consider this example:

var o1 = {identify: 'o1'},
    o2 = {identify: 'o2'}; 

o1.fn = function() { 
  return "I am " + this.identify; 
}; 
o2.fn = o1.fn; 

var detached = o2.fn;   

console.log(o1.fn() + " and " + o2.fn() + " and " + detached());

// Output:
// I am o1 and I am o2 and I am undefined

If we desugar the calls in the console.log line, everything becomes clear:

console.log(o1.fn.call(o1), o2.fn.call(o2), detached.call(undefined))

This example shows that it doesn't matter where the function is attached to when it's passed around. The only place where it matters is when it's being called. Therefore, at the call sites (inside console.log), this = whatever is left of the dot, even though the function was originally attached to o1. Its the exact same function, called with a different this.

Callbacks

What happens with callbacks? Say we have the following

fs.readFile(this.file, 'utf8', function callback(err, data) {
  this.data = data;
});

On the receiving end, node gets our callback as an argument and then calls it directly:

function readFile(file, opt, callback) {
  // do the work
  callback(null, result); // if no errors found.
}

This is the call site of the callback. There is nothing on the left of the dot when node calls that callback. Which means, this is the global object.

If we desugar that, we get callback.call(global, null, result). Its now obvious why readFile doesn't have access to this. It was never passed to it as an argument, so it can't even use callback.call to pass it back to the callback function.

How do I access this from callbacks then?

We have a couple of options:

  1. Use a temporary variable, creating a closure:

    var self = this;
    fs.readFile(this.file, 'utf8', function callback(err, res) {
      self.data = data;
    });
  2. Bind the callback function

    fs.readFile(this.file, 'utf8', bind(function callback(err, res) {
      this.data = data;
    }, this));

    Bind is a function that creates a new function where the this argument is fixed to a certain value. Here is one possible implementation of bind:

    function bind(fn, object) { 
      return function bound() { 
        return fn.apply(object, arguments); 
      } 
    }

    note 1: fn.apply(object) calls fn as if it were attached to the object. Basically

    fn.apply(object, [arg1, arg2, arg3])

    desugars to

    fn.call(object, arg1, arg2, arg3);

    note 2: arguments is an array-like hidden variable which refers to all the arguments in the current function - in this case, the returned bound function. For example, if we call the bound function with 3 strings:

    bound('s1', 's2', 's3');

    then inside bound(), arguments is an array-like object which has

    {
      length: 3,
      0: 's1',
      1: 's2',
      2: 's3'
    }

Why on earth does JS behave like this?

To enable prototypes to work their magic. Here is an example:

var o = function(id) { 
  this.identity = id; 
}; 
o.prototype.fn = function() { 
  return "I am " + this.identity; 
}; 

var o1 = new o('o1'), o2 = new o('o2'); 

console.log(o1.fn() + " and " + o2.fn());

// Output: I am o1 and I am o2

When we construct objects with new o(...), a hidden property is attached to them called __proto__ and it's made to point to o.prototype. In this example, both o1 and o2 have __proto__ properties which point to o.prototype.

Then when we try to access a property of an object it it will first be looked up inside the object, then inside that object's prototype. For example, trying to access o1.fn will result with the following lookup:

  • check inside o1 (not found)
  • check inside o1.__proto__, which is a reference to o.prototype (found)
  • use o.prototype.fn

However we don't want to call the function as if it were attached to the object o.prototype. We want it to be called as if it were attached to the object o1. JavaScript takes whatever is left of the dot at the call site, the place where we actually call the function. That is why this behaves as an additional argument, passed when calling the function, with a slightly awkward syntax - being passed left of the dot.

Lets apply that logic for both objects:

  • In o1.fn(), the function o.prototype.fn is called with o1 left of the dot

  • meaning, o1.prototype.fn is called with the argument this set to o1

  • therefore, this.identity == 'o1', and the return result is 'I am o1'

  • In o2.fn(), the function o.prototype.fn is called with o2 left of the dot

  • meaning, o1.prototype.fn is called with the argument this set to o2

  • therefore, this.identity == 'o2', and the return result is 'I am o2'

Magic overload?

Indeed. But ES6 will improve things a lot:

  1. We're going to get classes, so we wont have to muck around with attaching things to the prototype object and creating prototype chains that emulate classical inheritance ourselves, manually.

    Don't worry, prototypes will still be at work under the hood. Most of this is just sugar.

  2. We'll be able to use a shortcut to write bound functions:

    fs.readFile(this.file, 'utf8', (err, res) => { 
      this.data = res; 
    });
  3. Instead of the magical arguments variable, we'll get "rest" arguments - a way to expess that we want the rest of the function arguments to be represented by an array:

    function bind(fn, object) { 
      return function bound(...args) { 
        return fn.apply(object, args); 
      } 
    }

Can't wait!

The far future

In ES7, if we get the bind operator, we'll also be able to bind any function to any object:

object::fn

get a bound version of the function attached to the object

::object.fn

and call any function on any object with a short and sweet syntax:

var {lmap, lreduce} = require('lodash');
array::lmap(...)::lreduce(...);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment