Skip to content

类继承

像其他具有面向对象特性的语言一样,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 ),但是原型链本身不能被修复。