Arrow Function Semantics

In the last post, we took a deep-dive into the syntax of JavaScript’s new arrow functions. It’s great to see a new language feature that’s able to remove so much boilerplate from an idiomatic ES5 example.

But arrow functions are much more than function expressions with a fresh coat of paint. On top of the new appearance, there are some significant differences in the way they behave too.

This post explores these new behaviours; including why arrow functions don’t have their own values for this or arguments, and what that means for the code you write.

No more this

Before arrow functions, every function call would create a special variable and add it to the scope - this. What this equalled depended on how you called the function. Call it using .call(o), .apply(o) or as a method on o, and this would be equal to o.

var fullName = function(){
	return this.firstName + ' ' + this.lastName;
};

var user = {
	firstName: 'alex',
	lastName: 'the great',
	fullName: fullName
};

// returns 'alex the great'
user.fullName();

// returns 'ivan the terrible'
fullName.apply({
	firstName: 'ivan',
	lastName: 'the terrible'
});

// returns 'keith the nondescript'
fullName.call({
	firstName: 'keith',
	lastName: 'the nondescript'
});

This behaviour becomes an issue when you start using nested functions, causing one of the most famous JavaScript gotchas.

'strict mode';

var CollapseableSection = function(){
	this.state = 'closed';
}

CollapseableSection.prototype.bindToElement = function(el) {
	el.classList.add('hidden');

	el.addEventListener('click', function(){
		if ( this.state === 'closed' ) {
			this.state = 'open';
			el.classList.remove('hidden');
		} else {
			this.state = 'closed';
			el.classList.add('hidden');
		}
	});
}

var section = new CollapseableSection();
var el = document.querySelector('#extra-info');
section.bindToElement(el);

Instead of adding and removing the ‘hidden’ class when the #extra-info element is clicked, this code will result in errors being printed to the console.

In the click-handler function we pass to el.addEventListener, this doesn’t refer to our CollapseableSection instance. Instead, the click-handler creates it’s own value for this, which gets set to undefined. The inner function’s this shadows the this we really want, in the same way that variables defined in inner functions shadow variables of the same name defined in the outer scope.

Lexical this

Because arrow functions don’t create their own this, they get to re-use the this available in the outer scope. That leads to a neat fix to our above example:

'strict mode';

var CollapseableSection = function(){
	this.state = 'closed';
}

CollapseableSection.prototype.bindToElement = function(el) {
	el.addEventListener('click', () => {
		if ( this.state === 'closed' ) {
			this.state = 'open';
			el.classList.remove('hidden');
		} else {
			this.state = 'closed';
			el.classList.add('hidden');
		}
	});
}

Wrong methodology

As with most design choices, arrow functions this-lessness comes with a downside. Since they don’t have their own this, you can’t use them to define methods that need access to the object on which they’re called.

No more arguments

In regular functions, arguments is another special variable created at call time. Its main job is to provide a list of the arguments the function was called with. This is a hugely popular feature - used to create variadic functions, functions that accept an unknown number of arguments.

For instance, as an alternative to writing a sum function that operates on an array of numbers (i.e. sum([1, 2, 3, 4])), you could write the following:

  
var sum = function(){
	var args = [].slice.call(arguments)
			.reduce(function(a, b){ return a + b })
}

sum(1, 2, 3, 4) //=== 10

If you’ve not run into this before, you might be asking yourself “but what’s the [].slice.call(arguments) all about.

The issue is that arguments has never been an array. It’s been an array-like; an object with a .length property and numeric properties. That might not matter if you intend to use a for(;;;) loop, but if you want to leverage the modern array methods, you’ll have to first convert to an array. That’s what the [].slice.call(arguments) trick is for.

In arrow functions, there is no arguments created - just like there’s no this created - when the function is called. Instead, you can use the new spread syntax to express that you want to take a variable number of arguments, and turn them into a real array.

  
var sum = (...numbers) => numbers.reduce((a, b) => a + b)

Much cleaner.

Inferring a .name

When JavaScript engines print stack traces, it’s common for them to look at the .name property of a function to find out what they should print out for the user to see.

But we’ve seen that arrow functions are look anonymous - they don’t seem to have a name, the same way that a plain function expression doesn’t have a name when written in this form.

var fn = function(){ };

fn.name //=== undefined

Of course, in conversation and in code, we think about the function as having the name fn - the name of the variable to which it’s bound. In ES2015, whenever you create an arrow function and immediately assign it to a variable or property, the .name variable gets set.

var fn = () => {};

fn.name //=== 'fn'

Wrapping up

I haven’t actually covered all of the new behaviours of arrow functions here. Since ES2015 is a pretty hefty advance in JavaScript, there are lots of new language features that arrow functions interact with too - the super keyword, tail-call optimisation, generators, and more. But hopefully this will be enough of an overview for you to see the benefits - and drawbacks - of using the new form.