前言

重点内容

  • 继承的作用
  • 成员的重写
  • 里氏替换原则
  • instanceof
  • 单根性
  • 传递性

参考资料

  • TS 官方文档 instanceof:链接

    notes

继承的作用

继承可以描述类与类之间的关系。

image.png 坦克、玩家坦克、敌方坦克 玩家坦克是坦克 —— 「玩家坦克类」继承「坦克类」 敌方坦克是坦克 —— 「敌方坦克类」继承「坦克类」

如果 A 和 B 是都是「类」,并且可以描述为「A是B」,则 A 和 B 形成继承关系,它们之间的相对关系可以描述为:

  • B 是父类,A 是子类
  • B 派生 A,A 继承自 B
  • B 是 A 的基类,A 是 B 的派生类

如果 A 继承自 B,则 A 中自动拥有 B 中的所有成员。

  1. export class Tank {
  2. x: number = 0;
  3. y: number = 0;
  4. }
  5. export class PlayerTank extends Tank {}
  6. export class EnemyTank extends Tank {}
  7. const p = new PlayerTank(); // 己方坦克
  8. const e = new EnemyTank(); // 敌方坦克

image.png

1-2. 类的继承 - 图3

成员的重写

成员
成员属性 + 成员方法

重写(override)
子类中覆盖父类的成员
无论是属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配。

this
在继承中,this 的指向是动态的。
在调用方法时,根据具体的调用者确定 this 指向。

super
super 关键字,指向父类
在子类方法中,可以使用 super 关键字读取父类成员

重写是 override,并不是 overwrite

  1. export class Tank {
  2. x: number = 0;
  3. y: number = 0;
  4. shoot() {
  5. console.log("Tank 发射子弹");
  6. }
  7. }
  8. export class PlayerTank extends Tank {
  9. x: number = 20;
  10. y: number = 20;
  11. shoot() {
  12. console.log("PlayerTank 发射子弹");
  13. }
  14. }
  15. export class EnemyTank extends Tank {
  16. shoot() {
  17. console.log("EnemyTank 发射子弹");
  18. }
  19. }
  20. const p = new PlayerTank(); // 己方坦克
  21. const e = new EnemyTank(); // 敌方坦克
  22. console.log("PlayerTank 属性成员 x、y 已重写", p.x, p.y);
  23. console.log("EnemyTank 属性成员 x、y 未重写", e.x, e.y);
  24. p.shoot();
  25. e.shoot();
  1. Object.defineProperty(exports, "__esModule", {
  2. value: true
  3. });
  4. exports.EnemyTank = exports.PlayerTank = exports.Tank = void 0;
  5. class Tank {
  6. constructor() {
  7. this.x = 0;
  8. this.y = 0;
  9. }
  10. shoot() {
  11. console.log("Tank 发射子弹");
  12. }
  13. }
  14. exports.Tank = Tank;
  15. class PlayerTank extends Tank {
  16. constructor() {
  17. super(...arguments);
  18. this.x = 20;
  19. this.y = 20;
  20. }
  21. shoot() {
  22. console.log("PlayerTank 发射子弹");
  23. }
  24. }
  25. exports.PlayerTank = PlayerTank;
  26. class EnemyTank extends Tank {
  27. shoot() {
  28. console.log("EnemyTank 发射子弹");
  29. }
  30. }
  31. exports.EnemyTank = EnemyTank;
  32. const p = new PlayerTank();
  33. const e = new EnemyTank();
  34. console.log("PlayerTank 属性成员 x、y 已重写", p.x, p.y);
  35. console.log("EnemyTank 属性成员 x、y 未重写", e.x, e.y);
  36. p.shoot();
  37. e.shoot();

image.png

⚠️ 重写的时候,得确保成员的类型保持不变。
x: number = 20
x: string = "20"
确保逻辑是正确的,当我们在重写成员时,修改成员的属性,那么逻辑是说不通的。

  1. 坦克的 x 坐标是数字
  2. 己方坦克是坦克
  3. 己方坦克的 x 坐标是数字

「条件1」+「条件2」推导出「结论3」

export class Tank {
  name: string = "普通坦克";

  sayHello() {
    console.log(`my name is ${this.name}`);
  }
}

export class PlayerTank extends Tank {
  name: string = "PlayerTank";
}

export class EnemyTank extends Tank {
  name: string = "EnemyTank";
}

const p = new PlayerTank(); // 己方坦克
const e = new EnemyTank(); // 敌方坦克
p.sayHello();
e.sayHello();

image.png

sayHello 方法中的 this 指向是动态的,它指向当前调用 sayHello 的具体实例对象。

export class Tank {
  name: string = "普通坦克";

  sayHello() {
    console.log(`my name is ${this.name}`);
  }
}

export class PlayerTank extends Tank {
  name: string = "PlayerTank";

  testSuper () {
    super.sayHello();
    this.sayHello();
  }
}

export class EnemyTank extends Tank {
  name: string = "EnemyTank";
}

const p = new PlayerTank(); // 己方坦克
p.testSuper();
// => my name is PlayerTank
// => my name is PlayerTank

**super.sayHello()****this.sayHello()**
此时,它们两个的效果是一致的,子类没有 sayHello 方法,会使用继承自父类的 sayHello 方法。

若在子类中重写了 sayHello 方法,那么 this.sayHello 指向子类中重写后的 sayHello,而 super.sayHello 依旧指向父类的 sayHello。

export class Tank {
  name: string = "普通坦克";

  sayHello() {
    console.log(`my name is ${this.name}`);
  }
}

export class PlayerTank extends Tank {
  name: string = "PlayerTank";

  sayHello() {
    console.log(`我的名字是 ${this.name}`);
  }

  testSuper () {
    super.sayHello();
    this.sayHello();
  }
}

export class EnemyTank extends Tank {
  name: string = "EnemyTank";
}

const p = new PlayerTank(); // 己方坦克
p.testSuper();
// => my name is PlayerTank
// => 我的名字是 PlayerTank

类型匹配

鸭子辨型法
子类的对象,始终可以赋值给父类

在面向对象中,这种现象叫做「里氏替换原则」
如果需要判断一个变量的具体子类类型,可以使用关键字 instanceof

里氏替换原则 image.png 译文:派生类对象可以在程序中代替其基类对象。

export class Tank {
  name: string = "普通坦克";
}

export class PlayerTank extends Tank {
  name: string = "PlayerTank";
  life: number = 5; // 默认 5 条命
}

export class EnemyTank extends Tank {
  name: string = "EnemyTank";
}

let p: Tank = new PlayerTank(); // 己方坦克
p.life; // ×
p.name; // √

**const p: Tank = new PlayerTank()**
己方坦克是坦克,所以己方坦克可以赋值给坦克,配型匹配是正确的。

**p.lefe**
虽然从上述代码中,我们可以明显得知 p 是 PlayerTank 类的实例,PlayerTank 类中有 life 成员,但是我们无法访问 life。
分析:因为我们在声明变量 p 时,将其强制约束为一个 Tank 类型,所以它只能访问 Tank 身上有的东西。

试想一下,如果 ts 让我们能够访问 p.life,那么会造成什么后果?

上述代码,现在看似没有问题,但如果在访问 p.life 之前,给 p 重新赋值:p = new EnemyTank()
首先明确一点,这么赋值是允许的,类型匹配也能通过,但是 EnemyTank 类身上并没有我们需要的属性 life,这就会导致后续一系列的问题。

image.png

**p.lefe**
这么写错误的本质原因就是因为 ts 无法确认,p 究竟是不是
PlayerTank 类的实例(即便一开始创建时是,但是无法确保之后是否会变)

为此,我们可以借助一个关键字 instanceof 来帮我们限制 p 可能的类型。

export class Tank {
  name: string = "普通坦克";
}

export class PlayerTank extends Tank {
  name: string = "PlayerTank";
  life: number = 5; // 默认 5 条命
}

export class EnemyTank extends Tank {
  name: string = "EnemyTank";
}

let p: Tank = new PlayerTank(); // 己方坦克
if (p instanceof PlayerTank) {
  p.life; // √
  p.name; // √
}

image.png

image.png

protected 修饰符

我们前面学习过的一些修饰符:

  • 只读修饰符:readonly
  • 访问权限修饰符:privatepublicprotected
访问修饰符 描述
public 公开的
默认的访问修饰符,所有位置均可访问
private 私有的
只有在类中可以访问
protected 受保护的
只能在自身和子类中访问

这些修饰符都是给我们写代码提供限制和约束的,它们都不会出现在编译结果中。

重写、修饰符

在重写时,我们通常会将访问修饰符原封不动地带上,即便这并非强制要求的。

若我们在子类中,重写了某个成员,但是访问修饰符和父类不一致,这是被允许的。

export class Tank {
  protected name: string = "普通坦克";
}

export class PlayerTank extends Tank {
  name: string = "PlayerTank";
  life: number = 5; // 默认 5 条命
}

export class EnemyTank extends Tank {
  name: string = "EnemyTank";
}

let p: Tank = new PlayerTank(); // 己方坦克
p.name; // ×
if (p instanceof PlayerTank) {
  p.name; // √
}

image.png

export class Tank {
  protected name: string = "普通坦克";
}

export class PlayerTank extends Tank {
  protected name: string = "PlayerTank";
  life: number = 5; // 默认 5 条命
}

export class EnemyTank extends Tank {
  name: string = "EnemyTank";
}

let p: Tank = new PlayerTank(); // 己方坦克
p.name; // ×
if (p instanceof PlayerTank) {
  p.name; // √
}

通常重写某个成员时,会将访问修饰符一起带上,并且和父类保持一致。

image.png

export class Tank {
  protected name: string = "普通坦克";
}

export class PlayerTank extends Tank {
  protected name: string = "PlayerTank";
}

export class EnemyTank extends Tank {
  sayHello() {
    console.log(`my name is ${this.name}`);
  }
}

let e: Tank = new EnemyTank(); // 己方坦克
if (e instanceof EnemyTank) e.sayHello(); // => my name is 普通坦克

单根性和传递性

面向对象的一些特点:

  • 单根性:每个类最多只能拥有一个父类
  • 传递性:如果 A 是 B 的父类,并且 B 是 C 的父类,那么可以认为 A 也是 C 的父类
export class Tank {
  name: string = "普通坦克";
  sayHello() {
    console.log(`my name is ${this.name}`);
  }
}

export class PlayerTank extends Tank {
  name: string = "PlayerTank";
}

export class EnemyTank extends Tank {
  name: string = "EnemyTank";
  bloodVolume: number = 1; // 血量
}

export class BossTank extends EnemyTank {
  bloodVolume: number = 10; // 血量
}

const b = new BossTank(); // Boss 坦克

Tank、EnemyTank 都是 BossTank 的父类,BossTank 的实例默认就具有它们身上的成员。

b.sayHello();
console.log(b.name);
console.log(b.bloodVolume);

image.png