前言
重点内容
- 继承的作用
- 成员的重写
- 里氏替换原则
- instanceof
- 单根性
- 传递性
参考资料
- TS 官方文档 instanceof:链接
notes
继承的作用
继承可以描述类与类之间的关系。
坦克、玩家坦克、敌方坦克 玩家坦克是坦克 —— 「玩家坦克类」继承「坦克类」 敌方坦克是坦克 —— 「敌方坦克类」继承「坦克类」
如果 A 和 B 是都是「类」,并且可以描述为「A是B」,则 A 和 B 形成继承关系,它们之间的相对关系可以描述为:
- B 是父类,A 是子类
- B 派生 A,A 继承自 B
- B 是 A 的基类,A 是 B 的派生类
如果 A 继承自 B,则 A 中自动拥有 B 中的所有成员。
export class Tank {
x: number = 0;
y: number = 0;
}
export class PlayerTank extends Tank {}
export class EnemyTank extends Tank {}
const p = new PlayerTank(); // 己方坦克
const e = new EnemyTank(); // 敌方坦克
成员的重写
成员
成员属性 + 成员方法
重写(override)
子类中覆盖父类的成员
无论是属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配。
this
在继承中,this 的指向是动态的。
在调用方法时,根据具体的调用者确定 this 指向。
super
super 关键字,指向父类
在子类方法中,可以使用 super 关键字读取父类成员
重写是 override,并不是 overwrite
export class Tank {
x: number = 0;
y: number = 0;
shoot() {
console.log("Tank 发射子弹");
}
}
export class PlayerTank extends Tank {
x: number = 20;
y: number = 20;
shoot() {
console.log("PlayerTank 发射子弹");
}
}
export class EnemyTank extends Tank {
shoot() {
console.log("EnemyTank 发射子弹");
}
}
const p = new PlayerTank(); // 己方坦克
const e = new EnemyTank(); // 敌方坦克
console.log("PlayerTank 属性成员 x、y 已重写", p.x, p.y);
console.log("EnemyTank 属性成员 x、y 未重写", e.x, e.y);
p.shoot();
e.shoot();
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.EnemyTank = exports.PlayerTank = exports.Tank = void 0;
class Tank {
constructor() {
this.x = 0;
this.y = 0;
}
shoot() {
console.log("Tank 发射子弹");
}
}
exports.Tank = Tank;
class PlayerTank extends Tank {
constructor() {
super(...arguments);
this.x = 20;
this.y = 20;
}
shoot() {
console.log("PlayerTank 发射子弹");
}
}
exports.PlayerTank = PlayerTank;
class EnemyTank extends Tank {
shoot() {
console.log("EnemyTank 发射子弹");
}
}
exports.EnemyTank = EnemyTank;
const p = new PlayerTank();
const e = new EnemyTank();
console.log("PlayerTank 属性成员 x、y 已重写", p.x, p.y);
console.log("EnemyTank 属性成员 x、y 未重写", e.x, e.y);
p.shoot();
e.shoot();
⚠️ 重写的时候,得确保成员的类型保持不变。x: number = 20
✅x: string = "20"
❎
确保逻辑是正确的,当我们在重写成员时,修改成员的属性,那么逻辑是说不通的。
- 坦克的 x 坐标是数字
- 己方坦克是坦克
- 己方坦克的 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();
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
里氏替换原则
译文:派生类对象可以在程序中代替其基类对象。
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,这就会导致后续一系列的问题。
**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; // √
}
protected 修饰符
我们前面学习过的一些修饰符:
- 只读修饰符:
readonly
- 访问权限修饰符:
private
、public
、protected
访问修饰符 | 描述 |
---|---|
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; // √
}
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; // √
}
通常重写某个成员时,会将访问修饰符一起带上,并且和父类保持一致。
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);