在早期的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获取账号数据
}
}
可以看到,首先我们定义了一个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>
)
}
}
可以看到,在定义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 {}
成员修饰符
在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 {}
只读属性
如果我们不希望外部随意修改某一属性,而是希望在确定值后直接使用,那么可以使用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)
// 其他为请求拦截,响应拦截逻辑
}
}
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 {}
静态成员
在类中定义的属性和方法都属于对象级别的,但在开发中,有时也需要定义类级别的属性和方法,也就是类的静态成员,在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.抽象类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))
可以看到,抽象类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())