箭头函数有两个优点。
首先,它们比传统的函数表达式更简洁
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`
});
}
以下变量在箭头函数内部都是词法变量
arguments
super
this
new.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` 的来源是箭头函数的一个重要区别
其值由词法作用域决定的变量的完整列表是
arguments
super
this
new.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
,你仍然可以使用传统的函数表达式。