类与箭头函数(一个警告)(Of Classes and Arrow Functions (a cautionary tale))
注意,新的热点!箭头函数赶走了令人厌烦的 function
关键字,并且(凭借 this
词法作用域的优点)给广大程序员带来了乐趣。然而,正如下文所述,即便是最好的工具也应该被谨慎的使用。
一个匆忙的复习(A Hasty Refresher)
传统的函数表达式创建一个函数,它的 this
值是动态的,如果没有明确的调用者的话,要么是指向调用它的对象,要么是全局对象¹。另一方面,箭头函数表达式总是假定 this
指向当前包裹它的代码。
let outerThis, tfeThis, afeThis;
let obj = {
outer() {
outerThis = this;
traditionalFE = function() {tfeThis = this};
traditionalFE();
arrowFE = () => afeThis = this;
arrowFE();
}
}
obj.outer();
outerThis; // obj
tfeThis; // global
afeThis; // obj
outerThis === afeThis; // true
箭头函数和类(Arrow functions and classes)
考虑到箭头函数对待上下文的严谨方式,很容易将其作为类中方法的替代品。考虑这个简单的类,它抑制给定容器中的所有点击事件,并且报告哪个 DOM 节点的点击事件被抑制了:
class ClickSuppresser {
constructor(domNode) {
this.container = domNode;
this.initialize();
}
suppressClick(e) {
e.preventDefault();
e.stopPropagation();
this.clickSuppressed(e);
}
clickSuppressed(e) {
console.log('click suppressed on', e.target);
}
initialize() {
this.container.addEventListener(
'click', this.suppressClick.bind(this));
}
}
此实现使用ES6方法简写语法。我们需要将事件监听绑定到当前实例上,否则 suppressClick
中的 this
值将指向容器节点。
使用箭头函数代替方法语法消除了绑定处理程序的需要:
class ClickSuppresser {
constructor(domNode) {
this.container = domNode;
this.initialize();
}
suppressClick = e => {
e.preventDefault();
e.stopPropagation();
this.clickSuppressed(e);
}
clickSuppressed = e => {
console.log('click suppressed on', e.target);
}
initialize = () => {
this.container.addEventListener(
'click', this.suppressClick);
}
}
完美!
等等,这是什么?
ClickSuppresser.prototype.suppressClick; // undefined
ClickSuppresser.prototype.clickSuppressed; // undefined
ClickSuppresser.prototype.initialize; // undefined
为什么方法没有被添加到原型上?
问题不在于箭头函数本身,而在于它是如何到达那里的。箭头函数不是方法,它们是匿名函数表达式,所以将它们添加到类中的唯一方法是赋值给属性。ES类以完全不同的方式处理方法和属性。
方法被添加到类的原型中,这正是我们需要它们的地方——这意味着它们只定义一次,而不是每个实例定义一次。相比之下,类属性语法(在撰写本文时是一个ES7候选提议²)是为相同的属性分配给每一个实例的语法糖。实际上,类属性是这样工作的:
class ClickSuppresser {
constructor(domNode) {
this.suppressClick = e => {...}
this.clickSuppressed = e => {...}
this.initialize = e => {...}
this.node = domNode;
this.initialize();
}
}
换句话说,每次ClickSuppresser
的新实例被创建时,我们示例代码中的三个函数都会被重新定义。
const cs1 = new ClickSuppresser();
const cs2 = new ClickSuppresser();
cs1.suppressClick === cs2.suppressClick; // false
cs1.clickSuppressed === cs2.clickSuppressed; // false
cs1.initialize === cs2.initialize; // false
往好里说,这是令人惊讶和不直观的,往坏里说,这是不必要的低效。无论如何,它都违背了使用类或共享原型的目的。
在这一点上(甜蜜的讽刺),箭头函数起到了拯救作用(In which (sweet irony) arrow functions come to the rescue)
由于这一意外事件的发生,我们的英雄感到灰心丧气,于是返回到标准方法语法。但 bind
函数仍然是一个棘手的问题。 bind
除了速度相对较慢之外,还会创建难以调试的不透明包装器。
initialize() {
this.container.addEventListener(
'click', e => this.suppressClick(e));
}
为什么这次生效了?由于使用常规方法语法定义了 suppressClick
,它将获取调用它的实例的上下文(在上面的示例中)。由于箭头函数在词法上有作用域,因此 this
将是我们类的当前实例。
如果你不想每次都要查找参数,你可以利用rest/spread操作符:
initialize() {
this.container.addEventListener(
'click', (...args) => this.suppressClick(...args));
}
让我们结束今天的内容(Wrap up)
使用箭头函数作为类方法的替代品,我从来没有感到舒服过。方法应该根据调用它们的实例动态确定范围,但是箭头函数的定义是静态确定范围的。事实证明,使用属性来描述公共功能所带来的效率问题同样会导致范围问题。无论哪种方式,在使用箭头函数作为类定义的一部分时都应该三思。
寓意:箭头函数很好,但是使用合适的工具更好。
1、 undefined
严格模式下
2、 https://github.com/jeffmo/es-class-static-properties-and-fields
原文链接:https://javascriptweblog.wordpress.com/2015/11/02/of-classes-and-arrow-functions-a-cautionary-tale/