Javscript中的类

/post/js-class article cover image

类定义

主要有两种: 类声明和类表达式(使用class关键字)

js
// 类声明
class Person {}

// 类表达式
const Person = class {};

<Callout> 与函数表达式类似,类表达式在它们被求值前也不能引用。不过,与函数定义不同的是,虽然函数声明可以提升,但类定义 </Callout>

js
console.log(foo); // foo() {}
function foo() {}

console.log(ClassDeclaration) // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration{}
console.log(ClassDeclaration) // class ClassDeclaration()

类的构成

js
// 空类定义,有效
class Foo {}

// 有获取函数的类,有效
class Baz {
	get myBaz() {}
}

// 有静态方法的类,有效
class Qux {
	static myQux() {}
}

<Callout> 类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称字符串。但不能在类表达式作用域外部访问这个标识符。 </Callout>

ts
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

实例原型和类成员

js
// 实例成员
class Person {
	constructor() {
		this.name = 'xxx';
	}
}

const p1 = new Person();
js
// 原型方法
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
js
// 访问器
class Person {
	get name() {
		return this.name;
	}

	set name(newName) {
		this.name = newName;
	}
}

const p = new Person();
p.name = 'ahoho';
console.log(p.name);
js
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 {}
js
// 静态方法非常适合作为实例工厂
class Person {
	construcor(age) {
		this.age = age;
	}

	sayAge() {
		console.log(this.age);
	}

	static create() {
		return new Person(17);
	}
}

console.log(Person.create());
js
// 非函数原型和类成员
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
js
// 迭代器与生成器方法
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`
js
// 继承
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 是不是抽象基类,可以阻止对抽象基类的实例化:

js
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 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:

js
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访问器这个访问器决定在创建返回的实例时使用的类

js
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>

js
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 组合到这个超类中。实现这种模式有不同的策略。 一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类。这些组合函数可以连缀调用,最终组合成超类表达式:

js
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