A lot of people come to JavaScript from languages like Java. And, at first glance, the languages seem kind of similar – they even have similar names! Of course, it soon becomes apparent that there are some pretty important differences. For example, Java is object-oriented, statically typed and lexically scoped. JavaScript isn’t these things (except sometimes, when it is). And one of the places that these differences are really obvious is in the use of the keyword this.

the adventures of this and the global scope

One of the big differences between JavaScript this and this in Java (and I assume other, more sane languages), is that in JavaScript you can use this outside of a class.

this.foo = 42;

is totally legal anywhere in JavaScript. What does this refer to in this case? Well, it depends.

In non-strict (or sloppy) mode, a global this refers to the window object. (If you’re not familiar with strict/non-strict mode, see Strict mode on MDN.) Your browser console runs in non-strict mode by default:

>> this
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}

In strict mode, however, things are a bit different – this is undefined in the global scope:

>> (function () { 'use strict'; console.log(this) })()
undefined
undefined

(I’m using an immediately invoked function expression here so I can force strict mode. If you’re not familiar with them, see IIFE on MDN. Why does the console print undefined twice? The first is our console.log. The second is the return value of the IIFE.)

ES6 modules run in strict mode by default, so if you’re writing modern code, you’re probably in strict mode, but it’s not always obvious.

Moral of the story? As we all learned from the events of Jurassic Park, just because you can do a thing doesn’t mean that you should do that thing, so

Don’t use this in a global scope

this, like, literally

A super-handy feature of the JavaScript is the ability to create object literals. As you might expect, if you use this within an object, it refers to the object itself.

const velociraptor = {
  location: 'raptor_enclosure',
  hasEscaped: () => this.location !== 'raptor_enclosure'
}

Great. We know whether or not our raptor has escaped.

The one exception to this is if we use this in our initial definition.

const files = {
 directory: 'src/app',
 htmlFiles: `${this.directory}/**/*.html`,
 testFiles: `${this.directory}/**/*.spec.ts`
}

Logging files.htmlFiles will give us undefined/**/*.html, which is almost certainly not what we wanted.

Why does this happen? Well, at the point we’re using this, we haven’t actually finished creating the object yet, so there’s nothing for this to refer to.

So, using this inside object literals:

  • inside a function is fine 🎉
  • to define a property, doesn’t work 😿

lexical scope & dynamic scope

This is where the fun really begins with this in JavaScript. In most (sensible) languages, this is lexically scoped. That means that this refers to the thing that it looks like it refers to when you write the code. It just works the way you would expect.

class SomeJavaClass {
  boolean worksAsExpected;

  SomeJavaClass() {
    this.worksAsExpected = true;  // this refers to the current instance of this class, as expected
  }
}

JavaScript doesn’t work that way. In JavaScript, what this refers to depends entirely on where it’s called from.

const object1 = {
  expected: true,
  worksAsExpected: function () { return this.expected; },
};

const object2 = {
  expected: false,
  worksAsExpected: object1.worksAsExpected,
};

object1.worksAsExpected(); // true
object2.worksAsExpected(); // false

Even though the worksAsExpected function is declared as part of object1, when we use it in object2, this refers to object2. It might help to remember that functions in JavaScript are objects in their own right. When you declare a function inside an object or class, you’re really creating a Function object and giving your object a reference to that Function. Another object can also get a reference to that Function. Neither object “owns” the function, they just both have references to it.

This is going to come back to bite you in situations where you need to pass a callback, and you pass a callback that belongs to an object. Common cases are setTimeout, Array.prototype.forEach/some/every/find/include, and Promise callbacks. So how do you deal with it? There are two options.

The “better” way is to use Function.prototype.bind. The first argument of bind specifies the object that you want this to refer to when the function is called. (You can also specify other arguments to bind to the parameters of the function.) bind returns a new function, which has this bound to the specified object.

const object1 = {
  expected: true,
  worksAsExpected: function () { return this.expected; },
};

const object2 = {
  expected: false,
  worksAsExpected: object1.worksAsExpected.bind(object1),
};

object1.worksAsExpected(); // true
object2.worksAsExpected(); // true

Binding via Function.prototype.bind is (almost) the strongest kind of binding there is. You can’t override it by calling the function in a different location. (You can sort of override it by calling bind again, but when you do that, you’re really just getting another function with a different this). You also can’t override it using Function.prototype.call or Function.prototype.apply. If you’re not familiar with call and apply, they work similar to bind. The difference is that bind returns a new function that you can call later. call and apply actually call the function immediately (hence the name, I suppose). As such, they’re a bit less useful than bind.

The other way to work around dynamic binding is to use arrow functions. Arrow functions, unlike the rest of JavaScript, use lexical binding.

const object1 = {
  expected: true,
  worksAsExpected: () => this.expected,
};

const object2 = {
  expected: false,
  worksAsExpected: object1.worksAsExpected,
};

object1.worksAsExpected(); // true
object2.worksAsExpected(); // true

Arrow functions are handy when you want an anonymous function that is lexically scoped. I generally prefer arrow functions when I’m passing a callback inline, and bind when I’m passing in a named function. (bind also has the nice effect of making it explicit what you’re doing. The fact that arrow functions are lexically scoped is easy to forget and can lead to unexpected bugs).

this.amount = 4;
fetch(priceUrl)
  .then(response => response.json())
  .then(price => this.total = price * this.amount)

***************************

setTimeout(this.onTimeout.bind(this), delay);

There’s also no way to override the lexical binding of an arrow function – JavaScript won’t let you call bind on them.

why would JavaScript even do that?

At this point, I’m guessing you’re probably wondering why have dynamic scope at all? Wouldn’t it just be easier to have lexical scope like a normal language? Well, yes and no. There are common tasks in JavaScript that are made easier by being able to use dynamic scope. For example, event handlers! The code below could be used for adding an animation to a button when it’s clicked (like the Material Design ink ripple), and it’s super handy to be able to refer to the target element using this. Dynamic scoping means I can use this same handler across multiple buttons.

const button = document.querySelector('button');
button.addEventListener('click', function (event) {
  this.classList.add('ripple');
  this.addEventListener('transitionend', () => this.classList.remove('ripple');
});

The thing is, when you’re writing code in a framework, you’re generally not touching the DOM like this. So you’re not really seeing the advantages of dynamic binding. But trust me – other people are!

conclusion

So, the things to remember about this in JavaScript:

  • Don’t use this in the global scope. Just don’t.
  • When defining a property in an object literal,  this refers to the parent scope. (In a function, this refers to the object)
  • this is dynamically scoped, but you can bind this within a function using Function.prototype.bind/call/apply. bind is the strongest binding and can’t be overridden
  • this is lexically scoped within arrow functions

bonus javascript weirdness

Haven’t had enough? Well, there is one other really weird thing JavaScript does with this, which is almost useless to know about. So let me tell you all about it! Remember before when I said bind was (almost) the strongest way to set what this refers to in a function? There is one way that is stronger – constructor this!

Unlike more object-oriented languages, JavaScript doesn’t really have the concept of a constructor function. (Yes, it exists in ES6 classes, but those are just syntactic sugar). Instead, you can call any function with the new operator, and it magically becomes a constructor.

When you call a function with the new operator, a brand new object gets created and this inside the function now refers to that object. This new object is returned by the function.

function printDino() {
  console.log(this.dino);
}

const boundPrintDino = printDino.bind({ dino: 'raptor' });
console.log(boundPrintDino()); // raptor

console.log(new boundPrintDino()); // undefined

If you run this code in a console, you’ll also get a line that looks something like

>> printDino {}__proto__: Object

This is the new object returned by new printDino.

You can override this behaviour by returning an object from your constructor. If you do that, then this inside your constructor is still going to refer to the new object created when the constructor was called. But you now have no way of accessing that object.

class MySillyClass {
    constructor() {
        this.foo = 42;
        return { bar: 88 };
    }
}

const mySillyObject = new MySillyClass();
console.log(mySillyObject.foo); // undefined
console.log(mySillyObject.bar); // 88

Is this helpful? Not really. It’s just kind of a holdover from the pre-classy days of JavaScript.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s