Skip to content

Instantly share code, notes, and snippets.

@spion
Last active May 6, 2022 02:01
Show Gist options
  • Save spion/7180482 to your computer and use it in GitHub Desktop.
Save spion/7180482 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 function 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)

But what if you don't call the function, only access it as a property?

leftOfTheDot.fn

Now that is different! Instead of passing a this argument, not calling the function is just normal record access. There is very big difference between obj.fn and obj.fn() - the first one is record access, the second one is calling obj.fn with the arg this being set to obj

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?

Use an arrow function:

var self = this;
fs.readFile(this.file, 'utf8', (err, res) => {
  self.data = data;
});

You could think of it as a this "binding" operation:

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'

The future?

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