类属性中的箭头函数可能不如我们想象的那么伟大

原文地址:Arrow Functions in Class Properties Might Not Be As Great As We Think

类属性中的箭头函数可能不如我们想象的那么伟大

Class Properties Proposal简化了我们的生活,特别是在内部的React中state ,甚至是propTypesdefaultProps

无类属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Greeting extends Component {
constructor(props){
super(props);
this.state = {
isLoading:false,
}
}

render(){
return(
<div>Hello,{this.props.name}</div>
)
}
}

Greeting.propTypes = {
name: PropTypes.String.isRequired,
};

Greeting.defaultProps = {
name:'Stranger',
}

有类属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Greeting extends Component {
static propTypes = {
name:PropTypes.String.isRequire,
};

static defaultProps ={
name: 'Stranger'
};

state = {
isLoading:false,
}

render(){
return(
<div>Hello,{this.props.name}</div>
)
}
}

此外,类属性/属性初始化器在过去(2017年11月16日)六个月中似乎更趋向于处理 React 中的绑定而不是构造函数中的绑定调用。

类属性中箭头函数的用法:

1
2
3
4
5
6
7
8
9
class ComponentA extends Component{
handleClick = () =>{
// ...
}

render(){
// ...
}
}

类字段属性中的箭头函数看起来似乎非常有用,因为它们会自动绑定,不需要加 this.handleClick = this.handleClick.bind(this) 在构造器中。

那么,我们应该在类属性中运用箭头函数吗?首先,让我们看看类属性的作用。

为什么类属性看起来像曾经转化为 ES2017

让我们编写一个简单的类,其中包含一个静态属性、一个实例属性、一个属性中的箭头函数,以及一个通常作为插件方法的函数transform-class-properties

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
static color = 'red';
counter = 0;

handleClick = () =>{
this.counter++;
}

handleLongClick(){
this.counter++;
}

}

一旦我们进入 Babel REPL 用下面的预设将上面的类转换为ES2017的:es2017stage-2

我们就可以得到下面的转换版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
constructor(){
this.counter = 0;

this.handleClick = () =>{
this.counter++;
}
}

handleLongClick(){
this.counter++;
}
}
A.color = 'red'

如我们所见,实例属性已经被移到了构造函数里面,而静态属性则被移到了类后面的声明。

就作者个人而言,他很喜欢加 static 关键字,因为我们可以直接 export 使用静态属性。

在实例属性上,它的优点是不用在构造函数中编译导致其过于臃肿。

对于属性中的箭头函数, handleClick 也同样被移动到了构造函数,就像实例属性一样。

对于我们通常定义的 handleLongClick 方法则没有任何改变。

属性初始化器可能对属性很有用,但是对于类属性中的箭头函数,它就像是实现绑定的一种设计不良的解决方法。

Mockability

如果你想去模仿或者是监视一个类的方法,最简单而且最恰当的方式是使用原型。因为所有对象都可以通过原型链看到对对象原型对象的所有更改。

对上面例子中的 class A 进行一些测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
static color = 'red';
counter = 0;

handleClick = () =>{
this.counter++;
}

handleLongClick(){
this.counter++;
}

}

A.prototype.handleLongClick 被定义了

A.prototype.handleClick 不是一个方法

因为我们在类属性中使用了箭头函数,所以我们的函数 handleClick只在构造函数初始化时定义,而不是在原型中定义。因此我们在实例化对象中模拟我们的函数,其他对象也不会通过原型链看到这些更改。

Inheritance

定义基本类 A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A {
handleClick = () =>{
this.counter++;
}

handleLongClick(){
this.counter++;
}
}
console.log(A.prototype);
// {constructor: ƒ, handleLongClick: ƒ}

new A().handleClick();
// A.handleClick

new A().handleLongClick();
// A.handleLongClick

如果类B 继承 A,handleClick 不会在原型中而且我们也无法通过 super.handleClick 调用我们的箭头函数 handleClick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class B extends A{
handleClick = () =>{
super.handleClick();
console.log('B.handleClick');
}

handleLongClick(){
super.handleLongClick();
console.log('B.handleLongClick');
}
}
console.log(B.prototype);
// {constructor: ƒ, handleLongClick: ƒ}

console.log(B.prototype.__proto__);
// {constructor: ƒ, handleLongClick: ƒ}

new B().handleClick();
// Uncaught TypeError: (intermediate value).handleClick is not a function

new B().handleLongClick();
// A.handleLongClick
// B.handleLongClick

如果类C 去继承类 A,但是 handleClick 实现为一个普通函数而不是箭头函数,然后handleClick 只执行super.handleClick()而不做其他的,这样不是很奇怪吗?

这是因为 handleClick 在父类的构造函数中的实例化会覆盖它。

C.prototype.handleClick() 会调用我们的实现但是会因为上一个错误而失败:Uncaught TypeError: (intermediate value).handleClick is not a function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C extends A {
handleClick(){
super.handleClick();
console.log('C.handleClick');
}
}
console.log(C.prototype);
// {constructor: ƒ, handleLongClick: ƒ}

console.log(C.prototype.__proto__);
// {constructor: ƒ, handleLongClick: ƒ}

new C().handleClick();
// C.handleClick

如果类D 是一个继承类A的普通空白类,它就会有一个空的原型,然后 new D().handleClick 就会输出 A.handleClick

1
2
3
4
5
6
7
8
9
10
11
class D extends A {

}
console.log(D.prototype);
// {constructor: ƒ}

console.log(C.prototype.__proto__);
// {constructor: ƒ, handleLongClick: ƒ}

new D().handleClick();
// A.handleClick

Performance

现在是有趣的部分,让我们看看性能。

我们知道通常的函数在原型中定义,然后在所有的实例之间共享。如果我们有N个组件的列表,这些组件将会共享相同的方法。所以,如果我们的组件被点击,我们会调用方法N次,调用相同的原型。由于我们在原型中会多次调用相同的方法,JavaScript 引擎对其作出了优化。

另一方面,对于类属性的箭头函数,如果我们创建 N 个组件,这些组件也会创建 N 个函数。记得我们在转换版本中看到的内容,类属性在构造函数中初始化,这意味着我们点击 N 个组件会调用 N 个不同的方法。

让我们看看在 V8 引擎(Chrome)的基准测试是如何做的。

第一个很简单,我们只要测试实例化的时间,然后我们调用一次我们的方法。

注意,这个数字在这里并不重要,因为在应用程序中不会注意到实例化,我们要讨论的是每秒的操作,这个数字已经足够高了。我更关心函数之间的差距。

测试1

第二个,使用了一个代表性的用例,100个组件的实例化,就像一个列表,我们每次调用一个方法之后。

测试2

所有基准测试均在Mac OS Pro 13“2016 2GHz Mac OS X 10.13.1和Chrome 62.0.3202上运行。

简而言之,为了提高性能,应该在原型中去声明共享方法,并且只在需要的时候将其绑定到上下文(如果作为 prop 或者 callback 传递)。将我们的共享方法绑定到原型并在实例的构造函数中初始化我们的属性是有意义的,但方法并不多。

关于 high ops/s,我们可以清楚地看到类属性中的箭头函数并不像我们想象中那么高效。

结论

  • 类属性中箭头函数会被转换为构造函数
  • 类属性中的箭头函数不会在原型中,我们也无法通过 super 调用它们。
  • 类属性中的兼有函数比绑定函数慢得多,并且两者都慢于普通的函数
  • 如果你打算传递它,应该只用 .bind() 或者箭头函数绑定一个方法。
0%