类定义
主要有两种: 类声明和类表达式(使用class关键字)
// 类声明
class Person {}
// 类表达式
const Person = class {};
<Callout> 与函数表达式类似,类表达式在它们被求值前也不能引用。不过,与函数定义不同的是,虽然函数声明可以提升,但类定义 </Callout>
console.log(foo); // foo() {}
function foo() {}
console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration{}
console.log(ClassDeclaration) // class ClassDeclaration()
类的构成
// 空类定义,有效
class Foo {}
// 有获取函数的类,有效
class Baz {
get myBaz() {}
}
// 有静态方法的类,有效
class Qux {
static myQux() {}
}
<Callout> 类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称字符串。但不能在类表达式作用域外部访问这个标识符。 </Callout>
let Person = class PersonName {
identify() {
console.log(Person.name, PersonName.name);
}
}
let p = new Person();
p.identify(); // PersonName PersonName
console.log(Person.name); // PersonName
console.log(PersonName); // ReferenceError: PersonName is not defined
实例原型和类成员
// 实例成员
class Person {
constructor() {
this.name = 'xxx';
}
}
const p1 = new Person();
// 原型方法
class Person {
constructor() {
this.locate = () => console.log('instance');
}
locate() {
console.log('prototype');
}
// 类方法等同于对象属性,因此可以使用字符串、富豪或计算的值作为键值
[Symbol('symbolKey')]() {
console.log('invoked symbolKey')
}
['computed' + 'Key']() {
console.log('invoked computedKey');
}
}
const p1 = new Person();
p1.locate(); //instance
Person.prototype.locate(); // prototype
// 访问器
class Person {
get name() {
return this.name;
}
set name(newName) {
this.name = newName;
}
}
const p = new Person();
p.name = 'ahoho';
console.log(p.name);
class Person {
constructor() {
// 添加到this的所有内容都会存在于不同的实力上
this.locate = () => ('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('class', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
const p = new Person(); // instance Person()
p.prototype.locate(); // prototype. { constructor: '...'}
Person.locate(); // class, class Person {}
// 静态方法非常适合作为实例工厂
class Person {
construcor(age) {
this.age = age;
}
sayAge() {
console.log(this.age);
}
static create() {
return new Person(17);
}
}
console.log(Person.create());
// 非函数原型和类成员
class Person {
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
// 在类上定义数据成员
Person.greeting = 'My name is';
// 在原型上定义数据成员
Person.prototype.name = 'Jade';
const p = new Person();
p.sayName(); // My name is Jade
// 迭代器与生成器方法
class Person {
// 在原型上定义生成器方法
*createNicknameIterator() {
yield 'Jack';
yield 'Jake';
yield 'Jade';
}
// 在类上定义生成器方法
static *createJobIterator() {
yield 'Butcher';
yield 'Baker';
yield 'Candlestick maker';
}
}
const joblter = Person.createJobIterator();
console.log(joblter.next().value); // Butcher
console.log(joblter.next().value); // Baker
console.log(joblter.next().value); // Candlestick maker
const p = new Person();
const nicknameIter = p.createNicknameIterator();
console.log(nicknameIter.next().value); // Jack
console.log(nicknameIter.next().value); // Jake
console.log(nicknameIter.next().value); // Jade`
// 继承
class Vehicle {
constructor() {
this.className = 'Vehicle';
}
static identify() {
console.log('vehicle');
}
}
// 继承类
class Car extends Vehicle {
// 构造函数
constructor() {
// 1.不要再调用super之前引用this,否则会抛出ReferenceError
// 2.super函数只能在派生类构造函数和静态方法中使用
super(); // 相当于super.constructor()
console.log(this instanceof Vehicle); // true
}
static identify() {
super.identify();
}
}
<Callout> ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。super 始终会定义为[[HomeObject]]的原型 </Callout>
super使用注意事项
- super只能在派生类构造函数和静态方法中使用
- 不能单独引用super关键字,要么用它调用构造函数
- super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
- 如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数
- 在类构造函数中,不能调用super()之前引用this
- 如果在派生类中显示定义了构造函数,则要么必须在其中调用super(),要么在其中返回一个对象
抽象类
有时候可能需要定义这样一个类,它可供其他类继承,但本身不会被实例化。虽然 ECMAScript 没有专门支持这种类的语法 ,但通过 new.target 也很容易实现。new.target 保存通过 new 关键字调用的类或函数。通过在实例化时检测 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
继承内置类型
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);
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]
如果想要覆盖着默认行为则可以覆盖symbolspecies访问器这个访问器决定在创建返回的实例时使用的类
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));
console.log(a1); // [1, 2, 3, 4, 5]
console.log(a2); // [1, 3, 5]
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // false
类混入
把不同类的行为集中到一个类是一种常见的 JavaScript 模式。虽然 ES6 没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。
<Callout> Object.assign()方法是为了混入对象行为而设计的。只有在需要混入类的行为时才有必要自己实现混入表达式。如果只是需要混入多个对象的属性,那么使用Object.assign()就可以了。 </Callout>
class Vehicle {}
function getParentClass() {
console.log('evaluated expression');
return Vehicle;
}
class Bus extends getParentClass() {}
混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。如果 Person 类需要组合 A、B、C,则需要某种机制实现 B 继承 A,C 继承 B,而 Person再继承 C,从而把 A、B、C 组合到这个超类中。实现这种模式有不同的策略。 一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式:
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