面向对象概述

开发最重要最难的是什么?思维

  1. 面向对象:Oriented(基于)Object(事物),简称 OO;是一种编程思想,它提出一切以类对切入点思考问题;

  2. 编程思想

    • 面向过程:以功能流程为思考切入点,不太适合大型应用
    • 函数式编程:以数学运算为思考切入点
    • 面向对象:以划分类为思考切入点,类是最小的功能单元

类的继承

成员的重写

重写(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: 受保护的成员,只能在自身和子类中访问

单根性和传递性

  • 单根性:每个类最多只能拥有一个父类;

  • 传递性:如果 AB 的父类,并且 BC 的父类,可以认为 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.");
    }
}

静态成员

  1. 静态成员是指,附着在类上的成员(属于某个构造函数的成员),使用 static 修饰的成员,是静态成员;

  2. 静态方法中的 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);

再谈接口

  1. 接口用于约束类、对象、函数,是一个类型契约;

  2. 不使用接口实现时:

    • 对能力(成员函数)没有强约束力
    • 容易将类型和能力耦合在一起
  3. 面向对象领域中的接口的语义:表达了某个类是否拥有某种能力,其实,就是实现了某种接口

案例

  1. 有一个马戏团,马戏团中有很多动物,包括:狮子、老虎、猴子、狗,这些动物都具有共同的特征:名字、年龄、种类名称,还包含一个共同的方法:打招呼,它们各自有各自的技能,技能是可以通过训练改变的。狮子和老虎能进行火圈表演,猴子能进行平衡表演,狗能进行智慧表演

  2. 马戏团中有以下常见的技能:

    • 火圈表演:单火圈、双火圈
    • 平衡表演:独木桥、走钢丝
    • 智慧表演:算术题、跳舞
  1. 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;
    }
    
  2. 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}表演跳舞`);
        }
    }
    
  3. 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

  1. 最大的不同:interface 可以被类实现,而 type 不可以;

  2. 定义的类型上:

    • interface 主要声明的是函数和对象,并且总是需要引入特定的类型;
    • type 声明的可以是任何的数据类型(基本类型别名,联合类型,元组等类型);
  3. 扩展的方式不一样:

    • interface 可以使用 extends 关键字来进行扩展(这个继承是包含关系,如果父级有了,子集不可以声明重复的,会报错的),或者是 implements 来进行实现某个接口;
    • type 也可以进行扩展,使用 & 符号进行(这个继承是合并关系,如果父级有了一个类型,子集还可以声明)这个叫做 交叉类型;
  4. 合并声明:

    • interface 可以定义一个名字,后面的接口也可以直接使用这个名字,自动合并所有的声明,但是不建议这么使用,还是使用 extends 关键字;
    • type 不能命名,会直接报错;
  5. 实例类型进行赋值:

    • interface 没有这个功能;
    • type 可以使用 typeof 获取实例的 类型进行赋值;
  6. 类型映射:

    • interface 没有这个功能
    • type 可以通过 in 来实现类型映射;

索引器

什么是索引器

  1. ts 中 获取对象中的属性有好多种方式;

    • 通过点的方式来进行获取
    • 通过属性表达式的方式进行获取
    • 通过获取对象的属性描述符进行获取
  2. 对象[值] 使用成员表达式就叫做是索引器;这里只讨论 ts 如何给索引器添加类型检查;

const A = {
    name:'sam',
    age: 20,
};

// 第一种
console.log(A.name);
// 第二种,该种方式一般 [] 中放的是变量,ts 并不知道计算结果,所以会报错
console.log(A['name']);
// 第三种
console.log(Object.getOwnPropertyDescriptor(A, 'name')?.value);

不用索引器存在的问题

  1. 问题:对象赋值绕过 ts 检查

    const A = {
        name: 'sam',
        age: 20,
    };
    
    // 新增属性没有报错
    A['className'] = "一年级";
    
  2. 解决办法:默认情况下 TS 不对索引器(成员表达式)做严格的类型检查使用配置 noImplicitAny:true 开启对隐式 any 的检查;

索引器特征

  1. 在索引器中,键的类型可以是字符串,也可以是数字;在类中,索引器书写的位置应该是所有成员之前

  2. TS 中索引器的作用

    • 在严格的检查下,可以实现为类动态增加成员
    • 可以实现动态的操作类成员
    class User {
        [props: string]: any // 这里的 any 可以是 联合类型等
    
        name: string = 'cll'
        age: number = 9
    }
    
    const u = new User();
    u['pid'] = '123'
    
  3. 在 JS 中,所有的成员名本质上,都是字符串,如果使用数字作为成员名,会自动转换为字符串;在 TS 中,如果某个类中使用了两种类型的索引器,要求两种索引器的值类型必须匹配,或者是子集;

    interface A { }
    
    class User {
        [props: number]: A
        [props: string]: object
    }
    
    const u = new User();
    u[0] = {};
    u['pid'] = {};
    

this 指向约束

JS 中的 this 指向

  1. 明确:大部分时候 this 的指向取决于函数的调用方式

    • 如果直接调用函数(全局调用),this 指向全局对象或 undefined (启用严格模式);
    • 如果使用 对象.方法 调用,this 指向对象本身;
    • 如果是 dom 事件的处理函数,this 指向事件处理对象;
  2. 特殊情况:

    • 箭头函数,this 在函数声明时确定指向,指向函数位置的 this
    • 使用 bindapplycall 手动绑定 this 对象;

TS 中的 this 的问题

  1. 对象里面的 this 推断

  2. 类中的 this 推断

解决 ts 中 this 智能推断的问题

  1. 配置 noImplicitThis:true 不允许 this 隐式的指向 any,手动声明该函数中 this 的指向,解决了 对象里面的 this 问题

  2. 书写函数时,手动声明该函数中 this 的指向,将 this 作为函数的第一个参数,该参数只用于约束 this,并不是真正的参数,也不会出现在编译结果中,解决了 类中的 this 问题

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于产品、设计、开发的问题和看法,还有技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程

了解更多

目录

  1. 1. 面向对象概述
  2. 2. 类的继承
    1. 2.1. 成员的重写
    2. 2.2. 类型匹配
    3. 2.3. protected 修饰符
    4. 2.4. 单根性和传递性
  3. 3. 抽象类
  4. 4. 静态成员
  5. 5. 再谈接口
    1. 5.1. 案例
    2. 5.2. 类型保护函数
    3. 5.3. 接口可以继承类
    4. 5.4. interface 对比 type
  6. 6. 索引器
    1. 6.1. 什么是索引器
    2. 6.2. 不用索引器存在的问题
    3. 6.3. 索引器特征
  7. 7. this 指向约束
    1. 7.1. JS 中的 this 指向
    2. 7.2. TS 中的 this 的问题
    3. 7.3. 解决 ts 中 this 智能推断的问题