JS-class

Posted by Joel on February 22,2018

本文探究ES6class语法糖的一些行为及babel实现。

PS:关于ES5继承的细节不在本文讨论范围

PPS:最好有ES5的面向对象知识

what the hell is class

class是一个语法糖,好不好吃就要看各位的口味了,毕竟甲之蜜糖乙之砒霜。

既然是语法糖,那么就可以用现有的语言特性实现它(就是ES5的原型式继承),本文中我们直接分析babel编译出的ES5代码。

坠简单滴例子

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
constructor(name) {
this.name = name
}

sayHi() {
console.log('Hi, this is ', this.name)
}

static sayGoodbye() {
console.log('goodbye')
}
}

额,经过babel编译出来好大一坨玩意儿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
'use strict';

var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}

return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

var Person = function () {
function Person(name) {
_classCallCheck(this, Person);

this.name = name;
}

_createClass(Person, [{
key: 'sayHi',
value: function sayHi() {
console.log('Hi, this is ', this.name);
}
}], [{
key: 'sayGoodbye',
value: function sayGoodbye() {
console.log('goodbye');
}
}]);

return Person;
}();

太长不看右上角!

别急啊大爷,我带你一点点看。

首先最下面Person的变量就是是我们的Person类,它其实是一个由IIFE返回的函数。

1
2
3
4
5
function Person(name) {
_classCallCheck(this, Person);

this.name = name;
}

很眼熟吧,这个函数其实就是我们在ES5中的构造函数。

就是构造函数里多了点什么东西->_classCallCheck

为什么要有_classCallCheck

ES5中构造函数就是一个普通的函数,因此可以直接调用,只不过与通过new关键字调用有不同的行为,而ES标准要求class只能通过new调用,因此需要一个验证机制,在不当调用时报错。

1
2
3
4
5
6
// _classCallCheck
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

_classCallCheck函数很简单,接收两个参数,然后用instanceof检测instance是不是Constructor的实例,如果不是就报错。

当通过new调用时,Person内部的this为新创建的实例(也就是说this__proto__已经指向了Personprototype),此时能过通过检测。

Person作为函数调用时就会报错。

可以了解一下ES6的new.target

实例的静态属性

常写React的童鞋应该很熟悉这种写法

1
2
3
4
5
class App extends PureComponent {
state = {
...
}
}

那么这种写法与我们在constructor中定义的属性有什么区别吗?

我们将代码修改如下,注释处是修改的地方。

1
2
3
4
5
6
7
8
9
10
11
class Person {
// 静态实例属性
state = {}

constructor(name) {
this.name = name
}

...

}

打包出来的代码如下

1
2
3
4
5
6
7
8
function Person(name) {
_classCallCheck(this, Person);

// 静态实例属性
this.state = {};

this.name = name;
}

emmmm,好像并没有什么卵区别。

其实确实没有什么卵区别

按我的理解,所谓的静态实例属性,就是指不能在新建对象时指定值的属性,因而称为静态。

那么既然是实例属性,那么它必然是定义在实例上,并没有什么魔法。

其实这也是有些人不喜欢语法糖的原因:遮盖了实现的细节,容易让人产生疑惑。

_createClass是什么鬼???

在ES5中,我们将属性放在对象中,将方法放在原型中。

也就是定义了构造函数后再去修改构造函数的prototype

但是这样写其实很烦人。。。

上面我们看到Person函数就是定义好的构造函数,而_createClass处理构造函数,向原型和构造函数本身添加方法:

  • 实例方法:向Person.prototype添加sayHi方法。
  • 静态方法:向Person函数添加sayGoodbye方法,称为静态方法。

看不懂的去补JS基础。。。

1
2
3
4
5
6
// _createClass
function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};

先看_createClass的签名

  • Constructor:就是我们的构造函数Person
  • protoProps:是要放到原型上的方法(sayHi
  • staticProps是要放到Person函数的静态方法(sayGoodbye)。

类声明中,static关键字表示该方法是类的静态方法。

_createClass接着用defineProperties函数分别处理了Person和它的prototype

也就是ES5中需要手动处理的步骤。

再来看_createClass是怎么调用的

1
2
3
4
5
6
7
8
9
10
11
_createClass(Person, [{
key: 'sayHi',
value: function sayHi() {
console.log('Hi, this is ', this.name);
}
}], [{
key: 'sayGoodbye',
value: function sayGoodbye() {
console.log('goodbye');
}
}]);

可以发现,这些方法已经不是函数了,而是一个个属性描述符对象,这些对象被交给了defineProperties函数处理。

这又是为什么呢?

defineProperties用于写入属性,为什么要有这一步呢?为什么不能直接把方法写入要搞的这么复杂呢?

其实还是ES标准的原因。

ES规定class内部定义的所有方法都是不可枚举的,也就是说Object.keys(Person.prototype)不会返回sayHi方法。

defineProperties用于修改属性描述符,并写入目标对象。

这样就与标准的规定一致了。

至此我们的Person类已经被处理好了。

实例化一个类试试。

1
2
3
const jack = new Person('jack')

console.log(jack)

确实如我们预想的一样。

再看看Person

1
console.dir(Person)

可以看到Person确实是一个函数,sayGoodbye确实是该函数的一个属性。

What’s more?

为了解决this指向丢失的问题,我们常常采用下面这种写法,但是很多人并不知道到底发生了什么。。。

其实写这文章的原因就是群里有人提问实例静态属性和constructor中初始化的属性的区别。。。万恶的语法糖!

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
constructor(name) {
this.name = name
}

sayHi() {
console.log('Hi, this is ', this.name)
}

waveArm = () => {
console.log(this.name, 'is waving arm')
}
}

经过了上面的分析,我们可以看出来,这其实就是一个静态实例属性,而this指向问题是用箭头函数解决的。

下面是babel编译出的代码

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
var _this = this;

_classCallCheck(this, Person);

this.waveArm = function () {
console.log(_this.name, 'is waving arm');
};

this.name = name;
}

下面是实例的属性

可以看到,确实如我们预料的一样,waveArm方法并不在原型中,而是作为实例的属性。

只不过babel用闭包实现了箭头函数的行为。

一点看法

个人还是挺喜欢这个语法糖的,毕竟少写很多代码。。。

其实实现继承并不一定要用class或者ES5那一套做法,使用Object.create一样可以做到,而且更加简洁。。。

不过都得对原型实现面向对象这一套东西有深入了解。

毕竟万变不离其宗。

再说了,函数式大法好!要啥面相对象(手动滑稽

下一篇文章讲extend关键字干了什么。。。


EOF


>