天天看点

类与箭头函数(一个警告)类与箭头函数(一个警告)(Of Classes and Arrow Functions (a cautionary tale))

类与箭头函数(一个警告)(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/

继续阅读