十二、ES6系列之Class基本用法

简介

在 JavaScript 语言中,生成实例对象的传统方法是通过构造函数。

1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
}

Person.prototype.sayHello = function () {
return "hello, I am " + this.name;
};

var kevin = new Person("Kevin");
kevin.sayHello(); // hello, I am Kevin

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。
ES6 的 class 可以看作一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

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

sayHello() {
return "hello, I am " + this.name;
}
}

var kevin = new Person("Kevin");
kevin.sayHello(); // hello, I am Kevin

我们可以看到 ES5 的构造函数 Person,对应 ES6 的 Person 类的 constructor 方法。

1
2
3
Person.prototype.constructor === Person; // true
kevin.constructor === Person.prototype.constructor; // true
kevin.sayHello === Person.prototype.sayHello; // true

上面代码表明,类本身就指向构造函数,类的所有方法都定义在类的 prototype 属性上面。

值得注意的是:类的内部所有定义的方法,都是不可枚举的(non-enumerable)

以上面的例子为例,在 ES6 中:

1
2
Object.keys(Person.prototype); // []
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"]

然而在 ES5 中:

1
2
Object.keys(Person.prototype); // ['sayHello']
Object.getOwnPropertyNames(Person.prototype); // ["constructor", "sayHello"]

类的实例

生成类的实例的写法,与 ES5 完全一样,也是使用 new 命令。如果忘记加上 new,像函数那样调用 Class,将会报错。
这也是类跟普通构造函数的一个主要区别,后者不用 new 也可以执行。

1
2
3
4
5
6
7
8
9
class Person {
// ...
}

// 报错
var person = Person(2, 3);

// 正确
var person = new Person(2, 3);

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在 this 对象上),否则都是定义在原型上(即定义在 class 上)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}

sayHello() {
return (
"hello, I am " + this.name + "and I'm " + this.age + " years old today"
);
}
}
var kevin = new Person("Kevin"8);
kevin.sayHello(); // "hello, I am Kevinand I'm 8 years old today"
kevin.hasOwnProperty('name') // true
kevin.hasOwnProperty('age') // true
kevin.hasOwnProperty('sayHello') // false
kevin.__proto__.hasOwnProperty('sayHello') // true

同样,类的所有实例共享一个原型对象。

1
2
3
4
var p1 = new Person("lilei", 13);
var p2 = new Person("hanmeimei", 12);

p1.__proto__ === p2.__proto__; // true

实例属性

以前,我们定义实例属性,只能写在类的 constructor 方法里面。比如:

1
2
3
4
5
class Person {
constructor() {
this._count = 0;
}
}

然而现在也可以定义在类的最顶层,其他都不变:

1
2
3
class Person {
_count = 0;
}

对应到 ES5 都是:

1
2
3
function Person() {
this._count = 0;
}

静态方法

所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

ES6 中:

1
2
3
4
5
6
7
8
9
10
class Person {
static sayHello() {
return "hello";
}
}

Person.sayHello(); // 'hello'

var kevin = new Person();
kevin.sayHello(); // TypeError: kevin.sayHello is not a function

对应 ES5:

1
2
3
4
5
6
7
8
9
10
function Person() {}

Person.sayHello = function () {
return "hello";
};

Person.sayHello(); // 'hello'

var kevin = new Person();
kevin.sayHello(); // TypeError: kevin.sayHello is not a function

静态属性

静态属性指的是 Class 本身的属性,即 Class.propName,而不是定义在实例对象(this)上的属性。以前,我们添加静态属性只可以这样:

1
2
3
class Person {}

Person.name = "kevin";

现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上 static 关键字。

1
2
3
class Person {
static name = "kevin";
}

对应到 ES5 都是:

1
2
3
function Person() {}

Person.name = "kevin";

getter 和 setter

与 ES5 一样,在“类”的内部可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
get name() {
return "kevin";
}
set name(newName) {
console.log("new name 为:" + newName);
}
}

let person = new Person();

person.name = "daisy";
// new name 为:daisy

console.log(person.name);
// kevin

对应到 ES5 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name) {}

Person.prototype = {
get name() {
return "kevin";
},
set name(newName) {
console.log("new name 为:" + newName);
},
};

let person = new Person();

person.name = "daisy";
// new name 为:daisy

console.log(person.name);
// kevin

Babel 编译

现在我们已经知道了有关“类”的方法中,ES6 与 ES5 是如何对应的,实际上 Babel 在编译时并不会直接就转成这种形式,Babel 会自己生成一些辅助函数,帮助实现 ES6 的特性。

我们可以在 Babel 官网的 Try it out 页面查看 ES6 的代码编译成什么样子。

编译(一)

ES6 代码为:

1
2
3
4
5
class Person {
constructor(name) {
this.name = name;
}
}

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
"use strict";

function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}

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

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

this.name = name;
};

_instanceof 用于判断某对象是否为某构造器的实例。
_classCallCheck 的作用是检查 Person 是否是通过 new 的方式调用,在上面,我们也说过,类必须使用 new 调用,否则会报错。

当我们使用 var person = Person() 的形式调用的时候,this 指向 window,所以 instance instanceof Constructor 就会为 false,与 ES6 的要求一致。

编译(二)

ES6 代码为:

1
2
3
4
5
6
7
8
9
10
class Person {
// 实例属性
foo = "foo";
// 静态属性
static bar = "bar";

constructor(name) {
this.name = name;
}
}
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
"use strict";

function _instanceof(left, right) {...}

function _classCallCheck(instance, Constructor) {...}

function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}

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

_defineProperty(this, "foo", 'foo');

this.name = name;
};

_defineProperty(Person, "bar", 'bar');

_defineProperty 的作用是给某对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

编译(三)

ES6 代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
constructor(name) {
this.name = name;
}

sayHello() {
return "hello, I am " + this.name;
}

static onlySayHello() {
return "hello";
}

get name() {
return "kevin";
}

set name(newName) {
console.log("new name 为:" + newName);
}
}

对应到 ES5 的代码应该是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name) {
this.name = name;
}

Person.prototype = {
sayHello: function () {
return "hello, I am " + this.name;
},
get name() {
return "kevin";
},
set name(newName) {
console.log("new name 为:" + newName);
},
};

Person.onlySayHello = function () {
return "hello";
};

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
function _instanceof(left, right) {...}

function _classCallCheck(instance, Constructor) {...}

function _defineProperty(obj, key, value) {...}

function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var Person = /*#__PURE__*/function () {
function Person(name) {
_classCallCheck(this, Person);

this.name = name;
}

_createClass(Person, [{
key: "sayHello",
value: function sayHello() {
return "hello, I am " + this.name;
}
}, {
key: "name",
get: function get() {
return "kevin";
},
set: function set(newName) {
console.log("new name 为:" + newName);
}
}], [{
key: "onlySayHello",
value: function onlySayHello() {
return "hello";
}
}]);

return Person;
}();

我们可以看到 Babel 生成了一个 _createClass 辅助函数,该函数传入三个参数,第一个是构造函数,在这个例子中也就是 Person,第二个是要添加到原型上的函数数组,第三个是要添加到构造函数本身的函数数组,也就是所有添加 static 关键字的函数。该函数的作用就是将函数数组中的方法添加到构造函数或者构造函数的原型中,最后返回这个构造函数。

参考文献