目录
[1. 函数参数的高级处理](#1. 函数参数的高级处理)
[2. 函数重载](#2. 函数重载)
[1. 类的修饰符(访问控制)](#1. 类的修饰符(访问控制))
[2. 类的封装(Encapsulation)](#2. 类的封装(Encapsulation))
[3. 静态属性与静态方法(Static)](#3. 静态属性与静态方法(Static))
[4. 单例模式深度剖析](#4. 单例模式深度剖析)
[5. 继承、抽象类与多态](#5. 继承、抽象类与多态)
[1. 泛型的基本语法与应用](#1. 泛型的基本语法与应用)
[2. 泛型约束(Extends & Keyof)](#2. 泛型约束(Extends & Keyof))
[3. 泛型的默认类型与命名规范](#3. 泛型的默认类型与命名规范)
函数
函数在 TypeScript 中占据举足轻重的地位,它拥有单独的类型Function。在调用函数时,不仅必须要传递对应类型和数量的参数,其返回值类型如果开发者不手动定义,TS 也会进行自动推导
vbnet
// Function
// 第一种方式:
function fun(a: number, b: number): number {
return a + b
}
fun(1, 1)
// 第二种方式
let fun2 = function (a: number, b: number): number {
return a + b
}
// 第三种方式
let fun3 = (a: number, b: number): number => {
return a + b
}
// fun3(1,1)
// 第四种
type Tfun4 = (a: number, b: number) => number
let fun4: Tfun4 = (a, b) => {
return a + b
}
// 第五种
let fun5: Function = (a: number, b: number) => {
return a + b
}
fun5(1, 1)
1. 函数参数的高级处理
默认值陷阱:在设置函数参数的默认值时,带有默认值的参数必须要放在没有默认值的参数后面,否则编译器会抛出错误。
vbnet
// 定义一个带有默认值的函数
const fun2 = function(a: number = 1, b: number = 2): number {
return a + b;
}
// 场景一:不传值,直接使用默认值计算
console.log(fun2()); // 结果为 3 (1 + 2)
// 场景二:传值,传入的值会覆盖默认值
console.log(fun2(11, 22)); // 结果为 33 (11 + 22)
剩余参数 (Rest 参数):当函数需要接收不确定数量的参数时,可以使用 ...rest: any语法将剩余的所有实参收集到一个数组中进行处理。
vbnet
// 使用 ...rest: any 接收前面 name 和 age 之外的所有剩余参数
function fun(name: string, age: number, ...rest: any): string {
// 将传入的参数拼接成字符串并返回
return `hello,我的名字${name},我今年${age}岁了,乌拉乌拉${rest}`;
}
// 调用时,除了前两个参数,后面的 12, 456, 'hi' 都会被收集到 rest 数组中
fun('jack', 18, 12, 456, 'hi');
函数参数解构:若传入的参数是一个复杂的对象,可以直接在形参位置进行解构(例如 function fun1({ name, age }: TInfo)),以此来极大提高代码的简洁度与可读性。
vbnet
// 直接在形参位置进行解构:{name, age}
function fun1({name, age}: TInfo) {
// 内部可以直接使用 name 和 age 变量,极大地提升了代码的可读性
return `hello,我的名字${name},我今年${age}岁了`;
}
// 调用方式与普通写法完全一致
fun1({name: 'jack', age: 18});
2. 函数重载
函数重载的本质是为同一个函数提供多个函数类型的定义,通常是因为它们的函数名相同,但是参数的类型、顺序或个数不同 。需要特别强调的是,函数重载与返回值类型无关。它的核心优势在于,我们不需要把功能相似的逻辑强行拆分成多个不同名称的函数,从而降低了代码的维护成本。
代码比对:封装搜索函数的两种方式 需求场景:封装一个搜索函数,可以根据传入的 id 或者作者进行搜索。其中,id 对应返回单条数据对象,作者对应返回一个数据数组。
方式一:不使用函数重载(存在类型推断缺陷)
vbnet
function searchMsg(val: author | number) {
if (typeof val === 'number') {
return list.find(item => item.id == val)
} else {
return list.filter((item) => item.author == val)
}
}
// 弊端:调用 searchMsg(1) 时,TS 无法确定返回值究竟是单条数据、数组还是 undefined
// 这时去访问单条数据的属性(如 res.id)会报错,虽然可以用类型断言强制解决,但并不推荐这种破坏类型安全的做法
方式二:使用函数重载(严谨安全)
vbnet
// 第一步:编写重载签名(只定义类型,不写逻辑)
function searchMsg(val: author): listItem[]
function searchMsg(val: number): listItem
// 第二步:编写实现签名(具体逻辑,其参数和返回值类型必须能兼容上面的所有重载签名)
function searchMsg(val: author | number) {
if (typeof val === 'number') {
return list.find(item => item.id == val)
} else {
return list.filter((item) => item.author == val)
}
}
// 优势:此时调用 searchMsg(1) 或 searchMsg(author.jack),TS 可以极其精准地推断出返回值类型,不再报错
类
类是拥有相同属性和方法的一系列对象的集合,它定义了所包含全体对象的静态特征(属性)和动态特征(方法)。通过new 关键字,我们可以根据类这个"模具"生成具体的对象(实例)。面向对象(OOP)拥有三大灵魂特性:封装、继承、多态。
1. 类的修饰符(访问控制)
TS 提供了强大的修饰符关键字,用于限制类成员的性质:
public(公有):默认修饰符,在类的内部、外部以及子类中都可以随意被访问和修改
private(私有):私有属性或方法只能在声明它的类的内部被访问,外部强行读取或修改都会报错
protected(受保护):它和 private 类似,但在子类中是允许被访问的
readonly(只读):在类的内部和外部都是可读的,但绝对不能被改写
进阶技巧 :参数属性 修饰符可以直接使用在构造函数(constructor)的参数中,这等同于在类中定义该属性并同时完成了赋值,能够让代码变得极其简洁
2. 类的封装(Encapsulation)
封装的核心目的,是将对数据的操作细节隐藏起来 ,只暴露对外的安全结构 外界调用端不需要知道内部的复杂细节就能通过提供的接口访问对象,同时也保证了外界无法任意更改对象内部的数据,提升了系统的安全性
方法的封装:当一个公共行为需要调用多个复杂的底层逻辑时,我们可以将底层逻辑设为 private 私有方法,然后对外仅暴露一个 public 的公共方法。
属性的封装与存取器(getter & setter):我们通常将类的属性设置为 private,防止外部乱改,然后提供自定义的方法或 TS 内置的存取器来控制属性的读取和赋值行为
3. 静态属性与静态方法(Static)
静态属性和静态方法存在于类自身之中,只属于这个类本身,类的实例对象无法直接调用它们
调用时,无需实例化,直接使用 类名.方法名 或 类名.属性名 即可
使用场景:并不是所有的类都需要被实例化。例如我们编写一个处理日期的公共工具类 DateUtil,里面包含格式化时间、计算天数差等纯工具方法。这种情况下,直接定义为静态方法,调用时直接 DateUtil.timeConversion() 即可,免去了 new 对象的开销,非常高效
4. 单例模式深度剖析
单例模式的核心概念是:一个类只允许外部获取到它的唯一一个实例对象 。 要实现单例,必须结合刚刚讲到的静态属性 与私有化构造函数( private constructor ,禁止外部直接 new)。
实现方式一:立即创建模式 这种模式无论你后边调不调用,类在加载时底层都会立刻把对象实例化出来。
vbnet
class DateUtil {
// 立即创建模式,定义类的静态属性并赋值实例
static dateUtil = new DateUtil()
// 构造函数私有化,强制禁止外部实例化
private constructor() { console.log('立即创建了'); }
}
实现方式二:懒加载模式(更常用) 这种模式一开始不创建,只有在第一次需要获取实例的时候才去创建,从而节约系统资源。
vbnet
class DateUtil {
static dateUtil: DateUtil;
static getInstance() {
if (!this.dateUtil) { // 核心逻辑:如果为空则去创建一次
return this.dateUtil = new DateUtil()
} else { // 如果不为空,说明已经创建过,直接返回这唯一的实例
return this.dateUtil
}
}
private constructor() { }
}
// 业务侧获取实例:const util = DateUtil.getInstance()
5. 继承、抽象类与多态
继承(extends)与实现(implements):
**implements:**用于让一个新的类去实现父类或者接口所有的属性和方法,同时可以包含新的功能。一个类只能继承另一个类,但可以实现多个接口。
**extends:**子类继承父类的公有部分,以保证代码的可重用性。子类中必须使用 super 关键字来调用父类的构造函数和方法。
抽象类(abstract):
以 abstract 开头 的类是基类,专门用来当父类被继承,抽象类绝对不允许被实例化 。它内部可以包含没有方法体的抽象方法,强制要求继承它的子类必须对这些抽象方法进行重写和实现,从而规定了子类的结构。
多态:
由继承产生不同的子类,它们对同一个方法可以有不同的内部响应机制。在大型项目中,如果不用多态,每次扩展功能就得去改动源码;而有了多态机制,我们可以提前用接口规范好方法的属性,把具体的实现逻辑交给后期接入的子类程序去完成。
泛型
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候,再动态指定类型的一种特性。
它同时具备两大特点:泛型的宽泛 (定义时类型不明确,由调用者决定)与泛型的严谨(一旦指定,编译期间就会进行严格的数据类型检查)。
泛型的核心优点:
类型安全:在编译时进行类型检查,确保正确性,大幅减少运行时错误
代码复用:不需要为每种数据类型编写多个版本的函数或类
1. 泛型的基本语法与应用
泛型函数:可以将类型作为参数进行传递(类型参数化)。例如 function fun2<type>(arg1: type): type调用时可以通过 fun2<number>(1) 显式指定,如果不指定,TS 也会根据传入的值自动推断数据类型
vbnet
// 定义一个泛型函数 fun2,<type> 代表任意传入的类型变量
function fun2<type>(arg1: type, arg2: type): type {
return arg1 || arg2;
}
// 场景一:自动推断类型
// 调用时不显式指定类型,TS 会自动推断传入的 true 和 false 是 boolean 类型
fun2(true, false); [1]
// 场景二:显式指定为基础类型
// 明确告知泛型此时代表 number,后续参数也必须严格是 number
fun2<number>(1, 1); [1]
fun2<string>("hello", "word"); [1]
// 场景三:显式指定为复杂的对象类型
// 明确要求传入包含 length 属性(值为 number)的对象
fun2<{ length: number }>({ length: 0 }, { length: 1 });
泛型接口:可以限制并声明多个动态类型,例如 interface Iperson<T1, T2>,在使用时传入具体类型即可
vbnet
// 定义包含两个泛型变量 T1 和 T2 的接口
interface Iperson<T1, T2> {
name: T1,
age: T2
}
// 使用接口时,明确给 T1 传入 string 类型,给 T2 传入 number 类型
const p2: Iperson<string, number> = {
name: 'tom',
age: 10
}
泛型类:例如 class Phone<T1, T2>,在 new 实例化对象时,告知泛型具体的代表类型:new Phone<string, number>("huawei", 5.5)
vbnet
// 定义带有泛型 T1 和 T2 的 Phone 类
class Phone<T1, T2> {
name: T1;
size: T2;
price: T2;
// 构造函数也使用泛型来约束传入的参数
constructor(name: T1, size: T2, price: T2) {
this.name = name;
this.size = size;
this.price = price;
}
}
// 方式一:TS 根据传入的参数自动推断类型
const P1 = new Phone("iphone", 6.5, 1000);
// 方式二:实例化时,手动、明确地声明泛型的具体类型(推荐做法,更严谨)
const P2 = new Phone<string, number>("huawei", 5.5, 1200);
// 方式三:在变量声明时指定类的泛型类型
const P3: Phone<string, number> = new Phone("chuizi", 5.5, 2000);
2. 泛型约束(Extends & Keyof)
如果仅仅使用 <T>,那么 T 可以是任何类型,有时候这种完全的自由会带来潜在风险
为了规范调用者传值,我们需要对泛型进行约束。
keyof 关键字:用于获取一个类、对象或接口的所有属性名组合而成的联合类型
extends约束 :限制泛型参数可以接收的数据类型范围,从而避免调用者在传值时"乱传"任意类型,确保代码的类型安全。
我们可以使用 keyof(获取一个类、对象或接口的所有属性名组成的联合类型)结合 extends 来规范泛型的传值范围
vbnet
class Order {
orderid!: number
ordername!: string
static count: number
printOrd() { }
static getCount() { }
}
// 危险写法:如果直接这么写,会有风险,因为 allKey 接收的 <T> 可以是任意毫无关联的类型
// type allKey<T> = keyof T
// 安全写法:使用 extends 对 T 做出强制约束
// 约束后,给泛型传值时就不可以乱传了,T 必须是 Order 及其子类
type allKey<T extends Order> = keyof T
// 正常使用,获取 Order 类的所有属性名
type orderAllKey = allKey<Order>
3. 泛型的默认类型与命名规范
像函数参数的默认值一样,泛型也可以设置默认的数据类型
在实际开发中,为了提高代码可读性,业界有一套常用的泛型字母缩写规范:
T:代表 Type,定义泛型时通常用作第一个类型变量名称
K:代表 Key,表示对象中的键类型
V:代表 Value,表示对象中的值类型
E:代表 Element,表示数组或集合中的元素类型
vbnet
// 给泛型 T 设置默认类型为 string
function fun<T = string>(name: T) {
return name;
}
// 不指定泛型时,默认为 string 类型处理
console.log(fun("adb"));
// 手动指定泛型为 number 类型,覆盖了默认的 string
console.log(fun<number>(111));