ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
类定义
// 类声明
class Person {}
// 类表达式
const Animal = class {};
函数受函数作用域限制,而类受块作用域限制
类的构成
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的。空的类定义照样有效。编程风格必须类名的首字母要大写,以区别于通过它创建的实例
class Baz {
// 有构造函数的类,有效
constructor() {}
// 有获取函数的类,有效
get myBaz() {}
// 有静态方法的类,有效
static myQux() {}
}
类构造函数
constructor 关键字用于在类定义块内部创建类的构造函数。
class Person {
constructor(name) {
console.log(arguments.length);
this.name = name || null;
}
}
let p3 = new Person('Jake');
console.log(p3.name); // Jake
类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果
不使用 new 调用,那么就会以全局的 this(通常是 window)作为内部对象。
使用 instanceof 操作符检查一个对象与类构造函数,以确定这个对象是不是类的实例。
类本身在使用 new 调用时就会被当成构造函数。重点在于,类中定义的 constructor 方法不会被当成构造函数,在对它使用
instanceof 操作符时会返回 false。
class Person {}
let p1 = new Person();
console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Person.constructor); // false
let p2 = new Person.constructor();
console.log(p2.constructor === Person); // false
console.log(p2 instanceof Person); // false
console.log(p2 instanceof Person.constructor); // true
实例、原型和类成员
实例成员
每次通过new调用类标识符时,都会执行类构造函数。在这个函数内部,可以为新创建的实例(this)添加“自有”属性。
class Person {
constructor() {
// 这个例子先使用对象包装类型定义一个字符串
// 为的是在下面测试两个对象的相等性
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
}
let p1 = new Person(),
p2 = new Person();
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
原型方法与访问器
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。
可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据:
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance');
}
locate() {
console.log('prototype');
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
静态类方法
静态类成员在类定义中使用 static 关键字作为前缀。在静态成员中,this 引用类自身。
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate(); // instance, Person {}
Person.prototype.locate(); // prototype, {constructor: ... }
Person.locate(); // class, class Person {}
非函数原型和类成员
虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加:
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
// 在类上定义数据成员
Person.greeting = 'My name is';
// 在原型上定义数据成员
Person.prototype.name = 'Jake';
let p = new Person();
p.sayName(); // My name is Jake
迭代器与生成器方法
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'J-Dog';
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}
let jobIter = Person.createJobIterator();
console.log(jobIter.next().value); // Butcher
console.log(jobIter.next().value); // Baker
console.log(jobIter.next().value); // Candlestick maker
let p = new Person();
let nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // J-Dog
// 因为支持生成器方法,所以可以通过添加一个默认的迭代器
继承
ECMAScript 6 新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链。
extends 关键字:可以继承任何拥有[[Construct]]和原型的对象
class Vehicle {}
// 继承类
class Bus extends Vehicle {}
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
function Person() {}
// 继承普通构造函数
class Engineer extends Person {}
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true
构造函数、HomeObject 和 super()
通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅
限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。
super():
class Vehicle {
constructor() {
this.hasEngine = true;
}
}
class Bus extends Vehicle {
constructor() {
// 不要在调用 super()之前引用 this,否则会抛出 ReferenceError
super(); // 相当于 super.constructor()
console.log(this instanceof Vehicle); // true
console.log(this); // Bus { hasEngine: true }
}
}
new Bus();
super注意事项
super 只能在派生类构造函数和静态方法中使用。
class Vehicle {
constructor() {
super();
// SyntaxError: 'super' keyword unexpected
}
}
不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法。
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(super);
// SyntaxError: 'super' keyword unexpected here
}
}
调用 super()会调用父类构造函数,并将返回的实例赋值给 this。
class Vehicle {}
class Bus extends Vehicle {
constructor() {
super();
console.log(this instanceof Vehicle);
}
}
new Bus(); // true
super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {
constructor(licensePlate) {
super(licensePlate);
}
}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的
参数。
class Vehicle {
constructor(licensePlate) {
this.licensePlate = licensePlate;
}
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' }
在类构造函数中,不能在调用 super()之前引用 this。
class Vehicle {}
class Bus extends Vehicle {
constructor() {
console.log(this);
}
}
new Bus();
// ReferenceError: Must call super constructor in derived class
// before accessing 'this' or returning from derived constructor
如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回
一个对象。
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
constructor() {
super();
}
}
class Van extends Vehicle {
constructor() {
return {};
}
}
console.log(new Car()); // Car {}
console.log(new Bus()); // Bus {}
console.log(new Van()); // {}
抽象基类
这个就是与Java中的抽象类差不多,可以供其他类继承,但是本身不会被实例化。
new.target
// 抽象基类
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
}
}
// 派生类
class Bus extends Vehicle {}
new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在
调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法:
// 抽象基类
class Vehicle {
constructor() {
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
if (!this.foo) {
throw new Error('Inheriting class must define foo()');
}
console.log('success!');
}
}
// 派生类
class Bus extends Vehicle {
foo() {}
}
// 派生类
class Van extends Vehicle {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()
继承内置类型
class SuperArray extends Array {
shuffle() {
// 洗牌算法
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
}
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // [3, 1, 4, 5, 2]
类混入
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
}
};
let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};
class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
通过写一个辅助函数,可以把嵌套调用展开:
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
}
};
let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
这种方式已被很多开发框架弃用,组合好于混入,提供极大的灵活性。