JavaScript arrow functions and context
Arrow functions have been introduced in the JavaScript ES2015 specification of the language, and provide another more concise way of defining functions. I assume they have been inspired by the CoffeeScript transpiler project.
Nowadays, CoffeeScript is not widely used because new features get adopted quite fast into the core language itself, many of which have made it from CoffeeScript into the JavaScript standard already.
Let us first look at functions. A normal function declaration in JavaScript looks something like this:
function add(x, y) { return x+y; }
you can also define functions as an expression:
var add = function(x, y) { return x+y; }
which accomplishes the same thing but by assigning the anonymous function to the variable “add”.
One important aspect to note is that each function has a context tied to it, which you can think of as an environment that the function has access to. The global environment would be the window object, and when you say something like var x = 10;
, it is equivalent to window.x = 10;
.
This is also why using the “var” keyword is important in JavaScript, because even though you can omit it and just say x = 10;
, if you did this inside a function and you wanted x to be a local variable, it would actually become global and tied to the window object as you omitted the var keyword.
The variable that points to the current context is called this. For example, if you open the JavaScript console and evaluate window === this
, you will notice that it evaluates to true.
The problem is that, when nesting functions and objects, you can lose your context. Consider the following code snippet:
class Cat { constructor(name) { this.name = name; this.sound = "meow"; } addSoundHandler() { document.getElementsByTagName('body')[0].addEventListener('mouseenter', function() { console.log(`${this.name} says: ${this.sound}!`); }); } } var cosmo = new Cat("Cosmo"); cosmo.addSoundHandler();
This declares a Cat class, creates an instance of that class and calls its addSoundHandler() method to attach an event handler for the mouse enter event to the document body, that will print out a message with the cat’s name and the sound that the cat makes.
Unfortunately, this will print out undefined says: undefined!
Why?
The issue is that the this object present in both this.name and this.sound refers to a different context – instead of evaluating to the cat object, it evaluates to the context of the callback function, in this case it’s the body element of the page.
We can remedy this by calling bind() on the function. From MDN:
The bind() method creates a new function that, when called, has its
this
keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
In essence, we can use bind() to set the correct context for the function, making sure that the this object in the callback function refers to the instance of the Cat class.
class Cat { constructor(name) { this.name = name; this.sound = "meow"; } addSoundHandler() { document.getElementsByTagName('body')[0].addEventListener('mouseenter', function() { console.log(`${this.name} says: ${this.sound}!`); }.bind(this)); } } var cosmo = new Cat("Cosmo"); cosmo.addSoundHandler();
We will now get Cosmo says: meow!
, as expected.
However, imagine if you had chains of callbacks inside callbacks and had to manually keep track of where each this points?
Where do the arrow functions come in?
Aside from syntactic/visual differences, the arrow function does something else: it preserves the context. In this way, there is no need for binding this to the correct value; it is done automatically.
The arrow function syntax somewhat resembles the ordinary function expression assignment style, and the return is implicit when you don’t include braces { } for functions that only have a single statement, but you must specify it if you do surround the function body in braces:
var add = (x,y) => x+y;
Thus, for Cat, our final code looks like this:
class Cat { constructor(name) { this.name = name; this.sound = "meow"; } addSoundHandler() { document.getElementsByTagName('body')[0].addEventListener('mouseenter', () => { console.log(`${this.name} says: ${this.sound}!`); }); } } var cosmo = new Cat("Cosmo"); cosmo.addSoundHandler();
Notice the lack of bind(), and the code works as expected. Now we can create as many cats as we want with minimum effort!