Appearance
类继承
像其他具有面向对象特性的语言一样,JavaScript 中的类可以继承自基类。
implements 子句
你可以使用一个 implements 子句来检查一个类,是否满足了一个特定的接口。如果一个类不能正确地实现它,就会发出一个错误。
typescript
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
pong() {
console.log("pong!");
}
}

类也可以实现多个接口,例如 class C implements A, B {}
注意事项
重要的是要明白, implements 子句只是检查类是否可以被当作接口类型来对待。它根本不会改变类的类型或其方法。一个常见的错误来源是认为 implements 子句会改变类的类型--它不会!它不会。
typescript
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) {
// any:注意这里没有错误
return s.toLowercse() === "ok";
}
}

在这个例子中,我们也许期望 s 的类型会受到 check 的 name: string 参数的影响。事实并非如此--实现子句并没有改变类主体的检查方式或其类型的推断。
同样地,实现一个带有可选属性的接口并不能创建该属性。
typescript
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10;

extends 子句
类可以从基类中扩展出来。派生类拥有其基类的所有属性和方法,也可以定义额外的成员。
typescript
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog();
// 基类的类方法
d.move();
// 派生的类方法
d.woof(3);
重写方法
派生类也可以覆盖基类的一个字段或属性。你可以使用 super. 语法来访问基类方法。注意,因为 JavaScript 类是一个简单的查找对象,没有 "超级字段 "的概念。
TypeScript 强制要求派生类总是其基类的一个子类型。
例如,这里有一个合法的方法来覆盖一个方法。
typescript
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
派生类遵循其基类契约是很重要的。请记住,通过基类引用来引用派生类实例是非常常见的(而且总是合法的!)
typescript
// 通过基类引用对派生实例进行取别名
const b: Base = d;
// 没问题
b.greet();
如果 Derived 没有遵守 Base 的约定怎么办?
typescript
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// 使这个参数成为必需的
greet(name: string) {
console.log(`Hello, ${name.toUpperCase()}`);
}
}

如果我们不顾错误编译这段代码,这个样本就会崩溃:
typescript
const b: Base = new Derived();
// 崩溃,因为 "名称 "将是 undefined。
b.greet();

初始化顺序
在某些情况下,JavaScript 类的初始化顺序可能会令人惊讶。让我们考虑一下这段代码:
typescript
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// 打印 "base", 而不是 "derived"
const d = new Derived();
这里发生了什么? 按照 JavaScript 的定义,类初始化的顺序是:
- 基类的字段被初始化
- 基类构造函数运行
- 派生类的字段被初始化
- 派生类构造函数运行
这意味着基类构造函数在自己的构造函数中看到了自己的 name 值,因为派生类的字段初始化还没有运行。
继承内置类型
注意:如果你不打算继承 Array、Error、Map 等内置类型,或者你的编译目标明确设置为 ES6/ES2015 或以上,你可以跳过本节。
在 ES2015 中,返回对象的构造函数隐含地替代了 super(...) 的任何调用者的 this 的值。生成的构造函数代码有必要捕获 super(...) 的任何潜在返回值并将其替换为 this 。
因此,子类化 Error 、 Array 等可能不再像预期那样工作。这是由于 Error 、 Array 等的构造函数使用 ECMAScript 6 的 new.target 来调整原型链;然而,在 ECMAScript 5 中调用构造函数时,没有办法确保 new.target 的值。其他的下级编译器一般默认有同样的限制。
对于一个像下面这样的子类:
typescript
class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message;
}
}
你可能会发现:
- 方法在构造这些子类所返回的对象上可能是未定义的,所以调用 sayHello 会导致错误。

- instanceof 将在子类的实例和它们的实例之间被打破,所以 (new MsgError())instanceof MsgError 将返回 false 。

作为建议,你可以在任何 super(...) 调用后立即手动调整原型。
typescript
class MsgError extends Error {
constructor(m: string) {
super(m);
// 明确地设置原型。
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
return "hello " + this.message;
}
}

然而, MsgError 的任何子类也必须手动设置原型。对于不支持 Object.setPrototypeOf 的运行时,你可以使用 proto 来代替。
不幸的是,这些变通方法在 Internet Explorer 10 和更早的版本上不起作用。我们可以手动将原型中的方法复制到实例本身(例如 MsgError.prototype 到 this ),但是原型链本身不能被修复。