13. 箭头函数
目录
请支持本书:购买 (PDF, EPUB, MOBI)捐赠
(广告,请不要屏蔽。)

13. 箭头函数



13.1 概述

箭头函数有两个优点。

首先,它们比传统的函数表达式更简洁

const arr = [1, 2, 3];
const squares = arr.map(x => x * x);

// Traditional function expression:
const squares = arr.map(function (x) { return x * x });

其次,它们的 `this` 是从周围环境中获取的(*词法作用域*)。因此,你不再需要 `bind()` 或 `that = this` 了。

function UiComponent() {
    const button = document.getElementById('myButton');
    button.addEventListener('click', () => {
        console.log('CLICK');
        this.handleClick(); // lexical `this`
    });
}

以下变量在箭头函数内部都是词法变量

13.2 传统函数由于 `this` 的原因,不适合作为非方法函数

在 JavaScript 中,传统函数可以用作

  1. 非方法函数
  2. 方法
  3. 构造函数

这些角色之间存在冲突:由于角色 2 和 3,函数始终拥有自己的 `this`。但这会阻止你从回调函数(角色 1)内部访问例如周围方法的 `this`。

你可以在以下 ES5 代码中看到这一点

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) { // (A)
    'use strict';
    return arr.map(function (x) { // (B)
        // Doesn’t work:
        return this.prefix + x; // (C)
    });
};

在 C 行,我们想访问 `this.prefix`,但不能,因为 B 行函数的 `this` 遮蔽了 A 行方法的 `this`。在严格模式下,非方法函数中的 `this` 是 `undefined`,这就是为什么我们在使用 `Prefixer` 时会出错

> var pre = new Prefixer('Hi ');
> pre.prefixArray(['Joe', 'Alex'])
TypeError: Cannot read property 'prefix' of undefined

在 ECMAScript 5 中,有三种方法可以解决这个问题。

13.2.1 解决方案 1:`that = this`

你可以将 `this` 赋值给一个不会被遮蔽的变量。这就是下面 A 行所做的

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
    var that = this; // (A)
    return arr.map(function (x) {
        return that.prefix + x;
    });
};

现在 `Prefixer` 可以按预期工作了

> var pre = new Prefixer('Hi ');
> pre.prefixArray(['Joe', 'Alex'])
[ 'Hi Joe', 'Hi Alex' ]

13.2.2 解决方案 2:指定 `this` 的值

一些数组方法有一个额外的参数,用于指定在调用回调函数时 `this` 应该具有的值。这就是下面 A 行中的最后一个参数。

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
    return arr.map(function (x) {
        return this.prefix + x;
    }, this); // (A)
};

13.2.3 解决方案 3:`bind(this)`

你可以使用 `bind()` 方法将一个 `this` 由其调用方式(通过 `call()`、函数调用、方法调用等)决定的函数转换为一个 `this` 始终为相同固定值的函数。这就是我们在下面 A 行所做的。

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
    return arr.map(function (x) {
        return this.prefix + x;
    }.bind(this)); // (A)
};

13.2.4 ECMAScript 6 解决方案:箭头函数

箭头函数的工作原理与解决方案 3 非常相似。但是,最好将它们视为一种不会在词法上遮蔽 `this` 的新型函数。也就是说,它们不同于普通函数(你甚至可以说它们的功能更少)。它们不是绑定了 `this` 的普通函数。

使用箭头函数,代码如下所示。

function Prefixer(prefix) {
    this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
    return arr.map((x) => {
        return this.prefix + x;
    });
};

为了完全使用 ES6 语法,你可以使用类和更紧凑的箭头函数变体

class Prefixer {
    constructor(prefix) {
        this.prefix = prefix;
    }
    prefixArray(arr) {
        return arr.map(x => this.prefix + x); // (A)
    }
}

在 A 行,我们通过调整箭头函数的两个部分来节省了一些字符

13.3 箭头函数语法

选择“胖”箭头 `=>`(而不是瘦箭头 `->`) 是为了与 CoffeeScript 兼容,CoffeeScript 的胖箭头函数非常相似。

指定参数

    () => { ... } // no parameter
     x => { ... } // one parameter, an identifier
(x, y) => { ... } // several parameters

指定函数体

x => { return x * x }  // block
x => x * x  // expression, equivalent to previous line

语句块的行为类似于普通的函数体。例如,你需要使用 `return` 来返回值。对于表达式主体,表达式始终是隐式返回的。

请注意,带有表达式主体的箭头函数可以大大减少代码的冗余。比较

const squares = [1, 2, 3].map(function (x) { return x * x });
const squares = [1, 2, 3].map(x => x * x);

13.3.1 省略单个参数周围的括号

只有当参数由单个标识符组成时,才能省略参数周围的括号

> [1,2,3].map(x => 2 * x)
[ 2, 4, 6 ]

一旦有其他任何内容,你就必须输入括号,即使只有一个参数。例如,如果你解构单个参数,则需要括号

> [[1,2], [3,4]].map(([a,b]) => a + b)
[ 3, 7 ]

如果单个参数具有默认值(`undefined` 会触发默认值!),则需要括号

> [1, undefined, 3].map((x='yes') => x)
[ 1, 'yes', 3 ]

13.4 词法变量

13.4.1 传播变量值:静态与动态

以下是传播变量值的两种方式。

首先,静态(词法):变量的可访问性由程序的结构决定。在作用域中声明的变量在其嵌套的所有作用域中都可访问(除非被遮蔽)。例如

const x = 123;

function foo(y) {
    return x; // value received statically
}

其次,动态:变量值可以通过函数调用传播。例如

function bar(arg) {
    return arg; // value received dynamically
}

13.4.2 箭头函数中的词法变量

`this` 的来源是箭头函数的一个重要区别

其值由词法作用域决定的变量的完整列表

13.5 语法陷阱

有一些与语法相关的细节有时会让你感到困惑。

13.5.1 箭头函数的绑定优先级很低

如果你将 `=>` 视为运算符,你可以说它的优先级很低,绑定得很松散。这意味着如果它与其他运算符冲突,其他运算符通常会获胜。

这样做的原因是允许表达式主体“粘在一起”

const f = x => (x % 2) === 0 ? x : 0;

换句话说,我们希望 `=>` 在与 `===` 和 `?` 的竞争中失败。我们希望它被解释如下

const f = x => ((x % 2) === 0 ? x : 0);

如果 `=>` 在两者中都获胜,它将如下所示

const f = (x => (x % 2)) === 0 ? x : 0;

如果 `=>` 在与 `===` 的竞争中失败,但在与 `?` 的竞争中获胜,它将如下所示

const f = (x => ((x % 2) === 0)) ? x : 0;

因此,如果箭头函数与其他运算符竞争,你通常必须将它们括在括号中。例如

console.log(typeof () => {}); // SyntaxError
console.log(typeof (() => {})); // OK

另一方面,你可以使用 `typeof` 作为表达式主体,而无需将其括在括号中

const f = x => typeof x;

13.5.2 箭头函数参数后不能换行

ES6 禁止在箭头函数的参数定义和箭头之间换行

const func1 = (x, y) // SyntaxError
=> {
    return x + y;
};
const func2 = (x, y) => // OK
{
    return x + y;
};
const func3 = (x, y) => { // OK
    return x + y;
};

const func4 = (x, y) // SyntaxError
=> x + y;
const func5 = (x, y) => // OK
x + y;

参数定义 *内部* 的换行是可以的

const func6 = ( // OK
    x,
    y
) => {
    return x + y;
};

此限制的理由是,它为将来使用“无头”箭头函数保留了选择(在定义具有零个参数的箭头函数时,你可以省略括号)。

13.5.3 不能使用语句作为表达式主体

13.5.3.1 表达式与语句

快速回顾(有关详细信息,请参阅“Speaking JavaScript”

表达式产生(被求值为)值。例子

3 + 4
foo(7)
'abc'.length

语句做事情。例子

while (true) { ··· }
return 123;

大多数表达式1 都可以用作语句,只需在语句位置提及它们即可

function bar() {
    3 + 4;
    foo(7);
    'abc'.length;
}
13.5.3.2 箭头函数的函数体

如果表达式是箭头函数的函数体,则不需要大括号

asyncFunc.then(x => console.log(x));

但是,语句必须放在大括号中

asyncFunc.catch(x => { throw x });

13.5.4 返回对象字面量

JavaScript 语法中的某些部分是不明确的。以以下代码为例。

{
    bar: 123
}

它可能是

鉴于箭头函数的函数体可以是表达式或语句,如果你希望对象字面量是表达式主体,则必须将其括在括号中

> const f1 = x => ({ bar: 123 });
> f1()
{ bar: 123 }

为了进行比较,这是一个函数体为代码块的箭头函数

> const f2 = x => { bar: 123 };
> f2()
undefined

13.6 立即执行的箭头函数

还记得立即执行的函数表达式 (IIFE) 吗?它们如下所示,用于在 ECMAScript 5 中模拟块级作用域和返回值的代码块

(function () { // open IIFE
    // inside IIFE
})(); // close IIFE

如果使用立即执行的箭头函数 (IIAF),则可以节省一些字符

(() => {
    return 123
})();

13.6.1 分号

与 IIFE 类似,你应该使用分号终止 IIAF(或使用等效措施),以避免将两个连续的 IIAF 解释为函数调用(第一个作为函数,第二个作为参数)。

13.6.2 带有代码块主体的箭头函数的括号

即使 IIAF 具有代码块主体,你也必须将其括在括号中,因为它不能(直接)作为函数调用。这种语法约束的原因是为了与函数体为表达式的箭头函数保持一致(如下所述)。

因此,括号必须放在箭头函数周围。相反,对于 IIFE,你可以选择将括号放在整个表达式周围

(function () {
    ···
}());

或者只放在函数表达式周围

(function () {
    ···
})();

考虑到箭头函数的工作原理,从现在开始,应该优先考虑后一种加括号的方式。

13.6.3 带有表达式主体的箭头函数的括号

如果你想理解为什么不能在箭头函数后面直接加括号来调用它,你必须先了解表达式体的运作方式:表达式体后面的括号应该属于表达式的一部分,而不是整个箭头函数的调用。这与箭头函数的松散绑定有关,正如前面章节所解释的那样。

让我们看一个例子

const value = () => foo();

这应该被解释为

const value = () => (foo());

而不是

const value = (() => foo)();

延伸阅读:关于可调用实体的章节中的一节提供了更多关于在 ES6 中使用 IIFE 和 IIAF 的信息。剧透:你很少需要它们,因为 ES6 通常提供更好的替代方案。

13.7 箭头函数与 bind()

ES6 箭头函数通常是 Function.prototype.bind() 的一个引人注目的替代方案。

13.7.1 提取方法

如果要将提取的方法作为回调函数使用,则必须指定固定的 this,否则它将作为普通函数被调用(并且 this 将是 undefined 或全局对象)。例如

obj.on('anEvent', this.handleEvent.bind(this));

另一种方法是使用箭头函数

obj.on('anEvent', event => this.handleEvent(event));

13.7.2 通过参数传递 this

以下代码演示了一个巧妙的技巧:对于某些方法,你不需要 bind() 来绑定回调函数,因为它们允许你通过额外的参数指定 this 的值。filter() 就是这样一种方法

const as = new Set([1, 2, 3]);
const bs = new Set([3, 2, 4]);
const intersection = [...as].filter(bs.has, bs);
    // [2, 3]

但是,如果使用箭头函数,这段代码更容易理解

const as = new Set([1, 2, 3]);
const bs = new Set([3, 2, 4]);
const intersection = [...as].filter(a => bs.has(a));
    // [2, 3]

13.7.3 部分求值

bind() 使你能够进行部分求值,你可以通过填充现有函数的参数来创建新函数

function add(x, y) {
    return x + y;
}
const plus1 = add.bind(undefined, 1);

同样,我发现箭头函数更容易理解

const plus1 = y => add(1, y);

13.8 箭头函数与普通函数

箭头函数与普通函数只有两个方面的不同

除此之外,箭头函数和普通函数之间没有其他可观察到的差异。例如,typeofinstanceof 会产生相同的结果

> typeof (() => {})
'function'
> () => {} instanceof Function
true

> typeof function () {}
'function'
> function () {} instanceof Function
true

有关何时使用箭头函数以及何时使用传统函数的更多信息,请参阅关于可调用实体的章节

13.9 常见问题解答:箭头函数

13.9.1 为什么 ES6 中有“胖”箭头函数 (=>),却没有“瘦”箭头函数 (->)?

ECMAScript 6 为具有词法 this 的函数提供了语法,即所谓的*箭头函数*。但是,它没有为具有动态 this 的函数提供箭头语法。这种省略是故意的;方法定义涵盖了瘦箭头的绝大多数用例。如果你真的需要动态 this,你仍然可以使用传统的函数表达式。

下一页:14. 类之外的新 OOP 特性