What are ES6 arrow functions, how do they work? [d

2019-01-29 14:09发布

问题:

This question already has an answer here:

  • Arrow function vs function declaration / expressions: Are they equivalent / exchangeable? 3 answers

I am curious about ES6 arrow functions (fat arrow functions). Are they simply syntactic sugar derived from CoffeeScript, or is there more to them than meets the eye?

回答1:

ES6 Arrow functions in depth

One of the prettiest features of ES6, it could easily win a beauty contest, if such a contest would be held. What many people don’t know is that the arrow function is not simply a form of syntactic sugar that we can use instead of the regular callback. As I like to explain it to the people who attend my trainings/workshops, arrow functions are this-less, arguments-less, new.target-less and super-less. Let us now get past the shorter syntax and dive deeper into the specifics of the arrow function.

Lexical-bound this

Previously, regular functions would have their this value set to the global object if they were used as callbacks, to a new object in case they were called with the new operator or, in the case of libraries like jQuery, they would be set to the object that triggered an event in case of event handlers, or the current element in a $.each iteration.This situation proved very confusing even for experienced developers. Let’s say you have a piece of code like the one below.

var obj = {
  nameValue: 'default',
  initializeHandlers: function() {
    var nameInput = document.querySelector('#name');

    nameInput.addEventListener('blur', function(event) {
      this.nameValue = event.target.value;
    });
  }
};

obj.initializeHandlers();

The problem is that this inside the blur event handler is set to the global object rather than obj. In strict mode — ‘use strict’; — you risk breaking your application because this is set to undefined. In order to side-step this issue we have two options:

  • Convert the event handler to a function bound to the outer scope, using Function.prototype.bind
  • Use the dirty var self = this; expression in the initializeHandlers function (I see this as a hack)

Both options are illustrated below.

[...]
initializeHandlers: function() {
  var nameInput = document.querySelector('#name');
  // more elegant but we can do better
  var blurHandler = function(event) {
    this.nameValue = event.target.value;
  }.bind(this)

  nameInput.addEventListener('blur', blurHandler);
}
[...]

[...]
initializeHandlers: function() {
  var nameInput = document.querySelector('#name');
  // ugly and error-prone
  var self = this;

  nameInput.addEventListener('blur', function(event) {
    self.nameValue = event.target.value;
  });
}
[...]

On the other hand, arrow functions have no internal context. They inherit their context from the outer scope. Let’s take a look at how arrow functions solve this problem.

const obj = {
  nameValue: 'default',
  initializeHandlers: function() {
    const nameInput = document.querySelector('#name');

    nameInput.addEventListener('blur', (event) => {
      // this references obj instead of the global object
      this.nameValue = event.target.value;
    });
  }
};

In our new implementation this is a hard reference to the obj object and doesn’t get lost due to nesting.

Lexical arguments

Have you ever tried to access the arguments object inside an arrow function? I have, and I wasted 3 solid hours trying to figure out why do I get the arguments of the outer function instead of those of the arrow functions. Thankfully, MDN exists, and as good practice dictates, you check the documentation at the end, when you sit in a corner, knees tucked to your chest, rocking and repeating to yourself: “I should have been a carpenter!” Fun aside, arrow functions do not expose an arguments object. If you try to access it, you will get the arguments of the surrounding function. In our case, given the fact that the outer function is an arrow function as well, and we have no more functions further up the chain, we will get a ReferenceError.

const variadicAdder = (x) => {

  return () => {
    let args = Array.prototype.slice.call(arguments, 0);
    return args.reduce((accumulator, current) => {
      return accumulator + current;
    }, x);
  }
}

const variadicAdderOf5 = variadicAdder(5);

console.log(variadicAdderOf5(10, 11, 12));
// ReferenceError: arguments is not defined

There is no fix here, as there is nothing broken. What we can do is to return a plain function, rather than an arrow, from our variadicAdder(). This will give us the opportunity to access the arguments object without an issue. The updated code will look like the one below with the only difference that it will actually work and not throw an error.

const variadicAdder = (x) => {

  return function() {
    let args = Array.prototype.slice.call(arguments, 0);
    return args.reduce((accumulator, current) => {
      return accumulator + current;
    }, x);
  }
}

const variadicAdderOf5 = variadicAdder(5);

console.log(variadicAdderOf5(10, 11, 12));
// 38

To find out more about Array.prototype.reduce, head to the Mozilla Developer Network.

Other characteristics

As I mentioned in the introductory section of this article, arrow functions have several more characteristics besides the context and the arguments. The first thing I would like to mention is that you are unable to use the new operator with arrow functions. As a direct implication, arrow functions also don’t have super(). Snippets like the one below would simply throw a TypeError.

const Person = (name) => {
  this.name = name;
};

let p = new Person('John');
// TypeError: Person is not a constructor

The third characteristic, which is as well, a direct implication of the inability to use the new operator, is the fact that arrow functions don’t have new.target. In a nutshell, new.target allows you to detect whether or not a function has been called as a constructor. Arrow functions, inherit new.target from their surrounding scope. If the outer scope is a function, and it is called like a constructor (e.g. new Person('Adrian');), then new.target will point to the outer function. The Mozilla Developer Network hosts a detailed explanation on new.target and I encourage you to check it out.

This article is also published on my blog, here: /es6-arrow-functions-in-depth/