面向对象概述
开发最重要最难的是什么?思维
-
面向对象:Oriented(基于)Object(事物),简称 OO;是一种编程思想,它提出一切以类对切入点思考问题;
-
编程思想
- 面向过程:以功能流程为思考切入点,不太适合大型应用
- 函数式编程:以数学运算为思考切入点
- 面向对象:以划分类为思考切入点,类是最小的功能单元
类的继承
成员的重写
重写(override):子类中覆盖父类的成员
- 无论是属性还是方法,子类都可以对父类的相应成员进行重写,但是重写时,需要保证类型的匹配;
- 注意 this 关键字:在继承关系中,this 的指向是动态——调用方法时,根据具体的调用者确定 this 指向;
- super 关键字:在子类的方法中,可以使用 super 关键字读取父类成员;
export class Tank {
protected name: string = "坦克"
sayHello() {
console.log(`我是一个${this.name}`)
}
}
export class PlayerTank extends Tank {
name: string = "玩家坦克"
life: number = 5;
sayHello() {
console.log("啦啦啦啦");
}
test() {
super.sayHello();
this.sayHello();
}
}
export class EnemyTank extends Tank {
name: string = "敌方坦克"
health: number = 1;
}
类型匹配
鸭子辨型法:子类的对象,始终可以赋值给父类(子类的成员大于等于父类的),面向对象中,这种现象,叫做里氏替换原则;
如果需要判断一个数据的具体子类类型,可以使用 instanceof
protected 修饰符
protected: 受保护的成员,只能在自身和子类中访问
单根性和传递性
单根性:每个类最多只能拥有一个父类;
传递性:如果 A 是 B 的父类,并且 B 是 C 的父类,可以认为 A 也是 C 的父类;
抽象类
父类中,可能知道有些成员是必须存在的,但是不知道该成员的值或实现是什么,因此,需要有一种强约束 (抽象类),让继承该类的子类,必须要实现该成员;
抽象类中 可以有抽象成员,这些抽象成员必须在子类中实现;
abstract class Chess {
x: number = 0
y: number = 0
abstract readonly name: string;
// 移动
move(targetX: number, targetY: number): boolean {
console.log("1. 边界判断");
console.log("2. 目标位置是否有己方棋子");
//3. 规则判断
if (this.rule(targetX, targetY)) {
this.x = targetX;
this.y = targetY;
console.log(`${this.name}移动成功`)
return true;
}
return false;
}
// 不同棋子的移动规则不同
// 模板模式:有些方法,所有的子类实现的流程完全一致,只是流程中的某个步骤的具体实现不一致,可以将该方法提取到父类,在父类中完成整个流程的实现,遇到实现不一致的方法时,将该方法做成抽象方法
protected abstract rule(targetX: number, targetY: number): boolean;
}
class Horse extends Chess {
readonly name: string = "马";
protected rule(targetX: number, targetY: number): boolean {
return true;
}
}
class Pao extends Chess {
readonly name: string;
constructor() {
super();
this.name = "炮";
}
protected rule(targetX: number, targetY: number): boolean {
return false
}
}
class Soldier extends Chess {
protected rule(targetX: number, targetY: number): boolean {
return true;
}
get name() {
return "兵";
}
}
class King extends Chess {
name: string = "将";
protected rule(targetX: number, targetY: number): boolean {
throw new Error("Method not implemented.");
}
}
静态成员
静态成员是指,附着在类上的成员(属于某个构造函数的成员),使用 static 修饰的成员,是静态成员;
静态方法中的 this:
- 实例方法中的 this 指向的是 当前对象
- 静态方法中的 this 指向的是 当前类
class Board {
width: number = 500;
height: number = 700;
init() {
console.log("初始化棋盘");
}
private constructor() { }
private static _board;
// 创建棋盘,单例模式
static createBoard(): Board {
if (this._board) {
return this._board;
}
this._board = new Board();
return this._board;
}
}
const b1 = Board.createBoard();
const b2 = Board.createBoard();
console.log(b1 === b2);
再谈接口
接口用于约束类、对象、函数,是一个类型契约;
不使用接口实现时:
- 对能力(成员函数)没有强约束力
- 容易将类型和能力耦合在一起
面向对象领域中的接口的语义:表达了某个类是否拥有某种能力,其实,就是实现了某种接口
案例
有一个马戏团,马戏团中有很多动物,包括:狮子、老虎、猴子、狗,这些动物都具有共同的特征:名字、年龄、种类名称,还包含一个共同的方法:打招呼,它们各自有各自的技能,技能是可以通过训练改变的。狮子和老虎能进行火圈表演,猴子能进行平衡表演,狗能进行智慧表演
马戏团中有以下常见的技能:
- 火圈表演:单火圈、双火圈
- 平衡表演:独木桥、走钢丝
- 智慧表演:算术题、跳舞
-
interfaces.ts
export interface IFireShow { singleFire(): void; doubleFire(): void; } export interface IWisdomShow { suanshuti(): void; dance(): void } export interface IBalanceShow { dumuqiao(): void; zougangsi(): void; } export function hasFireShow(ani: object): ani is IFireShow { if ((ani as IFireShow).singleFire && (ani as IFireShow).doubleFire) { return true; } return false; } export function hasWisdomShow(ani: object): ani is IWisdomShow { if ((ani as IWisdomShow).dance && (ani as IWisdomShow).suanshuti) { return true; } return false; }
-
animals.ts
import { IFireShow, IWisdomShow, IBalanceShow } from "./interfaces"; export abstract class Animal { abstract type: string; constructor( public name: string, public age: number ) { } sayHello() { console.log(`各位观众,大家好,我是${this.type},我叫${this.name},今年${this.age}岁`) } } export class Lion extends Animal { type: string = "狮子"; } export class Tiger extends Animal implements IFireShow { type: string = "老虎"; singleFire() { console.log(`${this.name}穿越了单火圈`); } doubleFire() { console.log(`${this.name}穿越了双火圈`); } } export class Monkey extends Animal implements IBalanceShow, IFireShow { type: string = "猴子"; dumuqiao() { console.log(`${this.name}表演走独木桥`); } zougangsi() { console.log(`${this.name}表演走钢丝`); } singleFire() { console.log(`${this.name}穿越了单火圈`); } doubleFire() { console.log(`${this.name}穿越了双火圈`); } } export class Dog extends Animal implements IWisdomShow { type: string = "狗"; suanshuti() { console.log(`${this.name}表演做算术题`); } dance() { console.log(`${this.name}表演跳舞`); } }
-
index.ts
import { Animal, Lion, Tiger, Monkey, Dog } from "./animals"; import { IFireShow, IWisdomShow, hasFireShow, hasWisdomShow } from "./interfaces"; const animals: Animal[] = [ new Lion("王富贵", 12), new Tiger("坤坤", 21), new Monkey("小六", 1), new Dog("旺财", 3), new Dog("狗剩", 5) ]; //1. 所有的动物打招呼 // animals.forEach(a => a.sayHello()); //2. 所有会进行火圈表演的动物,完成火圈表演 animals.forEach(a => { if (hasFireShow(a)) { a.singleFire(); a.doubleFire(); } }) //3. 所有会智慧表演的动物,完成智慧表演 animals.forEach(a => { if (hasWisdomShow(a)) { a.suanshuti(); a.dance(); } })
类型保护函数
通过调用该类型保护函数,会触发 TS 的类型保护,该函数必须返回 boolean
// 是否有跳火圈(单火圈、双火圈)的能力
export function hasFireShow(ani: object): ani is IFireShow {
// ani instanceof IFireShow,不能用 instanceof 来判断,ts 编译之后,接口就不存在了
// 也不能用具有跳火圈的能力的动物来判断,这样代码耦合在一起了,不友好
if ((ani as IFireShow).singleFire && (ani as IFireShow).doubleFire) {
return true;
}
return false;
}
// 是否有智慧表演(跳舞、算术题)的能力
export function hasWisdomShow(ani: object): ani is IWisdomShow {
if ((ani as IWisdomShow).dance && (ani as IWisdomShow).suanshuti) {
return true;
}
return false;
}
接口可以继承类
接口可以继承类,表示该类的所有成员都在接口中
class A {
a1: string = ""
a2: string = ""
a3: string = ""
}
class B {
b1: number = 0;
b2: number = 0;
b3: number = 0;
}
interface C extends A, B { }
// 结果
const c: C = {
a1: "",
a2: "",
a3: "",
b1: 0,
b2: 3,
b3: 4
}
interface 对比 type
最大的不同:interface 可以被类实现,而 type 不可以;
定义的类型上:
- interface 主要声明的是函数和对象,并且总是需要引入特定的类型;
- type 声明的可以是任何的数据类型(基本类型别名,联合类型,元组等类型);
扩展的方式不一样:
- interface 可以使用 extends 关键字来进行扩展(这个继承是包含关系,如果父级有了,子集不可以声明重复的,会报错的),或者是 implements 来进行实现某个接口;
- type 也可以进行扩展,使用 & 符号进行(这个继承是合并关系,如果父级有了一个类型,子集还可以声明)这个叫做 交叉类型;
合并声明:
- interface 可以定义一个名字,后面的接口也可以直接使用这个名字,自动合并所有的声明,但是不建议这么使用,还是使用 extends 关键字;
- type 不能命名,会直接报错;
实例类型进行赋值:
- interface 没有这个功能;
- type 可以使用 typeof 获取实例的 类型进行赋值;
类型映射:
- interface 没有这个功能
- type 可以通过 in 来实现类型映射;
索引器
什么是索引器
ts 中 获取对象中的属性有好多种方式;
- 通过点的方式来进行获取
- 通过属性表达式的方式进行获取
- 通过获取对象的属性描述符进行获取
对象[值] 使用成员表达式就叫做是索引器;这里只讨论 ts 如何给索引器添加类型检查;
const A = {
name:'sam',
age: 20,
};
// 第一种
console.log(A.name);
// 第二种,该种方式一般 [] 中放的是变量,ts 并不知道计算结果,所以会报错
console.log(A['name']);
// 第三种
console.log(Object.getOwnPropertyDescriptor(A, 'name')?.value);
不用索引器存在的问题
-
问题:对象赋值绕过 ts 检查
const A = { name: 'sam', age: 20, }; // 新增属性没有报错 A['className'] = "一年级";
-
解决办法:默认情况下 TS 不对索引器(成员表达式)做严格的类型检查使用配置
noImplicitAny:true
开启对隐式 any 的检查;
索引器特征
-
在索引器中,键的类型可以是字符串,也可以是数字;在类中,索引器书写的位置应该是所有成员之前
-
TS 中索引器的作用
- 在严格的检查下,可以实现为类动态增加成员
- 可以实现动态的操作类成员
class User { [props: string]: any // 这里的 any 可以是 联合类型等 name: string = 'cll' age: number = 9 } const u = new User(); u['pid'] = '123'
-
在 JS 中,所有的成员名本质上,都是字符串,如果使用数字作为成员名,会自动转换为字符串;在 TS 中,如果某个类中使用了两种类型的索引器,要求两种索引器的值类型必须匹配,或者是子集;
interface A { } class User { [props: number]: A [props: string]: object } const u = new User(); u[0] = {}; u['pid'] = {};
this 指向约束
JS 中的 this 指向
-
明确:大部分时候 this 的指向取决于函数的调用方式
- 如果直接调用函数(全局调用),this 指向全局对象或 undefined (启用严格模式);
- 如果使用
对象.方法
调用,this 指向对象本身; - 如果是 dom 事件的处理函数,this 指向事件处理对象;
-
特殊情况:
- 箭头函数,this 在函数声明时确定指向,指向函数位置的 this;
- 使用 bind、apply、call 手动绑定 this 对象;
TS 中的 this 的问题
-
对象里面的 this 推断
-
类中的 this 推断
解决 ts 中 this 智能推断的问题
-
配置 noImplicitThis:true 不允许 this 隐式的指向 any,手动声明该函数中 this 的指向,解决了 对象里面的 this 问题
-
书写函数时,手动声明该函数中 this 的指向,将 this 作为函数的第一个参数,该参数只用于约束 this,并不是真正的参数,也不会出现在编译结果中,解决了 类中的 this 问题
📱 常见问题
上一篇