在早期的javascript(es5)开发中,需要使用函数和原型链实现类和继承,从es6开始,引入class关键字,我们可以更加方便地定义和使用类

作为javascript的超集,typescript同样支持使用class关键字,并且可以对类的属性和方法等进行静态类型检测,然而,在javascript开发中,更倾向于函数式编程

  • 在react开发中,目前更常用函数组件及与之配合的hook开发模式
  • 在vue3开发中,更推荐使用compostion api

但是,在封装某些业务时,类具有更强的封装性,因此我们也需要掌握类的知识


类的定义

在面向对象编程中,类是描述一切事物的基础,包括特有的属性和方法.具体类的定义方式如下:

  • 通常使用class关键字来定义类
  • 类内部可以声明各种属性,包括类型声明和初始值设定
  • 如果没有类型声明,则默认为any类型
  • 属性可以有初始值
  • 在默认的strictPropertyInitalization模式下,属性必须初始化,否则编译时会报错
  • 类可以有自己的构造函数,当使用new关键字创建实例时,构造函数会被调用,另外构造函数不要返回任何值,它默认返回当前创建的实例
  • 类可以有自己的函数,这些函数称为方法
import {makeAutoObservable, runInAction} from "mobx";

interface Account {
    id: number;
    username: string;
    nickname: string;
    password_hash: string;
}

class AccountStore {
    // account集合
    public dataList: Account[] = [];

    constructor() {
        // 使类的所有属性和方法变为可观察的
        makeAutoObservable(this);
    }

    // 重置 dataList 数组的方法
    public reset = () => {
        // 使用 runInAction 确保在动作内更改状态
        runInAction(() => {
            this.dataList = [];
        });
    }

    // 请求后端api获取账号数据
    public fetchAllAccounts= (params = {}, reset = false) => {
        if (reset) this.reset();
        return //编写请求后台api获取账号数据
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

可以看到,首先我们定义了一个Account对象然后使用class关键字定义AccountStore类,为该类定义了dataList属性,dataList是一个Account对象的数组,用于存储账户的集合,然后我们添加了一个构造函数constructor,构造函数里执行了makeAutoObservable(this):这行代码使类的所有属性和方法变为可观察的,接下来我们定义一个reset方法,这个reset方法用来重置dataList数组


类的继承

面向对象编程中的一个重要特性就是继承,继承不仅可以减少代码量,而且是多态的使用前提,在javascript中使用extends关键字实现继承,然后在子类中使用super访问父类

function NotFound() {
 return   // 404页面
}
export class Router extends React.Component {
    routes = [];

    constructor(props) {
        super(props);
        this.initialRoutes();
    }

    initialRoutes() {
        for (let moduleRoute of moduleRoutes) {
            for (let route of moduleRoute['routes']) {
                route['path'] = moduleRoute['prefix'] + route['subPath'];
                this.routes.push(route)
            }
        }
    }

    render() {
        return (
            <Switch>
                {this.routes.map(route =>
                    <Route exact strict key={route.path} {...route}/>)}
                <Route component={NotFound}/>
            </Switch>
        )
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

可以看到,在定义Router类时,使用extends关键字继承React.Component,在Router构造器中我们调用父类的构造函数,传递`props参数`


类的多态

面向对象编程的三大特性:继承,封装,多态,多态的定义:不同数据类型在进行同一个操作时表现出不同的行为,这就是多态的体现

class Animal {
    action() {
    console.log("animal action")
    }
}
class Dog extends Animal {  //继承是多态的前提
    action() {  // 子类重写父类的action方法
        console.log("dog running")
    }
}
class Fish extends Animal {
    action(): void {
        console.log("fish swimming")
    }
}

// 1.多态是为了写出更具通用性的代码
function makeActions(animals: Animal[]){
    animals.forEach(animal => {
        // 2.animals是父类的引用,指向子类对象
        animal.action() // 3.调用子类的action方法
    });
}
makeActions([new Dog(), new Fish()])
export {}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.


成员修饰符

在typescript中可以使用三种修饰符来控制类的属性和方法的可见性,分别是public,private,protected

  • public: 默认的修饰符,它表示属性或方法是公有的,可以在类的内部和外部被访问
  • private: 表示属性或方法是私有的,只有类的内部或及其子类中被访问,外部无法访问
  • protected: 表示属性或方法可以增强类的封装性,只能在类的内部及其子类中被访问,外部无法访问

使用private和protected修饰符可以增强类的封装性,避免属性和方法被外部访问和修改

class Person {
    // 私有属性不能被外部访问,需要封装方法来操作name属性
    private name: string = ""
    // protected的属性,在类内部和子类中可以访问
    protected age: number = 123
    getName(){ // 默认是public方法
        return this.name  // 获取name
    }
    setName(newName: string){ 
        this.name = newName // 设置name
    }
}
class Student extends Person{
    getAge() {
        // 子类可以访问父类的protected属性
        return this.age
    }
}

const p = new Person()
// console.log(p.name) // 直接访问私有的name属性会报错
p.setName("why")
console.log(p.getName())
const stu = new Student()
// console.log(stu.age) // 直接访问受保护的age属性会报错
console.log(stu.getAge())
export {}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.


只读属性

如果我们不希望外部随意修改某一属性,而是希望在确定值后直接使用,那么可以使用readonly,在代码中readonly一般配合private一起使用,以下是对axios请求封装一个简单的示例

import axios, {AxiosInstance, AxiosRequestConfig} from 'axios'

const defaultConfig: AxiosRequestConfig = {
    timeout: 60000,
    headers: {
        'Content-Type': 'application/json',
    },
    withCredentials: true,
}

function generateAxiosInstance(apiConfig: AxiosRequestConfig): AxiosInstance {
    const config = {...apiConfig};
    config.headers = config.headers || {};
    config.headers['Content-Type'] = 'application/json';
    return axios.create(config);
}

function generateFormDataAxiosInstance(apiConfig: AxiosRequestConfig): AxiosInstance {
    const config = {...apiConfig};
    config.headers = config.headers || {};
    config.headers['Content-Type'] = 'application/x-www-form-urlencoded';
    return axios.create(config);
}

export default class AppHttpClient {
    private readonly _apiConfig: AxiosRequestConfig;  // 私有属性一般习惯以下划线开头命令
    private readonly _AXIOS: AxiosInstance;           // 私有属性一般习惯以下划线开头命令
    private readonly _AXIOS_FORM: AxiosInstance;      // 私有属性一般习惯以下划线开头命令

    // 只读属性可以在构造器中赋值,赋值之后就不可以修改
    constructor(apiConfig: AxiosRequestConfig) {
        this._apiConfig = {...defaultConfig, ...apiConfig}
        this._AXIOS = generateAxiosInstance(this._apiConfig)
        this._AXIOS_FORM = generateFormDataAxiosInstance(this._apiConfig)
        // 其他为请求拦截,响应拦截逻辑
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.


getter/setter

对于一些私有属性,我们不能直接访问,或者对于某些属性,我们想要监听其获取和设置的过程,这时,可以使用getter和setter访问器.

import { makeAutoObservable } from "mobx"
class ServerStore{
    dataList: Array<any>;

    constructor(){
        makeAutoObservable(this);
        this.dataList = [];
    }
    
    public set setDataList (data: Array<any>){  // 1.setter访问器
        this.dataList = data;
    }

    public get getDataList(): Array<any> {  // 2.getter访问器
        return this.dataList
    }
}
const server = new ServerStore()
// 3.调用setter访问器为dataList设置值
server.setDataList = [{"id": 1, "serverName": '测试服', "openTime": "2024-06-15 12:00:00", "basePort": 8001}]
// 4.调用getter访问器获取dataList的值
console.log(server.getDataList)
export {}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.


静态成员

在类中定义的属性和方法都属于对象级别的,但在开发中,有时也需要定义类级别的属性和方法,也就是类的静态成员,在ts中,可以使用static来定义类的静态成员.

import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export class BrowserHistory {
    static push = history.push;  // 1.定义类的静态属性
    static replace = history.replace;
    static initLocation = history.location;

    static get location() {   // 2.定义类的静态方法
        return history.location;
    }

    static get history() {
        return history;
    }
}
console.log(BrowserHistory.push("/login")) // 3.访问静态属性
console.log(BrowserHistory.location) // 调用静态getter访问器
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

抽象类

在面向对象编程中,继承和多态是密切相关的,为了定义通用的调用接口,我们通常会让调用者传入父类,通过多态实现更加灵活的调用方式,通过多态实现更加灵活的调用方式.父类本身可能不需要对某些方法进行具体实现,这时可以将这些方法定义为抽象方法

// 1.抽象类Shape
abstract class Shape {
    abstract getArea(): number //2.抽象方法,没有具体实现
}

class Rectangle extends Shape { // 继承抽象类
    // 计算矩形的面积
    private width: number
    private height: number

    constructor(width: number, height: number){
        super() //在类的继承中,构造器必须调用super函数
        this.width = width
        this.height = height
    }

    getArea() {  // 实现抽象类中的getArea抽象方法
        return this.width * this.height
    }
}

class Circle extends Shape {
    // 计算圆的面积
    private r: number
    constructor(r: number) {
        super()
        this.r = r
    }
    getArea(){ // 实现抽象类中的getArea抽象方法
        return this.r * this.r * 3.14
    }
}

function makeArea(shape: Shape){
    return shape.getArea() // 多态的应用 
}


const rectangle = new Rectangle(20 ,30)
const circle = new Circle(10)
console.log(makeArea(rectangle))
console.log(makeArea(circle))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

可以看到,抽象类Shape具有以下特点

  • Shape抽象类不能被实例化,也就是说,无法通过new关键字创建对象
  • Shape中的getArea抽象方法必须由子类Rectangle和Circle实现,否则该类必须也是一个抽象类


类作为数据类型使用

类不仅可以用于创建对象,还可以用作一种数据类型.

export {}
class Person {
    name: string = "coder"
    eating() {}
}

const p = new Person() //用类创建对象
const p1: Person = {  // 类做数据类型使用
    name: "why",
    eating() {}
}

function printPerson(p: Person) { //类作为数据了类型使用
    console.log(p.name)
}

printPerson(new Person())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.