ES6 Class基本用法

简介

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

基本概念

1. constructor方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。理所当然,它就相当于java中的构造方法。

2. 类的实例

类必须用new关键字创建出来,否则报错。

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

// 报错
var point = Point(2, 3);

// 正确
var point = new Point(2, 3);

3. 取值函数(getter)和存值函数(setter)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

4. Class 表达式

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

let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在Class外部,这个类只能用MyClass引用。
如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

1
const MyClass = class { /* ... */ };

采用 Class 表达式,可以写出立即执行的 Class。

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(); // "张三"

5. this的指向

类的方法内部如果含有this,它默认指向类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}

print(text) {
console.log(text);
}
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以this实际指向的是undefined),从而导致找不到print方法而报错。
一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

1
2
3
4
5
6
7
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}

// ...
}

另一种解决方法是使用箭头函数。

1
2
3
4
5
6
7
8
class Obj {
constructor() {
this.getThis = () => this;
}
}

const myObj = new Obj();
myObj.getThis() === myObj // true

箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。

6. 静态方法

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

1
2
3
4
5
6
7
8
9
10
11
class Foo {
static classMethod() {
return 'hello';
}
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

7. 实例属性的新写法

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

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 IncreasingCounter {
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}

//也可以这样写
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}

8. 静态属性

静态属性指的是 Class本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

1
2
3
4
5
class Foo {
}

Foo.prop = 1;
Foo.prop // 1

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
static myStaticProp = 42;

constructor() {
console.log(MyClass.myStaticProp); // 42
}
}


// 老写法
class Foo {
// ...
}
Foo.prop = 1;

// 新写法
class Foo {
static prop = 1;
}

9. 私有方法和私有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Foo {
#a;
#b;
constructor(a, b) {
this.#a = a;
this.#b = b;
}
#sum() {
return #a + #b;
}
printSum() {
console.log(this.#sum());
}
}

上面代码中,#sum()就是一个私有方法;#a就是一个私有属性。由于井号#是属性名的一部分,使用时必须带有#一起使用,所以#a和a是两个不同的属性。
私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性。

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

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

10. new.target 属性

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}

// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错

上面代码确保构造函数只能通过new命令调用。

Class 内部调用new.target,返回当前 Class。

1
2
3
4
5
6
7
8
9
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}

var obj = new Rectangle(3, 4); // 输出 true

需要注意的是,子类继承父类时,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

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

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); // 正确

注意,在函数外部,使用new.target会报错。

参考

ECMAScript 6 入门-Class的基本用法