ES6之class

类声明

cheat sheet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PersonClass {
// equivalent of the PersonType constructor
constructor(name) {
this.name = name;
}
// equivalent of PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}
let person = new PersonClass("Nicholas");
person.sayName(); // outputs "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // function
console.log(typeof PersonClass.prototype.sayName); // function

类属性不可被赋予新值,在之前的实例中,PersonClass.prototype就是这样一个只可读的类属性;

类语法

类与ES5类型(new Person)之间有诸多相识之初,但也有差异:

  1. 函数声明可以被提升,而类声明与let声明类似,不能被提升;真正执行声明语句之前,它们会一直存在于临时死区
  2. 类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式执行;
  3. 在自定义类型中,需要通过Object.defineProperty方法手工指定某个方法不可枚举;而在类中,所有方法都是不可枚举的;
  4. 每个类都有一个名为[[Construct]]内部方法,通过关键字new调用那些不含有[[Construct]]的方法会导致程序抛出错误;
  5. 使用除关键字new以外的方法调用类的构造函数会导致程序抛出错误;
  6. 在类中修改类名会导致程序报错;

下面是ES5来实现上面例子类PersonClass的等价代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let PersonType2 = (function() { // 1 临时死区
'use strict'; // 2 严格模式
const PersonType2 = function(name) { // 6 内部类名不可修改
if(typeof new.target === 'undefined') { // 5 只能使用new
throw new Error('必须通过关键字new来调用构造函数');
}
this.name = name;
}
Object.defineProperty(PersonType2.prototype, sayName, {
value: function() {
if(typeof new.target !== 'undefined') { // 4 不能使用new
throw Error('不能使用关键字new来调用该方法');
}
console.log(this.name);
},
enumerable: false,// 3 不可枚举
writable: true,
configurable: true,
});
return Persontype2;
}())

上述1和6可以看下面这个例子:

1
2
3
4
5
6
class Foo {
constructor() {
Foo = "bar";// throws an error when executed...
} }
// but this is okay after the class declaration
Foo = "baz";

类表达式

声明表达式

1
2
3
let PersonClass = class {
...
}

命名表达式

1
2
3
let PersonClass = class PersonClass2 {
...
}

在JS引擎中,类表达式的实现与类声明稍有不同。对于类声明来说,通过let定义的外部绑定与通过const定义的内部绑定具有相同的名称;而命名表达式通过const定义名称,从而PersonClass2只能在类的内部使用;

一等公民的类

在程序中,一等公民是指一个可以传入函数,可以从函数返回,并且可以复制给变量的值。JS函数是一等公民,ES6中把类也设计为一等公民。

作为参数传入函数:

1
2
3
4
5
function createObject(classDef) {
return new classDef();
}
let obj = createObject(class {...});

立即调用类构造函数:

1
2
3
let person = new class {
...
}('Hi');

访问器属性

类也支持直接在原型上定义访问器属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
} }
var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor);
console.log("set" in descriptor);
console.log(descriptor.enumerable);
// true
// true
// false

可计算成员名称

类方法和访问器属性也支持使用可计算名称;

1
2
3
4
5
6
7
8
9
10
11
let methodName = "sayName";
class PersonClass {
constructor(name) {
this.name = name;
}
[methodName]() {
console.log(this.name);
}
};
let me = new PersonClass("Nicholas");
me.sayName(); // "Nicholas"

生成器方法

在类中可以在方法名称前附加一个星号来定义生成器;

1
2
3
4
5
6
7
8
9
class MyClass {
*createIterator() {
yield 1;
yield 2;
yield 3;
}
}
let instance = new MyClass();
let iterator = instance.createIterator();

静态成员

ES6中类简化了创建静态成员的工程,在方法或访问器属性前面使用正式的静态注释即可;

1
2
3
4
5
6
7
8
class PersonClass {
constructor() {
this.name = name;
}
static create() {
...
}
}

不可在实例中访问静态成员,必须要直接在类中访问静态成员

继承与派生

ES6中使用extends关键字可以创造继承类;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// equivalent of Rectangle.call(this, length, length)
super(length, length);
}
}
var square = new Square(3);
console.log(square.getArea());// 9
console.log(square instanceof Square);// true
console.log(square instanceof Rectangle);// true

上述代码创造了一个Square继承Rectangle类;Square被称为派生类;注意:如果在派生类中指定了构造函数则必须要调用super,如果不这样做程序就会报错;如果不使用构造函数,则当创建新的类实例时会自动调用super并传入所有参数

1
2
3
4
5
6
7
8
9
class Square extends Rectangle {
// no constructor
}
// is equivalent to
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}

使用super要注意:

  • 只可在派生类的构造函数中使用super(),如果尝试在非派生类或者函数中使用会抛出错误
  • 在构造函数中访问this前一定要调用super,它负责初试化this,如果在调用super之前尝试访问this会报错
  • 如果不想调用super(),则唯一的方法是让类的构造函数返回一个对象

类方法遮蔽

派生类如果跟父类有同名函数,派生类的方法会覆盖父类的同名方法;如果想在派生类中调用父类的方法,可以super.funcName()这样来调用;

静态成员继承

如果基类有静态成员,那么这些静态成员在派生类中也可调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
static create(length, width) {
return new Rectangle(length, width);
} }
class Square extends Rectangle {
constructor(length) {
// equivalent of Rectangle.call(this, length, length)
super(length, length);
}
}
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
console.log(rect.getArea()); // 12
console.log(rect instanceof Square); // false

新的静态方法create被添加到Rectangle中,继承后的Square.create()与Rectangle.create()行为类似;

派生自表达式的类

ES6最强大的一面或许是从表达式导出类的功能了;只要表达式可以被解析为一个函数并且具有[[Construct]]属性和原型,那么就可以用extends进行派生;extends强大的功能使得类可以继承自任意类型的表达式,从而可以创造更多可能性,例如动态的确定类的继承目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
} }
var x = new Square(3);
console.log(x.getArea()); // 9
console.log(x.serialize()); // "{"length":3,"width":3}"

内建对象的继承

在ES6之前,几乎不可能通过继承方式创建属于自己的数组,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var colors = [];
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined
// trying to inherit from array in ES5
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
} });
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0
colors.length = 0;
console.log(colors[0]) // red

通过上面的代码,可以看出MyArray的行为与内建数组行为不一致;

ES6中可以通过extends来创建自定义数组;

1
2
3
4
5
6
7
8
class MyArray extends Array {
// empty
}
var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 1
colors.length = 0;
console.log(colors[0]); // undefined

在类的构造函数中使用new.target

类中new.targe一般就等于类本身:

1
2
3
4
5
6
7
8
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
} }
// new.target is Rectangle
var obj = new Rectangle(3, 4); // outputs true

但有时其值会不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
} }
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
// new.target is Square
var obj = new Square(3); // outputs false

Square调用Rectangle的构造函数,所以当调用发生时new.target等于Square;每个构造函数都可以根据自身被调用的方式改变自己的行为;例如构造一个抽象基类;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("This class cannot be instantiated directly.")
}
} }
class Rectangle extends Shape {
constructor(length, width) {
super();
this.length = length;
this.width = width;
} }
var x = new Shape(); // throws an error
var y = new Rectangle(3, 4); // no error
console.log(y instanceof Shape); // true

上例子中抽象基类Shape不能直接被实例化,只能通过派生的类来继承;