箭头函数有两个优点。
首先,它们比传统的函数表达式更简洁
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`
});
}
以下变量在箭头函数内部都是词法变量
argumentssuperthisnew.target在 JavaScript 中,传统函数可以用作
这些角色之间存在冲突:由于角色 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 中,有三种方法可以解决这个问题。
你可以将 `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' ]
一些数组方法有一个额外的参数,用于指定在调用回调函数时 `this` 应该具有的值。这就是下面 A 行中的最后一个参数。
function Prefixer(prefix) {
this.prefix = prefix;
}
Prefixer.prototype.prefixArray = function (arr) {
return arr.map(function (x) {
return this.prefix + x;
}, this); // (A)
};
你可以使用 `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)
};
箭头函数的工作原理与解决方案 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 行,我们通过调整箭头函数的两个部分来节省了一些字符
选择“胖”箭头 `=>`(而不是瘦箭头 `->`) 是为了与 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);
只有当参数由单个标识符组成时,才能省略参数周围的括号
> [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 ]
以下是传播变量值的两种方式。
首先,静态(词法):变量的可访问性由程序的结构决定。在作用域中声明的变量在其嵌套的所有作用域中都可访问(除非被遮蔽)。例如
const x = 123;
function foo(y) {
return x; // value received statically
}
其次,动态:变量值可以通过函数调用传播。例如
function bar(arg) {
return arg; // value received dynamically
}
`this` 的来源是箭头函数的一个重要区别
其值由词法作用域决定的变量的完整列表是
argumentssuperthisnew.target有一些与语法相关的细节有时会让你感到困惑。
如果你将 `=>` 视为运算符,你可以说它的优先级很低,绑定得很松散。这意味着如果它与其他运算符冲突,其他运算符通常会获胜。
这样做的原因是允许表达式主体“粘在一起”
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;
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;
};
此限制的理由是,它为将来使用“无头”箭头函数保留了选择(在定义具有零个参数的箭头函数时,你可以省略括号)。
快速回顾(有关详细信息,请参阅“Speaking JavaScript”)
表达式产生(被求值为)值。例子
3 + 4
foo(7)
'abc'.length
语句做事情。例子
while (true) { ··· }
return 123;
大多数表达式1 都可以用作语句,只需在语句位置提及它们即可
function bar() {
3 + 4;
foo(7);
'abc'.length;
}
如果表达式是箭头函数的函数体,则不需要大括号
asyncFunc.then(x => console.log(x));
但是,语句必须放在大括号中
asyncFunc.catch(x => { throw x });
JavaScript 语法中的某些部分是不明确的。以以下代码为例。
{
bar: 123
}
它可能是
鉴于箭头函数的函数体可以是表达式或语句,如果你希望对象字面量是表达式主体,则必须将其括在括号中
> const f1 = x => ({ bar: 123 });
> f1()
{ bar: 123 }
为了进行比较,这是一个函数体为代码块的箭头函数
> const f2 = x => { bar: 123 };
> f2()
undefined
还记得立即执行的函数表达式 (IIFE) 吗?它们如下所示,用于在 ECMAScript 5 中模拟块级作用域和返回值的代码块
(function () { // open IIFE
// inside IIFE
})(); // close IIFE
如果使用立即执行的箭头函数 (IIAF),则可以节省一些字符
(() => {
return 123
})();
与 IIFE 类似,你应该使用分号终止 IIAF(或使用等效措施),以避免将两个连续的 IIAF 解释为函数调用(第一个作为函数,第二个作为参数)。
即使 IIAF 具有代码块主体,你也必须将其括在括号中,因为它不能(直接)作为函数调用。这种语法约束的原因是为了与函数体为表达式的箭头函数保持一致(如下所述)。
因此,括号必须放在箭头函数周围。相反,对于 IIFE,你可以选择将括号放在整个表达式周围
(function () {
···
}());
或者只放在函数表达式周围
(function () {
···
})();
考虑到箭头函数的工作原理,从现在开始,应该优先考虑后一种加括号的方式。
如果你想理解为什么不能在箭头函数后面直接加括号来调用它,你必须先了解表达式体的运作方式:表达式体后面的括号应该属于表达式的一部分,而不是整个箭头函数的调用。这与箭头函数的松散绑定有关,正如前面章节所解释的那样。
让我们看一个例子
const value = () => foo();
这应该被解释为
const value = () => (foo());
而不是
const value = (() => foo)();
延伸阅读:关于可调用实体的章节中的一节提供了更多关于在 ES6 中使用 IIFE 和 IIAF 的信息。剧透:你很少需要它们,因为 ES6 通常提供更好的替代方案。
bind() ES6 箭头函数通常是 Function.prototype.bind() 的一个引人注目的替代方案。
如果要将提取的方法作为回调函数使用,则必须指定固定的 this,否则它将作为普通函数被调用(并且 this 将是 undefined 或全局对象)。例如
obj.on('anEvent', this.handleEvent.bind(this));
另一种方法是使用箭头函数
obj.on('anEvent', event => this.handleEvent(event));
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]
bind() 使你能够进行部分求值,你可以通过填充现有函数的参数来创建新函数
function add(x, y) {
return x + y;
}
const plus1 = add.bind(undefined, 1);
同样,我发现箭头函数更容易理解
const plus1 = y => add(1, y);
箭头函数与普通函数只有两个方面的不同
arguments、super、this、new.target[[Construct]] 和属性 prototype 支持 new。箭头函数两者都没有,这就是 new (() => {}) 会抛出错误的原因。除此之外,箭头函数和普通函数之间没有其他可观察到的差异。例如,typeof 和 instanceof 会产生相同的结果
> typeof (() => {})
'function'
> () => {} instanceof Function
true
> typeof function () {}
'function'
> function () {} instanceof Function
true
有关何时使用箭头函数以及何时使用传统函数的更多信息,请参阅关于可调用实体的章节。
=>),却没有“瘦”箭头函数 (->)? ECMAScript 6 为具有词法 this 的函数提供了语法,即所谓的*箭头函数*。但是,它没有为具有动态 this 的函数提供箭头语法。这种省略是故意的;方法定义涵盖了瘦箭头的绝大多数用例。如果你真的需要动态 this,你仍然可以使用传统的函数表达式。