Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Of Classes and Arrow Functions: the Context of a Cautionary Tale

DZone's Guide to

Of Classes and Arrow Functions: the Context of a Cautionary Tale

The Arrow Function has driven away the irksome "function" keyword and (by dint of this lexical scoping) bought joy to many a JavaScript programmer, but beware its context...

· Web Dev Zone
Free Resource

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

Yet, as the following account relates, even the best tools should be used with discretion.

A Hasty Refresher

Traditional function expressions create a function whose this value is dynamic and is either the object that calls it, or the global object¹ when there’s no explicit caller. Arrow function expressions, on the other hand, always assume the this value of the surrounding code.

let outerThis, tfeThis, afeThis;
let obj = {
  outer() {
    outerThis = this;

    traditionalFE = function() {tfeThis = this};
    traditionalFE();

    arrowFE = () => afeThis = this;
    arrowFE();
  }
}
obj.outer();

outerThis; // obj
tfeThis; // global
afeThis; // obj
outerThis === afeThis; // true

Arrow Functions and Classes

Given the arrow function’s no-nonsense approach to context, it’s tempting to use it as a substitute for methods in classes. Consider this simple class that suppresses all clicks within a given container and reports the DOM node whose click event was suppressed:

class ClickSuppresser {
  constructor(domNode) {
    this.container = domNode;
    this.initialize();
  }

  suppressClick(e) {
    e.preventDefault();
    e.stopPropagation();
    this.clickSuppressed(e);
  }

  clickSuppressed(e) {
    console.log('click suppressed on', e.target);
  }

  initialize() {
    this.container.addEventListener(
      'click', this.suppressClick.bind(this));
  }
}

This implementation uses ES6 method shorthand syntax. We have to bind the event listener to the current instance (line 18), otherwise the this value in suppressClick would be the container node.

Using arrow functions in place of method syntax eliminates the need to bind the handler:

class ClickSuppresser {
  constructor(domNode) {
    this.container = domNode;
    this.initialize();
  }

  suppressClick = e => {
    e.preventDefault();
    e.stopPropagation();
    this.clickSuppressed(e);
  }

  clickSuppressed = e => {
    console.log('click suppressed on', e.target);
  }

  initialize = () => {
    this.container.addEventListener(
      'click', this.suppressClick);
  }
}

Perfect!

But wait what’s this?

ClickSuppresser.prototype.suppressClick; // undefined
ClickSuppresser.prototype.clickSuppressed; // undefined
ClickSuppresser.prototype.initialize; // undefined

Why weren’t the functions added to the prototype?

It turns out the problem is not so much the arrow function itself, but how it gets there. Arrow functions aren’t methods, they’re anonymous function expressions, so the only way to add them to a class is by assignment to a property. And ES classes handle methods and properties in entirely different ways.

Methods get added to the class’s prototype which is where we want them — it means they’re only defined once, instead of once per instance. By contrast, class property syntax (which at the time of writing is an ES7 candidate proposal²) is just sugar for assigning the same properties to every instance. In effect, class properties work like this:

class ClickSuppresser {
  constructor(domNode) {

    this.suppressClick = e => {...}
    this.clickSuppressed = e => {...}
    this.initialize = e => {...}

    this.node = domNode;
    this.initialize();
  }
}

In other words our example code will redefine all three functions every time a new instance of ClickSuppresser is created.

const cs1 = new ClickSuppresser();
const cs2 = new ClickSuppresser();

cs1.suppressClick === cs2.suppressClick; // false
cs1.clickSuppressed === cs2.clickSuppressed; // false
cs1.initialize === cs2.initialize; // false

At best this is surprising and unintuitive, at worst needlessly inefficient. Either way it defeats the purpose of using a class or a shared prototype.

...In Which (Sweet Irony) Arrow Functions Come to the Rescue

Discouraged by this unexpected turn of events, our hero reverts to standard method syntax. But there’s still the gnarly matter of that bind function. Besides being relatively slow, bind creates an opaque wrapper that’s hard to debug.

Still, no dragon is unslayable. We can replace the bind from our earlier function with an arrow function.

initialize() {
  this.container.addEventListener(
    'click', e => this.suppressClick(e));
}

Why does this work? Since suppressClick is defined using regular method syntax, it will acquire the context of the instance that invoked it (this in the example above). And since arrow functions are lexically scoped, this will be the current instance of our class.

If you don’t want to have to look up the arguments each time, you can take advantage of the rest/spread operator:

initialize() {
  this.container.addEventListener(
    'click', (...args) => this.suppressClick(...args));
}

Wrap Up

I’ve never felt comfortable using arrow functions as stand-ins for class methods. Methods should be dynamically scoped according to the instance that calls them, but an arrow function is by definition statically scoped. As it turns out, the scoping issue is pre-empted by the equally problematic efficiency issue that comes from using properties to describe common functionality. Either way, you should think twice about using an arrow function as part of your class definition.

Moral: Arrow functions are great, but using the right tool for the job is better.

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
function properties ,classes

Published at DZone with permission of Angus Croll. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}