ES6引入的class关键字具有正式定义类的能力。类class是ES中新的基础语法糖结构。
虽然ES6类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。
引入class关键字为了解决使用各种实现继承的代码非常冗长和混乱的问题。
类定义
-
定义类的主要方式:
- 类声明
class Person {}
- 类表达式
let Person = Class {}
- 类声明
-
与函数表达式类似,类表达式在它们被求值之前不能引用。函数声明可以提升,但类定义不能提升。
-
函数受函数作用域限制,类受块作用域限制:
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
FunctionDeclaration; // FunctionDeclaration () {}
ClassDeclaration; // ReferenceError: ClassDeclaration is not defined
-
类的构成
- 构造函数constructor
- 实例方法
- 获取函数get
- 设置函数set
- 静态类方法static
-
空类照样有效。默认情况下,类定义中的代码都在严格模式下执行。
// 空类,有效
class Foo {}
// 有构造函数的类,有效
class Foo {
constructor() {}
}
// 有获取函数,有效
class Foo {
get mybaz() {}
}
// 有静态方法的类,有效
class Foo {
static myFoo() {}
}
- 类表达式的名称是可选的。在把类表达式赋值给变量之后,可以通过name属性取得类表达式的名称字符串。但不能在类表达作用域外部访问这个标识符。
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}
let p = new Person();
p.indentify(); // 'PersonName' 'PersonName'
Person.name; // PersonName
PersonName; // ReferenceError: PersonName is not defined
类构造函数
实例、原型和类成员
继承
- ES6新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧是使用的是原型链。
- 继承基础
- ES6类支持单继承。使用extends关键字,就可以继承任何拥有
[[Constructor]]
和原型的对象。很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)
// 继承类
class Vehicle {}
class Bus extends Vehicle {};
let b = new Bus();
b instanceof Bus; // true
b instanceof Vehicle; // true
function Person () {}
// 继承普通构造函数
class E extends Person {}
let e = new E();
e instanceof E; // true
e instanceof Person; // true
- 派生类都会通过原型链访问到类和原型上定义的方法。this的值会反映调用相应方法的实例或类:
class Vehicle {
identifyPrototype(id) {
console.log(id, this);
}
static identifyClass(id) {
console.log(id, this);
}
}
class Bus extends vehicle {}
let v = new Vehicle();
let b = new Bus();
b.idenetifyPrototype('bus'); // bus, Bus {}
v.identifyPrototype('vehicle'); // vehicle, Vehicle {}
Bus.identifyClass('bus'); // bus, class Bus {}
Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}
- 【注意】extends关键字也可以在类表达式中使用,因此
let Bar = class extends Foo{}
是有效的语法。
- 构造函数、HomeObject和super()
- 派生类的方法可以通过super关键字引用它们的原型。
- 这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
- 在类构造函数中使用super可以调用父类构造函数。
class V {
constructor() {
this.hasEngine = true;
}
}
class Bus extends V {
constructor() {
// 不要在调用super()之前引用this,否则会抛出ReferenceError
super(); // 相等于super.constructor()
console.log(this instanceof V); // true
console.log(this); // Bus (hasEngine: true)
}
}
new Bus()
- 在静态方法中可以通过super调用继承的类上定义的静态方法:
class V {
static identify() {
console.log('vehicle');
}
}
class Bus extends V {
static identify() {
super.identify();
}
}
Bus.identify(); // vehicle
-
ES6给类构造函数和静态方法添加了内部特性
[[HomeObject]]
,这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的。而且只能在JavaScript引擎内部访问。super始终会定义为[[HomeObject]]
的原型 -
在使用super时要注意以下几个问题:
- 1.super只能在派生类构造函数和静态方法中使用。
- 2.不能单独引用super关键字,要么用它调用构造函数,要么用它引用静态方法。
- 3.调用super()会调用父类构造函数,并将返回的实例赋值给this。
- 4.super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。
- 5.如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数。
- 6.在类构造函数中,不能在调用super之前引用this。
- 7.如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象。
// 1. class V { constructor() { super(); // SyntaxErrror: 'super' keyword unexpected } } // 2. class V {} class B extends V { constructor() { console.log(super); // SyntaxError: 'super' keyword unepexted here } } // 3. class V {} class B extends V { constructor() { super(); console.log(this instanceof V); } } new Bus(); // true // 4. class V { constructor(licensePlate) { this.licensePlate = licensePlate; } } class B extends V { constructor(licensePlate) { super(licensePlate); } } console.log(new B('1337H4X')); // B {licensePlate: '1337H4X'} // 5. class Vehicle { constructor(li) { this.li = li; } } class B extends V {} new B('1337H4X'); // B {li: '1337H4X'} // 6. class V {} class B extends V { constructor() { console.log(this); } } new B(); // Error // 7. class V {} class Car extends V {} class Bus extends V { constructor() { super(); } } class Van extends V { constructor() { return {}; } } new Car(); // Car {} new Bus(); // Bus {} new Van(); // {}
- 抽象基类
- 有可能需要定义一个类,它可供其他类继承,但本身不会被实例化。虽然ES没有专门支持这种类的语法,但通过
new.target
也能容易实现。 - new.target保存通过new关键字调用的类或函数。
- 通过在实例化时,监测new.target是不是抽象基类,可以阻止对抽象基类的实例化:
// 抽象基类
class V {
constructor() {
console.log(new.target);
if (new.target === V) {
throw new Error('V cannot be directly instantiated');
}
}
}
// 派生类
class Bus extends V {}
new Bus(); // class Bus() {}
new V(); // class V {}
// Error: V cannot be directly instantiated
- 通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过this关键字来检查相应的方法:
// 抽象基类
class V {
constructor() {
if (new.target === V) {
throw Error('V cannot be directly instantiated');
}
if (!this.foo) {
throw Error('Inheriting class must define foo()');
}
console.log('success!');
}
}
// 派生类
class Bus extends V {
foo() {}
}
// 派生类
class Van extends V {}
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()
- 继承内置类型
- ES6类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型。
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);
a instanceof Array; // true
a instanceof SuperArray; // true
a; // [1, ,2, 3, 4, 5]
a.shuffle(); // [3, 1, 4, 5, 2]
-
有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的:
-
如果想覆盖这个默认行为,则可以覆盖Symbol.species访问器,这个访问器决定在创建返回的实例时使用的类:
class SuperArray extends Array {
static get [Symbol.species] () {
return Array;
}
}
let a1 = new SuperArray(1, 2, 3, 4, 5);
let a2 = a1.filter(x => !!(x % 2));
// !!操作符等于否定之否定,即返回结果本身
a1; // [1,2,3,4,5]
a2; // [1,3,5]
a1 instanceof SuperArray; // true
a2 instanceof SuperArray; // false
// 由于改变了get()方法,使得get()返回Array类型,所以a2不再是SuperArray的实例
- 类混入
- 把不同的行为集中到一个类是一种创建的JS模式。
- ES6没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。
-【注意】Object.assign()方法是为混入对象行为而设计的。只有需要混入类的行为时才有必要自己实现混入表达式。如果只有需要混入多个对象的属性,那么使用Object.assign()就可以了。
- 在下面的代码片段中,extends关键字后面是一个JavaScript表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的。这个表达式会在求值类定义时被求值。
class V {}
function getParentClass() {
console.log('evaluated expression');
return V;
}
class Bus extends getParentClass() {}
// 可求值的表达式
let b = new Bus();
b instanceof Bus; // true
b instanceif V; // true
- 混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。如果Person类需要组合A、B、C,则需要某种机制实现B继承A,C继承B,而Person在继承C,从而把A、B、C组合到这个超类中。实现这种模式又不同的策略。
- 一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类/这些组合函数可以连缀调用,最终组合成超类表达式:
class V {}
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(V))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
- 通过写一个辅助函数,可以把嵌套调用展开:
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((acc, current) => current(acc), BaseClass);
}
class Bus2 extends mix(V, FooMixin, BarMixin, BazMixin) {}
let b2 = new Bus2();
b.bar(); // bar
b.baz(); // baz
- 【注意】很多JS框架特别是React已经抛弃了混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。
- 这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。