ES6 Class的基本语法

注:这是一篇学习笔记,记录自己在ES6学习过程中按照自己的思路觉得应该记录的一些要点,方便以后查看和复习。参考:

阮一峰ES6的书籍中ES6 Class 的基本语法
我的ES6入门学习规划

这篇文章学习ES6面向对象编程的一些语法要点。

面向对象编程不是新鲜事,这篇笔记要记录的内容更不是新鲜事。其它语言比如java,早就有这些东西了。

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

constructor

构造函数是类基本的东西,ES6的class也跟其它语言一样,提供了constructor函数,利用它可以完成类的实例化逻辑。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

1
2
3
4
5
class Popup {
constructor({title, content, zIndex}) {
// ...
}
}

构造函数一般不需要显示地return this,但是可以通过return返回其它对象。 虽然如此,我认为这是错误的用法。 如果要这么搞,就不要在类这个层次这样做,这样破坏了面向对象的语义。 js如果是强类型的语言,这样子肯定是通不过编译的。

类必须使用new调用,否则会报错。

实例方法

在class的内部,直接可以编写类的实例方法,这些方法只有类的实例才能调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Popup {
constructor({title, content, zIndex}) {
// ...
}

show() {

}

hide() {

}
}

实例方法会被添加到类的原型上。

1
Popup.prototype.hasOwnProperty('show')// true

类的多个实例,共用同一个原型,与ES5是一致的。

实例属性

在构造函数或实例方法内,通过this可为类的实例添加实例属性,同时实例属性也可以统一在实例方法并列的位置进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Popup {
_title = '';
constructor({title, content, zIndex}) {
// ...
this._title = title;
this._content = content;
}

show() {

}

hide() {

}
}

这点也是为了跟其它语言保持一致,让类的结构看起来更像是一个面向对象的风格。

setters和getters

在java语言里面,这两个东西是访问实例属性的最常用的方式,因为java语言里面有访问修饰符,不是随便就能访问到实例成员的。 在ES6的class里面,setter与getters可以与实例方法并列地放置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Popup {
_title = '';
constructor({title, content, zIndex}) {
// ...
this._title = title;
this._content = content;
}

show() {

}

hide() {

}

set title(title) {
this._title = title;
}

get title() {
return this._title;
}
}

setters和getters部署在属性描述对象(PropertyDescriptor)上面。

属性名表达式

ES6的class内部,除了构造函数,其它实例成员,可以使用属性名表达式,就跟在字面量对象中使用一样。

class表达式

ES6的class可以写出表达式,变成类似java的内部类:

1
2
3
4
5
const MyClass = class Me {
getClassName() {
return Me.name;
}
};

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。

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

sayName() {
console.log(this.name);
}
}('张三');

person.sayName(); // "张三"

上面代码中,person是一个立即执行的类的实例。

注意点

  1. 类的内部所有定义的方法,都是不可枚举的(non-enumerable)
  2. ES6的class默认开启严格模式,不能修改
  3. ES6的class声明不存在提升
  4. ES6的class可以部署同步或异步的Generator函数

静态成员

静态属性和静态方法,与实例属性、实例方法的区别,仅仅是前面有static的关键字。

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
class Popup {
_title = '';
_content ='';
_zIndex = 0;

static baseZIndex = 10;// 静态成员前面有static关键字
constructor({title = '', content = '', zIndex = undefined} = {}) {
// ...
this._title = title;
this._content = content;
if(!zIndex) {
this._zIndex = Popup.getZIndex(); // 通过类名访问静态方法
}

}

show() {

}

hide() {

}

set title(title) {
this._title = title;
}

get title() {
return this._title;
}

static getZIndex() {
return this.baseZIndex++;
}
static resetZIndex() {
this.baseZIndex-=1;
}
}

在静态方法内部,this指向的是类本身,而不是类实例。 静态属性,只需要在属性前面加上static关键字,就能部署到跟静态方法并列的位置。 静态方法可以通过类名来调用。

因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性法的前面,加上static关键字。

所以staic声明的静态属性目前来说还是不规范的,chrome浏览器倒是已经支持了。

私有属性的提案

由于ES6并没有引入访问修饰符,所以暂时没有标准方式来实现私有属性。目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。注意是属性名,包括静态属性、静态方法、实例属性、实例方法,都可以通过#号来实现私有。

1
2
3
4
5
6
7
8
9
10
class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}

上面代码中,#count就是私有属性,只能在类的内部使用(this.#count)。如果在类的外部使用,就会报错。

私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性。

1
2
3
4
5
6
7
8
class Foo {
#privateValue = 42;
static getPrivateValue(foo) {
return foo.#privateValue;
}
}

Foo.getPrivateValue(new Foo()); // 42

上面代码允许从实例foo上面引用私有属性。

私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class FakeMath {
static PI = 22 / 7;
static #totallyRandomNumber = 4;

static #computeRandomNumber() {
return FakeMath.#totallyRandomNumber;
}

static random() {
console.log('I heard you like random numbers…')
return FakeMath.#computeRandomNumber();
}
}

FakeMath.PI // 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
// 4
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错

new.target 属性

该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

需要注意的是,子类继承父类时,new.target会返回子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}

class Square extends Rectangle {
constructor(length) {
super(length, width);
}
}

var obj = new Square(3); // 输出 false

上面代码中,new.target会返回子类。

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类,就像java中的抽象类一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}

class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}

var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确