Arrow Functions in Class Properties Might Not Be As Great As We Think

Picture of the author
Nicolas CharpentierNovember 16, 2017
8 min read
Arrow Functions in Class Properties Might Not Be As Great As We Think

Disclaimer: This article was written in 2017 and may not be representative of the current state of technologies.

Since the last year, the Class Properties Proposal simplify our life, especially in React with the internal state , or even with statics ones like propTypes and defaultProps.

Comparison between without (left) and with (right) class properties

Furthermore, Class Properties/Property Initializers seems to be more trending in the last six months to handle binding in React instead of a bind call in the constructor.

https://twitter.com/housecor/status/921841848641097728

class ComponentA extends Component {
  handleClick = () => {
    // ...
  };

  render() {
    // ...
  }
}

Arrow functions in class field properties seem useful because they’re autobind, in short, no need to add this.handleClick = this.handleClick.bind(this) in the constructor.

But, should we really use arrow functions in class field properties?

First of all, let’s see what class properties do under the hood.

What Class Properties look like once transpiled to ES2017

Let’s code a simple class with a static property, an instance one, an arrow function in property, and a usual function as method with the plugin transform-class-properties.

class A {
  static color = 'red';
  counter = 0;

  handleClick = () => {
    this.counter++;
  };

  handleLongClick() {
    this.counter++;
  }
}

Once we go to the Babel REPL to get this class transpiled to ES2017 with the following presets: es2017 and stage-2.

We got this transpiled version:

class A {
  constructor() {
    this.counter = 0;

    this.handleClick = () => {
      this.counter++;
    };
  }

  handleLongClick() {
    this.counter++;
  }
}
A.color = 'red';

As we can see, instance properties have been moved inside the constructor and the static one moved to an afterward declaration.

Personally, I like the addition of static keyword as we can now directly export a class with static properties.

On instance properties, it’s great too, because in our code (not transpiled one) the constructor will be less bloated with declarations.

For our arrow function in a property, handleClick got moved in the constructor too, such as an instance property.

For our usual function method handleLongClick, nothing change.

Property initializers may be useful for properties, but when it comes to arrow functions in class properties, it feel like a hackish way of achieving a binding.

Did you spot some issues? Let’s see what I found.


Mockability

If you want to mock or spy on a class method, the easiest and proper way to do so is with the prototype as all changes to the Object prototype object are seen by all objects through prototype chaining.

Let’s say we want to do some tests with our previous class A.

class A {
  static color = 'red';
  counter = 0;

  handleClick = () => {
    this.counter++;
  };

  handleLongClick() {
    this.counter++;
  }
}

A.prototype.handleLongClick is defined.

A.prototype.handleClick is not a function.

Oops, since we used an arrow function in a class property our function handleClick is only defined on the initialization by the constructor and not in the prototype. So, even if we mock our function in the instantiated object, the changes won’t be seen by other objects through prototype chaining.

Inheritance

Let’s define our base class A.

class A {
  handleClick = () => {
    console.log('A.handleClick');
  };

  handleLongClick() {
    console.log('A.handleLongClick');
  }
}

console.log(A.prototype);
// {constructor: ƒ, handleLongClick: ƒ}

new A().handleClick();
// A.handleClick

new A().handleLongClick();
// A.handleLongClick

If class B inherit from class A ,handleClick won’t be in the prototype and we can’t call super.handleClick from our arrow function handleClick.

class B extends A {
  handleClick = () => {
    super.handleClick();

    console.log('B.handleClick');
  };

  handleLongClick() {
    super.handleLongClick();

    console.log('B.handleLongClick');
  }
}

console.log(B.prototype);
// A {constructor: ƒ, handleLongClick: ƒ}

console.log(B.prototype.__proto__);
// {constructor: ƒ, handleLongClick: ƒ}

new B().handleClick();
// Uncaught TypeError: (intermediate value).handleClick is not a function

new B().handleLongClick();
// A.handleLongClick
// B.handleLongClick

If class C inherit of class A, but implement handleClick as a function instead of an arrow function, handleClick will only executes super.handleClick() and nothing else. Strange isn’t?

It’s because the instantiation of handleClick in the constructor of our parent class overrides it.

C.prototype.handleClick() will call our implementation but will fail with the previous error: Uncaught TypeError: (intermediate value).handleClick is not a function.

class C extends A {
  handleClick() {
    super.handleClick();

    console.log('C.handleClick');
  }
}

console.log(C.prototype);
// A {constructor: ƒ, handleClick: ƒ}

console.log(C.prototype.__proto__);
// {constructor: ƒ, handleLongClick: ƒ}

new C().handleClick();
// A.handleClick

If class D is a plain class that inherit from class A, he will have an empty prototype and new D().handleClick() will log A.handleClick.

class D extends A {}

console.log(D.prototype);
// A {constructor: ƒ}

console.log(D.prototype.__proto__);
// {constructor: ƒ, handleLongClick: ƒ}

new D().handleClick();
// A.handleClick

Performance

Now the interesting part, let’s talk about performance.

We know that usual functions are defined in the prototype and will be shared across all instances. If we have a list of N components, these components will share the same method. So, if our components get clicked we still call our method N times, but it will call the same prototype. As we’re calling the same method multiple times across the prototype, the JavaScript engine can optimize it.

On the other hand, for the arrow functions in class properties, if we’re creating N components, these N components will also create N functions. Remember what we’ve seen in the transpiled version, class properties are initialized in the constructor. Which means if we click on N components, N different functions will be called.

Let’s see how they are doing in a benchmark with V8 engine (Chrome).

The first one is simple, we only measure the instantiation time, and we call our method one time.

Beware that number doesn’t really matter in this one, since one instantiation won’t be noticed in your application and we’re talking about operations per second and the number are high enough. I’m more concerned by the gap between our functions.

Ops/s of initialization and call (higher is better)

For the second one, I used a representative use case. The instantiation of 100 components — like a list — which after we called the method one time on each.

Ops/s of instantiation of 100 components and method call (higher is better)

All benchmarks were run on a MacBook Pro 13" 2016 2GHz on Mac OS X 10.13.1 and Chrome 62.0.3202.

In short, to improve performance, you should declare your shared method in the prototype and only bound it to the context if you need to (if you pass it as prop or callback). It makes sense to bound our shared methods to the prototype and initialized our properties in the constructor of each instance, but methods not much.

We’re still talking about high ops/s, but we clearly see that arrow functions in class properties are not as performant as we thought.

And yes you’re right, the usage of property initializers won’t be noticed in our applications unless we instantiate many components. Yes we can see this as a premature optimization and you’re right premature optimizations are the root of all evil — with E Corp — but instead, can we see arrow functions in class properties as premature feature or misused feature? Hopefully, engines will optimizes the arrow functions in class properties.

P.S: Class properties for properties are such a great improvement!

I’ve seen this in many application and libraries, even ones that contain components we can instantiate multiple times. Should we really use this ESnext feature for class methods in our packages knowing that can impact — for the moment — our performance?

Personally, I don’t think that arrow functions in class properties are convenient enough — in some cases — at the expense of the performance.


Our savior will be the autobind-decorator, unfortunately it’s only available with babel as it’s still a proposal at stage 2.

class Component {
  constructor(value) {
    this.value = value;
  }

  @autobind
  method() {
    return this.value;
  }
}

Even this, they also recommend to avoid autobind on all methods or class:

It is unnecessary to do that to every function. This is just as bad as autobinding (on a class). You only need to bind functions that you pass around. e.g. onClick=. Or fetch.then(this.hanldeDone) — Dan Abramov‏

I was the guy who came up with autobinding in older Reacts and I’m glad to see it gone. It might save you a few keystrokes but it allocates functions that’ll never be called in 90% of cases and has noticeable performance degradation. Getting rid of autobinding is a good thing — Peter Hunt

Conclusion

The initialization of arrow functions in class properties are transpiled into the constructor.

Arrow functions in class properties won’t be in the prototype and we can’t call them with super.

Arrow functions in class properties are much slower than bound functions, and both are much slower than usual function.

You should only bind with .bind() or arrow function a method if you’re going to pass it around.

Knowing how arrow functions in class properties are handled under the hood and with these benchmarks, you should be able to make an informed choice and aware of the possible consequences.

What do you think? Will you still use it? How will this affect you?

Feel free to share your thoughts.