潇湘一夜雨

TyperScript

一、TypeScript开发环境搭建

1、安装Node.js与npm

从Node.js官网下载并安装最新LTS版本,安装完成后通过终端验证

node -v  # 检查Node.js版本
npm -v   # 检查npm版本

说明:Node.js是TS的运行基础,npm用于包管理

2、安装TypeScript编译器

1、安装命令

npm install -g typescript  #全局安装TyperScript编译器命令

tsc --version  # 安装后验证,显示版本号即成功

2、TypeScript编译器的具体作用

  1. 代码转换:从 TypeScript 到 JavaScript

    1. 解析(Parsing)​
      编译器通过扫描器(Scanner)将代码分解为语法单元(Tokens),并生成抽象语法树(AST)用来精确描述代码结构,去除注释和空 格,为后续阶段提供逻辑骨架。
      即:Node.js和浏览器,只认识JS代码,不认识TS 代码。需要先将TS 代码转化为JS 代码,然后才能运行。
    2. 类型检查(Type Checking)​→
      基于 AST,编译器通过符号表(Symbol Table)和绑定器(Binder)分析变量、函数和类的类型信息。若检测到类型不匹配(如将字符串 赋给数字变量)、未定义属性或参数类型错误,编译器会立即报错
    3. ​代码生成(Code Generation)
      通过发射器(Emitter)将 AST 转换为目标 JavaScript 代码,并根据 tsconfig.json 中的配置(如 target 选项)调整输出代码的 ECMAScript 版本。例如,装饰器(Decorators)会被转换为兼容的 ES5 代码
  2. 错误检测与诊断
    编译器在多个阶段动态检测问题,显著减少运行时错误:
    1、语法错误: 如缺少分号、括号不匹配,在解析阶段直接报错。
    2、语义错误: 如类型不兼容、接口未实现,在类型检查阶段提示。
    3、配置错误: tsconfig.json 中无效选项,触发 ConfigFileErrors

    例如,代码 let a: number = “hello”; 会因类型不匹配触发 TypeErrors,避免潜在运行时崩溃

  3. 配置管理:tsconfig.json 的核心作用
    通过 tsconfig.json,开发者可以精细控制编译行为:
    关键配置示例:

    {
      "compilerOptions": {
    	 "target": "ES6",       // 输出 JS 版本
    	 "strict": true,        // 启用严格类型检查
    	 "outDir": "./dist",    // 输出目录
    	 "skipLibCheck": true   // 跳过第三方库类型检查(优化编译速度)[1](@ref)
      }
    }
    

    性能优化: 调整 skipLibCheck 可减少对第三方库类型定义的检查,显著缩短大型项目的编译时间

  4. 工具链集成与扩展
    编译器支持与现代开发工具无缝协作:
    1、实时开发: 结合 ts-node 可直接运行 .ts 文件,省去手动编译步骤;配合 nodemon 实现代码修改后自动重启
    2、构建工具: 与 Webpack、Babel 等集成(如通过 ts-loader),支持模块打包、热更新和代码压缩
    3、编辑器支持: 为 VS Code 等 IDE 提供类型推断和智能提示,提升编码效率

  5. 输出多样化与兼容性
    编译器不仅生成 .js 文件,还可输出:
    1、类型定义文件(.d.ts)​: 为其他 TypeScript 项目提供类型声明
    2、源映射(.js.map)​: 便于调试阶段映射编译后的 JavaScript 代码到原始TypeScript 代码
    3、跨平台兼容: 通过调整 target 选项,确保代码兼容旧版浏览器或 Node.js 环境

3、初始化项目配置

  • 在项目根目录执行:
    tsc --init  # 生成tsconfig.json配置文件
    
  • 关键配置项示例:
     {
    	"compilerOptions": {
    		"target": "ES6",       // 编译目标JS版本
    		"outDir": "./dist",    // 输出目录
    		"strict": true,        // 启用严格类型检查
    		"module": "CommonJS"   // 模块系统类型
    	},
    	"include": ["src/**/*"], // 需编译的源文件
    	"exclude": ["node_modules"]
    }
    
    建议:开启strict模式以提高代码质量

二、开发工具与进阶优化

1、编辑器配置(推荐VS Code)

  • 安装VS Code及插件
    • TypeScript插件:提供语法高亮、智能代码补全、静态类型检查与错误提示等
      ​​TypeScript and JavaScript Language Features 插件已默认集成,无需单独安装。
    • TSLint​(可选):代码规范检查

2、实时编译与调试工具

  • ts-node:直接运行TS文件,无需手动编译:
    npm i -g ts-node
    
    使用示例:
    ts-node src/index.ts  # 直接执行TS文件
    
  • nodemon:监听文件变化自动重启:
     npm install -g nodemon
     nodemon --exec ts-node src/index.ts
    
    适用场景:开发阶段快速调试

3、项目集成(Webpack示例)

安装依赖

npm install webpack webpack-cli ts-loader html-webpack-plugin -D

配置webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
	entry: './src/index.ts',
	module: { rules: [{ test: /\.ts$/, use: 'ts-loader' }] },
	plugins: [new HtmlWebpackPlugin({ template: './index.html' })],
	output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }
};

添加脚本命令(package.json)

"scripts": {
	"build": "webpack",
	"serve": "webpack serve"
}

作用:实现模块打包、热更新

三、编写,编译,并运行TS代码

1、编写TS代码:

在vscode中创建一个hello.ts的文件:内容如下

console.log("你好TS");
const age: number = 18;
console.log(age);

2、编译运行TS代码

第一种: 先将 TS 手动编译为 JS 文件再运行

在vscode终端中执行如下代码(会在同级目录下生成一个同名JS文件):

#将hello.ts这个TS文件编译成一个JS文件(编译成功后会在这个TS同级目录中生成一个同名的hello.js文件)
tsc hello.ts
 
#执行js文件
node hello.js //会在页面输出:你好TS 和 18

第二种:直接运行 TS 文件(无需手动编译)

在vscode终端中执行如下代码(不会在同级目录下生成一个同名JS文件):
因为第一种每次修改代码后,都要重复执行两个命令,才能运行TS代码,太繁琐
简化方式: 使用ts-node包,通过 ts-node 直接解析并执行 TS 文件,无需生成中间 JS 文件,适合快速调试。

#安装ts-node包
npm install -g ts-node typescript  # 全局安装
#或者
npm install --save-dev ts-node typescript  # 项目内安装

后直接在 Node.js 中执行TS 代码。

#适用ts-node命令执行 hello.ts这个文件
ts-node hello.ts   //解释:ts-node 命令在内部偷偷的将TS->JS,然后,再运行JS代码。会在页面输出:你好TS 和18  (不会生成一个同名的JS文件了)

两种方式对比总结

​方式 ​适用场景 ​优势 ​缺点
tsc + node 生产环境或复杂项目 生成标准 JS 文件,兼容性强 需额外编译步骤
ts-node 快速开发调试 无需手动编译,实时生效 不适合生产部署

​建议​: 开发阶段用 ts-node 快速迭代,发布前用 tsc 编译确保兼容性。结合自动编译(tsc -w)或热重载工具如 nodemon可进一步提升效率

四、TS申明变量关键字

在 TypeScript(以及 JavaScript)中 varconstlet 是三种变量声明方式,它们的作用域、可变性和行为特性有显著差异。以下是它们的核心区别及使用建议:

val

  1. val关键字申明的变量在 ​整个函数内有效,即使声明在代码块(如 if、for)内部,也能在函数内任意位置访问

    function example() {
      if (true) {
        var x = 10;
      }
      console.log(x); // 输出 10(块外仍可访问)
    }    
    
  2. val关键字声明的变量存在变量提升
    变量提升(Hoisting)是 JavaScript 的编译阶段行为,表现为 ​变量或函数的声明会被提升到作用域顶部,但赋值操作保留在原位。未声明前访问会返回 undefined

     console.log(a); // 输出 undefined(而非报错)
     var a = 1;
    

    上面代码实际等效于:

    var a;          // 声明被提升,初始化为 undefined
    console.log(a); // undefined
    a = 10;         // 赋值仍保留在原位
    

    函数作用域:无论 var 声明在函数内何处,均被视为在函数顶部声明

  3. val关键字声明的变量存在重复声明可变性
    用val关键字声明的变量允许重复声明。同一作用域内可多次声明同名变量,可能导致意外覆盖

    var c = 1;
    var c = 2; // 合法,c 被覆盖为 2
    

let和const

  1. letconst 存在暂时性死区,声明前访问会抛出 ReferenceError,强制“先声明后使用”的规范,所以不会存在变量提升的问题
    示例:

    console.log(a); // ❌ 报错:找不到名称“a”   a 未定义
    let a = 2;
    
  2. letconst 禁止重复声明,同一作用域内重复声明会触发编译错误
    示例:

    let a = 5;
    let a = 6;   ❌ 报错:无法重新声明块范围变量“a”。
    
    const b = 5;
    const b = 6; ❌ 报错:无法重新声明块范围变量“b”。
    
  3. const 的不可变性。
    声明时必须初始化,且 ​不可重新赋值​(但对象/数组的内容可修改)
    示例:

    const PI = 3.14;
    PI = 3.1415; // ❌ 报错:常量不可变
    
    const obj = { x: 1 };
    obj.x = 2; //   ✅ 合法(修改属性值)
    
    interface IPerson {
    	id: number
    	name: string
    	age: number
    	sex: string
    }
    // 在 TypeScript 中,const 关键字用于定义一个常量引用,这意味着 person1 变量本身是不可重新赋值的。
    // 然而,对象本身的属性仍然可以被修改,因为 const 只限制了变量本身的引用,而不是对象的内容。
    // 使用 const 可以向其他开发者明确传达:这个变量本身不应该被重新赋值,从而避免无意中修改引用。
    const person1: IPerson = {
    	id: 1,
    	name: 'tom',
    	age: 20,
    	sex: '男'
    }
    
    // 尽管 person1 是用 const 定义的,但它的属性是可变的。例如,可以修改 person1 的属性值
    person1.name = "lily" // ✅ 正确.可以修改属性的值
    
    //person1 使用 const 定义的原因是为了确保该变量本身不会被重新赋值,如果想修改只能修改person1对象的属性值
    //person1 = { id: 2, name: 'jerry', age: 22, sex: '男' }; // ❌ 报错:无法重新赋值给常量
    
    声明方式 使用场景 使用场景
    val 仅在需要函数级作用域或兼容旧代码时使用(现代开发中不推荐) 避免使用(除非处理遗留代码或需要函数级作用域提升)
    let 需要重新赋值的变量(如循环计数器、状态变量) 必要时用(明确需要重新赋值的变量。)
    const 默认选择,声明常量或引用类型(如对象、数组) 优先使用(减少意外修改,增强代码可维护性)

五、TypeScript常用数据类型

可以将 TypeScript中的常用基础类型分为两类:JavaScript已有类型 和 TypeScript 新增类型

1、JavaScript已有类型

JavaScript 的类型系统基于动态类型,主要分为原始类型和对象类型两大类:

原始类型

  • 类型列表:numberstringbooleannullundefinedsymbol(ES6 新增)
  • 特点: 类型名称与 JavaScript 原生类型完全一致,变量类型在运行时动态确定,允许随时改变类型
    //数值类型
    let count = 20; //当一个变量申明并且直接赋值后,可以不指明它的类型,因为TS可以自己做类型推断,推断出这个count是number类型
    let height: number;//当声明一个变量后,没有直接赋值,建议指明它的类型
    let age: number = 10;
    let weight: number = 75.6;
    
    //字符串类型
    let myname: string = "张三";
    
    //布尔类型
    let isOk: boolean = false;
    
    //null类型
    let snull: null = null; 
    
    //undefined类型声明
    let undefin: undefined = undefined;
    
    //可以把undefined赋值给void类型,但是没有任何意义
    let unusable: void = undefined;
    

对象类型:对象(Object)

  • object 类型
    object(小写)‌表示‌非原始类型的对象‌(包括普通对象、数组、函数等),‌不接受原始值‌(如 numberstringbooleansymbolnullundefined
    在严格模式下,null 和 undefined 类型也不能赋给 object。
    let per: object = { name: "Alice", age: 30 }  // ✅正确
    let obj: object = { a: 1 };      // ✅正确
    let arr: object = [1, 2, 3];     // ✅正确
    let func: object = () => {};     // ✅正确
    
    let invalid: object = "string";  // ❌ 错误: 不能将类型“string”分配给类型“object”。
    let snull: object = null;        // ❌ 错误: 不能将类型“null”分配给类型“object”。
    let undefin: object = undefined  // ❌ 错误: 不能将类型“undefined”分配给类型“object”。
    
  • Object 类型
    Object(大写)‌:包含所有 Object 类实例的属性和方法,可接受原始值(通过包装对象隐式转换)
    在严格模式下,null 和 undefined 类型也不能赋给 Object。
    let obj1: Object = 42;           // ✅正确(隐式转换为 Number 对象)
    let obj2: Object = "hello";   	 // ✅正确(隐式转换为 String 对象)
    
    let snull: Object = null;        // ❌ 错误: 不能将类型“null”分配给类型“Object”。
    let undefin: Object = undefined  // ❌ 错误: 不能将类型“undefined”分配给类型“Object”。
    

对象类型:数组(Array)

  • 单类型数组示例:
    //数组:方括号写法
    let emptyArr: number[] = []; // 显式类型声明
    emptyArr.push(1); // 动态添加元素合法
    	
    //数组:方括号写法
    let arr1: number[] = [1, 2, 3];
    	
    //数组:泛型写法
    let arr2: Array<number> = [1, 2, 4];
    let arr3: Array<string> = ["a", "b", "c"];
    
  • 联合类型与复杂类型数组
    1、多类型元素(联合类型):通过 (Type1 | Type2)[] 允许混合类型
    let mixed: (number | string)[] = [1, "two"]; // 圆括号不可省略
    
    2、对象数组:结合接口定义复杂结构
    interface User { name: string; age: number; }
    let users: User[] = [{ name: "Alice", age: 30 }]; 
    
  • 固定结构数组:元组(Tuple):
    适用于元素数量和类型固定的场景(如坐标):
    let point: [number, number] = [10, 20]; // 必须严格匹配类型和数量
    
    若元素类型或数量不匹配会编译报错
    可通过索引访问类型(如 type X = typeof point[0]
  • 多维数组
    通过嵌套语法声明多维结构
    let matrix: number[][] = [[1, 2], [3, 4]];      // 二维数组
    let cube: number[][][] = [[[1]], [[2]]];        // 三维数组
    

对象类型:函数(Function)

  • 函数的定义
    //命名函数​:定义个名称为greet的函数,函数带一个类型为string,名称为name的参数。 :void 表示函数没有返回值
    function greet(name: string): void {
    	console.log(`Hello, ${name}`);
    }
    
    //泛型函数​:增强类型灵活性,支持多种类型参数: 函数的参数value为T类型,函数的返回值为T类型
    function identity<T>(value: T): T {
    	return value;
    }
    let output = identity("Hello");  // 自动推断为 string 类型
    let output2 = identity(45);
    
    //​回调函数​:作为参数传递,需定义类型约束。 
    //参数callback的类型为一个没有返回值的匿名函数:(result: string) => void
    function fetchData<T>(callback: (result: T) => void, data: T): void {
    	callback(data)
    }
    // 调用示例
    fetchData<string>((result) => console.log(result), "success"); //输出:"success"
    fetchData<number>((count) => console.log(count * 2), 100); 	   //输出:200
    
    //​匿名函数​:通过函数表达式定义,常用于赋值或参数传递:
    const sum = function (a: number, b: number): number {
    	return a + b;
    }
    
    //箭头函数​:简化语法,自动绑定 this,适用于回调或简洁操作
    const square = (x: number): number => x * x;
    console.log(square(5)) //输出 25
    
    //​闭包与默认参数
    const counter = (start = 0) => () => start++;
    const c = counter(10);
    console.log(c()); // 10
    console.log(c()); // 11
    
    
    //泛型函数重载:处理复杂类型逻辑时,可通过重载提升类型精确度:根据输入类型返回不同的处理结果
    function parseInput<T extends string | number>(input: T): T extends string ? string : number {
    	// 将数字分支改为返回数值类型(如四舍五入)
    	return (
        	typeof input === "string"
            	? input.trim()
            	: Number(input.toFixed(2)) // ✅ 转换为 number
    	) as T extends string ? string : number; // 需配合类型断言
    }
    
    //上面parseInput函数可以优化成如下
    function parseInput1(input: string | number): string | number {
    	return typeof input === "string" ? input.trim() : input.toFixed(2);
    }
    
    
    function FunSum<T>(x: T, y: T) {
    	if (typeof x === "number" && typeof y === "number") { console.log(x + y) }
    }
    
    const sum1: <T>(x: T, y: T) => void = (x, y) => {
    	if (typeof x === "number" && typeof y === "number") { console.log(x + y) }
    };
    
    // 数值求和(需泛型约束)
    // 泛型约束(Type Constraints)​​:通过 extends 关键字限制泛型类型的范围,确保类型满足特定条件:
    // T extends number 表示限制T的类型为number类型
    const sum2: <T extends number>(x: T, y: T) => void = (x, y) => {
    	console.log(x + y); // 需约束 T 为 number 类型
    };
    
    const sum3: <T = number>(x: T, y: T) => T = (x, y) => {
    	return x;
    };
    console.log(sum3<string>("ab", "d"));
    
    
    const sum4 = <T>(x: T, y: T) => {
    	console.log(x as any + y as any); // 绕过类型检查
    }
    
    
    const sum5 = (x: number): number => x * x;
    console.log(sum5(4))
    
    //通过 T extends Constraint 限制泛型类型的合法范围
    interface HasLength { length: number; }
    //限制T为HasLength类型
    function logSize<T extends HasLength>(obj: T): void {
    	console.log(obj.length);  // 安全访问约束属性
    }
    
    
    //默认泛型类型​:为泛型参数提供默认值,简化调用:
    //T = number表示T的默认类型为number,当然你也可以根据实际情况传递其他类型
    function createArray<T = number>(length: number, value: T): T[] {
    	return new Array(length).fill(value);
    }
    const numArr = createArray(3, 42);    // 默认 T 为 number
    const strArr = createArray<string>(3, "x");  // 显式指定 T 为 string
    

引用类型:类(class)

在 TypeScript 中,class 并不是一种简单的数据类型,而是一种用于定义类的语法结构。不过,从类型的角度来看,一个类在 TypeScript 中确实可以被用作一种引用类型(reference type),即它的实例类型
在原生 JavaScript 中,class 是从 ECMAScript 2015(ES6)开始引入的语法,用于定义对象构造器的语法糖。本质上,JavaScript 的 class 仍然是基于原型链(prototype-based)的继承机制实现的。
TypeScript 在 JavaScript 的基础上,为 class 提供了静态类型检查的功能。通过 TypeScript,你可以为类的属性和方法指定类型,从而增强代码的可读性和安全性。

  • 类的定义

    class Person {
    	readonly id: number; //readonly修饰符,表示id属性是只读的,实例对象不能修改这个属性
    	name = "狄仁杰"; 	 //直接赋值了,TS可以自动类型推断出name是string类型,相当于:name:string="狄仁杰"
    	age: number; 		 //因为age没有赋初始值,所以建议给它指定类型,否则它的类型就是any类型
    
    	//TS中用constructor关键字作为类的构造函数,构造函数是没有返回值的,所以不能指定返回类型
    	//类可以有构造函数,也可以不用构造函数
    	constructor(name: string, age: number) {
    		//构造方法内部可以设置只读属性的初始值。
    		//如果在顶层和构造方法中同时设置了初始值,那么最终会以构造方法中设置的值为准,但是在其他方法中修改只读属性都会报错。
    		this.id = 1; 
    		this.name = name;
    		this.age = age;
    	}
    	//类中的方法
    	greet(spe: string) {
    		console.log(`大家好,我叫${this.name},我今年${this.age}岁,我会${spe}`)
    	}
    }
    
    //用法:
    const p = new Person("狄仁杰", 18);
    p.greet("破案"); //输出:大家好,我叫狄仁杰,我今年18岁,我会破案
    
  • 类的继承(extends)
    在TypeScript中,使用extends关键字来指定一个类继承自另一个类
    子类通过 extends 继承父类属性和方法,并通过 super 调用父类构造函数
    在子类构造函数中必须调用 super(),否则会报错(除非子类没有构造函数)
    在子类构造函数中执行 super() 会调用父类的构造函数,并可以根据父类构造函数需要的参数传递相应的参数。
    子类可以定义与父类同名的方法,覆盖父类的实现(这叫方法重写)

    class Employee extends Person {
    	department: string;  
    	constructor(name: string, age: number, department: string) {
    		super(name, age); // 子类必须调用父类构造函数
    		this.department = department;
    	};
    	//子类方法
    	work() { 
    		//super.greet("跳舞"); // 子类中调用父类的 greet()方法。通过 super.methodName() 调用父类方法(即使子类重写了该方法)
    		console.log(`${this.name} works in ${this.department}`);
    	};
    	// 重写父类方法
    	greet(spe: string) {
        	console.log(`大家好,我叫${this.name},我今年${this.age}岁,我是${this.department}部门的,我会${spe}`)
    	}
    }
    
  • 抽象类(abstract): 定义抽象类作为基类,要求子类实现抽象方法:
    抽象类不能被实例化,只能作为基类被继承。
    包含抽象方法(无实现,需子类实现)和具体方法

    抽象类本身不能被实例化,但这并不意味着抽象类不能有构造函数。实际上,抽象类可以有构造函数,尽管它们不能直接实例化。抽象类 的构造函数主要用于以下目的:

    1、初始化抽象类的成员: 如果抽象类有实例属性或需要执行的初始化逻辑,可以通过构造函数来实现。
    2、被子类调用: 当子类实例化时,子类的构造函数会调用抽象类的构造函数(通过 super()),以便正确初始化从抽象类继承的部分

    abstract class Animal {
    	abstract makeSound(): void;     // 抽象方法
    	move(): void {console.log("Hello")};  // 具体方法
    
    	// 抽象类的构造函数。这里我们根据需要定义了一个带参数的构造函数。
    	//一般情况抽象类不需要定义构造函数。TypeScript 会自动提供一个无参数的构造函数
    	//constructor(protected name: string) {
        	//console.log(`Initializing Animal with name: ${name}`);
    	//}
    }
    
    class Dog extends Animal {
    	/*
     	* 默认构造函数
     	* 当一个类没有显式定义构造函数时,TypeScript 会自动提供一个无参数的构造函数
     	* 构造函数是隐式的,相当于:constructor() { super(); // 调用父类 Animal 的构造函数 }
     	*/
    	makeSound() { //实现父类的抽象方法 makeSound()
        	console.log("Woof!");
    	}
    }
    //调用
    const dog = new Dog()
    dog.move(); 	 //输出:Hello
    dog.makeSound(); //输出:Woof!
    
  • 多态(Polymorphism): 通过继承和方法重写实现多态:

    function animalSound(animal: Animal) {
    	animal.makeSound(); // 运行时根据实际类型调用子类的方法
    }
    //调用
    animalSound(new Dog()); // 输出: Woof!
    
  • 类继承接口(implements)
    当类通过 implements 关键字继承普通接口时,需满足以下规则:
    1、​强制实现所有成员​: 必须实现接口中定义的所有非可选属性和方法,否则会报错
    2、​单继承或多继承​: 一个类可以同时实现多个接口(如 class A implements B, C)
    ​3、静态与实例分离​: 接口仅约束实例成员,不涉及类的静态属性或构造函数
    示例:

    interface Animal {
    	name: string;
    	makeSound(): void;
    }
    
    class Dog implements Animal {
    	name: string;
    	constructor(name: string) {
        	this.name = name;
    	}
    	makeSound() { console.log("Woof!"); } // 必须实现接口方法
    }
    

    类继承多个接口
    一个类可以同时实现多个接口,只需用逗号分隔接口名称。
    示例:

    interface Flyable {
    	fly(): void;
    }
    
    interface Swimmable {
    	swim(): void;
    }
    
    class Duck implements Animal, Flyable, Swimmable {
    	name: string;
    	age: number;
    
     constructor(name: string, age: number) {
    	this.name = name;
    	this.age = age;
    }
    
     makeSound(): void {
    	console.log(`${this.name} says: Quack!`);
     }
    
    fly(): void {
    	console.log(`${this.name} is flying.`);
     }
    
      swim(): void {
    	console.log(`${this.name} is swimming.`);
      }
    }
    
    const duck = new Duck("Daffy", 5);
    duck.makeSound(); // 输出: Daffy says: Quack!
    duck.fly();       // 输出: Daffy is flying.
    duck.swim();      // 输出: Daffy is swimming.
    

    类继承泛型接口(泛型类实现泛型接口)​​
    泛型接口允许接口中包含类型参数,类在实现时需要处理泛型参数,常见场景如下:
    1、保留泛型参数​: 实现时类本身需声明泛型,并与接口参数对应,使类成为泛型类
    ​2、类型约束​: 通过 extends 限制泛型范围,例如 T extends Animal 确保泛型参数必须符合特定类型
    3、​灵活性增强​: 泛型接口适用于需要处理多种数据类型的场景(如存储库、集合等)

    interface Repository<T> {
    	add(item: T): void;
    	getAll(): T[];
    }
    
    class InMemoryRepository<T> implements Repository<T> {
    	private items: T[] = [];
    	add(item: T) { this.items.push(item); }
    	getAll() { return this.items; }
    }
    
    // 使用泛型类
    const repo = new InMemoryRepository<string>();
    repo.add("TypeScript");
    console.log(repo.getAll()); // 输出 ["TypeScript"]
    

    接口与抽象类的区别
    虽然抽象类和接口都可以用来定义类的行为,但它们有以下区别:

    1、接口: 只能定义属性和方法的签名,不能包含具体的实现。一个类可以实现多个接口。
    2、抽象类: 可以包含属性、方法的具体实现,以及构造函数。一个类只能继承一个抽象类。

    // 使用接口
    interface Shape {
    	draw(): void; // 接口中的方法没有具体实现,只有方法签名
    }
    
    class Circle implements Shape {
    	// 实现接口中定义的 draw 方法
    	draw(): void {
    		console.log("Drawing a circle"); // 具体实现:绘制一个圆形
    	}
    }
    
    // 使用抽象类
    abstract class AbstractShape {
    	abstract draw(): void; // 抽象方法 draw,子类必须实现这个方法
    	logMessage(): void {   // 普通方法 logMessage,提供通用的实现
    		console.log("This is a shape.");
    	}
    }
    
    class Rectangle extends AbstractShape {
    	// 实现抽象类中定义的 draw 方法
    	draw(): void {
    		console.log("Drawing a rectangle"); // 具体实现:绘制一个矩形
    	}
    }
    
  • 静态成员(Static Members):
    在Class中,我们还可以定义静态属性和方法,它们属于类本身而不是类的实例。我们可以通过使用static关键字来定义静态成员。
    例如,下面是一个使用静态属性和方法的示例:

    class MathUtils {
    	static PI: number = 3.14; //静态常量 PI
    
    	static calculateArea(radius: number) {
      		return MathUtils.PI * radius * radius; //使用类常量PI计算面积: πr²
    	}
    }
    //调用
    const ab= MathUtils.calculateArea(5); 
    console.log(ab); //输出:78.5
    

2、TypeScript 新增类型

TypeScript 在 JavaScript 的基础上引入了静态类型系统,新增了以下类型和特性:
1、​联合类型(Union Types)2、字面量类型(Literal Types)3、元组(Tuple)
4、枚举(Enum) 5、​类型别名(Type Aliases) 6、void 7、never
8、 类型断言 7、泛型(Generics) 10、 类型守卫 11、接口(Interface)

联合类型(Union Types)

联合类型:用 | 表示变量可接受多种类型。例如:

let id: number | string = "ID123";  // 允许数字或字符串。

交叉类型(Intersection Types)

交叉类型是 TypeScript 中用于合并多个类型的核心特性,通过 & 操作符将多个类型的属性聚合成一个新类型。它类似于“取并集”的逻辑,常用于增强类型灵活性或复用复杂结构。
定义: type Combined = TypeA & TypeB;
表示 Combined 类型同时包含 TypeA 和 TypeB 的所有属性
关键点:
1、属性叠加: 新类型拥有所有输入类型的成员。
2、冲突处理: 若属性名重复但类型不兼容,会报错;兼容时合并为联合类型。

interface User {
    id: number;
    name: string;
    email?: string;
}

interface Admin {
    role: string;
    permissions: string[];
}

// 交叉类型:同时拥有 User 和 Admin 的属性
type AdminUser = User & Admin;

const admin: AdminUser = {
    id: 1,
    name: "Alice",
    role: "superadmin",
    permissions: ["create", "delete"],
    email:"123@qq.com" //因为email在User中是可选属性,admin对象中可以定义email也可以不定义email。
};

字面量类型(Literal Types)

在TypeScript中声明字面量类型的关键字是 typeconst
字面量类型:直接限定变量值为特定值。例如:

//使用type关键字声明字面量类型
type Status = 'loading' | 'success' | 'error';

type Flag = true | false;
let isEnabled: Flag;
isEnabled = true; // ✅ 正确
isEnabled = 'yes'; // ❌ 错误: 类型 '"yes"' 不可分配给类型 'Flag'

//const关键字通常用于声明常量,但在TypeScript中,它也可以用于定义字面量类型
const str = "true"
type typeflag = typeof str//typeflag的值为"true"


let color: "red" | "blue";  //定义了一个名为 color 的变量,其类型是一个联合字面量类型"red" | "blue",变量color仅允许赋值 "red" 或 "blue"
		
let str1 = "Hello TS";   //str1的类型为:string
const str2 = "Hello TS"; //str2的类型为:"Hello TS"

在这里插入图片描述

let str1: 18 = 18; //当把str1的类型指定为数值类型的18后,那么它的值就只能为18了,不能为19或者其他,它相当于const str1 = 18;
let str2: "中国" = "中国"; //当把str2的类型指定为字符串类型的"中国"后,那么它的值就只能为"中国"不能为其他的值了,它相当于const str2 = "中国";
let str3: true = true; //当把str3的类型指定为布尔类型的true后,那么它的值就只能为true,不能为false ,它相当于const str32: true = true;

字面量类型的用处:

使用模式:字面量类型一般配合联合类型一起使用

使用场景:一般用来表示一组明确的可选值列表

比如:在贪吃蛇游戏中,游戏的方向的可选值只能是上,下,左,右中的任意一个值(它的用处和枚举很类似)

function changeDirection(direction: "上" | "下" | "左" | "右") {
	console.log("您按了" + direction);
}
changeDirection("上") //输出:您按了 上

相比string类型,字面量类型更加精确,严谨 ,不给你传错值的机会,固定你可以传哪些值。

元组(Tuple)

元组(Tuple)​:固定长度和元素类型的数组。例如:

//元组:元组类型表示允许一个已知元素数量和类型的数组,各个元素的类型可以不相同,也可以相同。 元祖严格约束数据的长度和类型。
let point: [number, string] = [122.5, "北京"]; 
		
let arr: [number, number, string, boolean] = [1, 2, "abc", true];

枚举(Enum)

枚举(enum) 是一种特殊的类型,它允许你定义一组命名的常量。枚举成员在默认情况下会被分配一个数值,从 0 开始递增

//枚举:用enum关键字来声明枚举,枚举成员是有值的,默认为从0开始自增的数值,
// 我们把枚举成员的值为数字的枚举称为:数字枚举
enum Direction1 {
	Up, //Up的默认为从0开始自增的数值,所以它的值默认为0 它相当于Up=0
	Down, //它相当于Down=1
	Left, //它相当于Left=2
	Right, //它相当于Right=3
}
//用法:
console.log(Direction1.Up); //输出:0
 
enum Direction2 {
	Up = 2, //Up的默认为从0开始自增的数值,但是此处我们已经指定了它的值为2
	Down, //它相当于Down=3
	Left, //它相当于Left=4
	Right, //它相当于Right=5
}

enum Direction3 {
	Up = 2, //Up的默认为从0开始自增的数值,但是此处我们已经指定了它的值为2
	Down, // Down的值为3
	Left = 8, 
	Right, //Right的值为9
}
 
enum Direction4 {
	Up = 2, //我们也可以手动指定枚举的值
	Down = 4,
	Left = 6,
	Right = 8,
}
 
//字符串枚举:枚举成员的值为字符串的时候此枚举为字符串枚举,
//字符串枚举没有自增长的行为,因此字符串枚举的每个成员必须要有初始值
enum Direction5 {
	Up = "上",
	Down = "下",
	Left = "左",
	Right = "右",
}
//用法:
console.log(Direction5.Up); //输出:上
 
//案例:用法
function changeDirection(direction: Direction5): void {
	console.log(direction);
}
changeDirection(Direction5.Down) //输出:下

在这里插入图片描述

类型别名(Type Aliases)

类型别名(Type Aliases)​:简化复杂类型定义。例如:

type Point = { x: number; y: number };  // 复用坐标结构

//类型别名: 类型别名其实就是自己先定义好一个类型,后续有其他变量要使用这个类型的时候,直接用了,避免每次写重复的类型代码:
type CustomArry = (string | number | boolean)[];
let arr1: CustomArry = [1, 2, "a"];
let arr2: CustomArry = ["a", "b", "c", 1, 2, 3, false];
		
//类型别名的作用是当存在多个变量使用同一个类型的时候,避免每次都写重复很长的数据类型 如:(string | number | boolean)[] 
// let arr1: (string | number | boolean)[] = [1, 2, "a"];
// let arr2: (string | number | boolean)[] = ["a", "b", "c", 1, 2, 3, false];
 
//用type关键字定义一个Person类型别名
type Person = {
	name: string;
	age: number;
	[key: string]: any; // 任意属性:即这个key可以是任意值的字符串
};
 
//用法:定义一个变量user,变量的类型为Person
let user: Person = {
	name: "Alice",
	age: 25,
	job: "Engineer",
};

void

void:表示函数无返回值。例如:

function log(message: string): void { console.log(message); }

never

never:表示永不返回的值(如抛出错误或死循环)。例如:

function error(): never { throw new Error(); }

never与 void 的区别

function noop(): void {} // 返回 undefined
function fail(): never { throw Error(); } // 无返回值

any

any:关闭类型检查,慎用(仅用于兼容旧代码或动态场景)

//any:任意类型,可以表示为任意类型,未标注类型时默认为此类型。
let temp: any; 
temp = 123;
temp = false;
temp = "abc";

unknown

unknown:类型安全的 any,需类型收窄,或类型断言后使用。
any 和unknown 都代表 TypeScript 中的任何值。
但有一个关键的区别:any 绕过了编译器的类型检查,本质上关闭了 TypeScript 对该变量的好处。
另一方面,unknown 保持类型检查完整,确保在对变量执行操作之前断言或缩小变量的类型。

unknown 的两种操作方式

  1. 类型断言(Type Assertion)
    开发者主动告知编译器具体类型,语法为 value as Type:

    let data: unknown = fetchExternalData(); // 外部数据来源
    data.trim(); // 错误:Object is of type 'unknown'
    
    let input: unknown = "Hello";
    // 必须先断言为 string,在使用str.toUpperCase() 
    let str: string = input as string; 
    console.log(str.toUpperCase());    // 安全
    

    风险​: 若断言错误(如 input 实际是数字),运行时可能崩溃

  2. 类型收窄(Type Narrowing)
    通过条件逻辑让编译器自动推断更精确的类型:

    let data: unknown = fetchData();
    //unknown类型的使用就是必须要有这个类型收窄。收窄为string → 安全
    if (typeof data === "string"){  
    	console.log(data.toUpperCase()
    );
    

    示例2

    /**
    * 处理未知类型的值,并在值为字符串时将其转为大写输出
    * @param value - 任意类型的输入值(类型为 unknown)
    */
    function handleValue(value: unknown) {
     	// 使用类型守卫检查 value 是否为 string 类型
    	if (typeof value === "string") { //unknown的使用就是必须要有这个类型收窄。
    		// 类型收窄:此时 TypeScript 知道 value 是 string 类型
    		// 调用字符串的 toUpperCase() 方法(类型安全)
    		console.log(value.toUpperCase());
     	}
     	// 注意:如果 value 不是 string 类型,此处不会有任何操作
     	// 也不会抛出运行时错误(因为 unknown 类型要求显式类型检查)
    }
    

接口(Interface)

  • 接口的定义
    接口(Interface)​:接口通过 interface 关键字声明。例如:

    interface User {
    	name: string;
    	age: number;
    	readonly id: string; // 只读属性
    	isAdmin?: boolean;   // 可选属性。通过 ? 标记可选属性:允许对象不包含 isAdmin 字段
    	greet(): void;       // 方法,当前方法没有返回值。
    }
    

    接口的只读属性​:使用 readonly 关键字防止属性被修改,只读属性初始化后不可变

    interface Point {
    	readonly x: number;
    	readonly y: number;
    }
    const p: Point = { x: 10, y: 20 }; //只读属性只能在初始化时赋值,之后不能修改
    p.x = 5; // ❌ 编译报错:无法分配到 "x",因为它是只读属性,初始化后无法重新赋值。
    

    readonly与 const 的区别
    const 用于变量级别的不可重新赋值,如 const a = 1; a = 2; 会报错。
    readonly 用于对象属性级别的不可修改,例如:

    const obj = { a: 1 } as const; // 对象内部属性也变为只读
    obj.a = 2; // ❌ 报错
    
  • 接口的继承

    // 声明一个名称为IAnimal的接口
    interface IAnimal {
    	name: string;
    }
    // 声明一个名称IDog 的接口,它继承了IAnimal接口 (extends关键字用于接口的继承。表示IDog 继承IAnimal。
    // 此时IDog中除了有自己定义的ability属性外,还多了一个从IAnimal接口中继承来的name属性)
    interface IDog extends IAnimal {
     	ability: string;
    }
    // 约束对象类型
    // 声明一个名称为pup的对象,并指明了它是IDog 类型(这里的接口IDog 就是用来给对象pup指定类型的)
    const pup: IDog = { name: "小狗", ability: "吃骨头" };
    
    // 约束数组类型
    const pups: IDog [] = [{ name: "大黄", ability: "看门" },{ name: "小黑", ability: "吃奶" }];
    

    接口多继承存在的问题:
    案例1:

    interface Father {
    	commonMethod(): string; 
    }
    interface Mother {
    	commonMethod(): number;
    }
    interface Son extends Father, Mother {  
    } //❌ 报错:接口“Son”不能同时扩展类型“Father”和“Mother”。“Father”和“Mother”类型的命名属性“commonMethod”不完全相同
    

    这个错误是由于 TypeScript 的接口继承规则导致的。具体来说,当你让 Son 接口同时继承 FatherMother 时,TypeScript 会尝试合并这两个接口的定义。然而,FatherMother 都定义了 commonMethod 方法,但它们的返回类型不同(stringnumber),这会导致类型冲突。
    1、接口继承冲突: 在 TypeScript 中,当一个接口继承多个父接口时,所有父接口中的同名方法必须具有相同的签名(包括返回类型)。在你的代码中,Father 的 commonMethod 返回 string,而 Mother
    2、类型不兼容: stringnumber 是两种完全不同的类型,TypeScript 不允许一个方法同时返回这两种类型。

    案例2:

    interface Father {
    	commonMethod(): string; 
    }
    interface Mother {
    	commonMethod(): number;
    }
    interface Son extends Father, Mother {
    	commonMethod(): string | number; // ❌ 报错:接口“Son”错误扩展接口“Father”。 在这些类型中,"commonMethod()" 返回的类型不兼容。不能将类型“string | number”分配给类型“string”。不能将类型“number”分配给类型“string”。
    }
    

    这个错误是因为 TypeScript 的接口继承规则要求子接口的方法签名必须与父接口的方法签名完全兼容。在你的代码中,Son 接口继承了 Father,但你试图在 Son 中将 commonMethod 的返回类型更改为 string | number,这与 FathercommonMethod 返回类型 string 不兼容。
    1、接口继承规则: 当一个接口(如 Son)继承另一个接口(如 Father)时,Son 必须完全实现 Father 中定义的所有方法,包括方法的返回类型。
    2、类型不兼容: string | number 是一个联合类型,表示返回值可以是 stringnumber。然而,FathercommonMethod 明确要求返回 string。因此,将 SoncommonMethod 定义为 string | number 会导致类型不兼容。

    解决方案1: 使用函数重载
    通过 ​重载签名​ 明确不同场景的返回值:

    interface Father {
    	commonMethod(): string;
    }
    interface Mother {
    	 commonMethod(): number;
    }
    interface Son extends Father, Mother {
    	// 重载声明
    	commonMethod(): string;
    	commonMethod(): number;
    }
    

    注意​: 需在实现时通过条件判断返回不同类型,例如:

    class SonImpl implements Son {
    		commonMethod(): string | number {
    		return Math.random() > 0.5 ? "success" : 42;
     	}
    }
    

    解决方案2: 统一父接口的返回值类型
    若业务允许,调整父接口的返回值类型为 ​联合类型​:

    interface Father {
    	commonMethod(): string | number; // 允许返回 string 或 number
    }
    interface Mother {
    	commonMethod(): string | number; // 允许返回 string 或 number
    }
    interface Son extends Father, Mother {
    	commonMethod(): string | number; // 兼容所有父接口
    }
    

    优点​: 通过联合类型确保所有父接口的类型兼容性

  • 接口继承类
    接口继承类的问题:
    接口可通过extends继承类的类型结构,而不能继承类的实现,常用于扩展已有类的类型定义:
    正确的做法是让类通过 implements 实现接口,或者将 Person 改为接口。

    // 定义一个 Person 类,表示一个人的基本信息
    class Person {
    	name: string;    // 姓名属性,类型为 string
    	age: number;     // 年龄属性,类型为 number
    
    	// 构造函数,用于初始化 Person 类的实例
    	constructor(name: string, age: number) {
        	this.name = name, this.age = age; // 初始化 name 和 age 属性
    	}
    
    	// say 方法,输出 "Hello"
    	say(): void { 
        	console.log("Hello"); 
    	}
    }
    
    // 定义一个接口 IEmployee,继承自 Person 类
    // 注意:在 TypeScript 中,接口不能直接继承类,但可以继承类的类型结构
    // 实际使用中,建议用 `implements` 让类实现接口,而不是接口继承类
    interface IEmployee extends Person {
    	jobTitle: string; // 扩展的 jobTitle 属性,表示职位,类型为 string
    }
    
    // 创建一个符合 IEmployee 接口的对象
    const worker: IEmployee = { 
    	name: "Alice",    // 姓名,来自 Person
    	age: 25,          // 年龄,来自 Person
    	jobTitle: "Engineer", // 职位,IEmployee 特有的属性
    	say() {}          // 实现 say 方法(空实现,因为接口不强制方法逻辑)
    };
    
  • 泛型接口
    案例1

    // 定义一个泛型接口 IPair,用于表示包含两个不同类型元素的键值对
    interface IPair<T, U> {
    	first: T;
    	second: U;
    }
    // 创建一个符合 IPair<string, number> 接口类型的对象 mixedPair,并赋值
    // 其中 first 是字符串类型,second 是数字类型
    const mixedPair: IPair<string, number> = { first: "score", second: 100 };
    

    案例2

    //该接口描述了一个函数类型,接受两个类型为T的参数并返回一个数字。然后,使用这个接口声明了一个具体的比较函数numberCompare,用于比较两个数字
    interface Comparator<T> {
    	(a: T, b: T): number; // 定义比较函数类型
    }
    const numberCompare: Comparator<number> = (a, b) => a - b;
    let num= numberCompare(10,8) //调用函数,num的值为2
    
    const numberCompare: Comparator<number> = (a, b) => a + b;
    let num2= numberCompare(10,8) //调用函数,num2的值为18
    
    //上面接口类型相当于:
    function numberCompare(a: number, b: number): number {
    	return a - b;
    }
    
  • 接口的使用场景
    接口声明后,直接使用接口名称作为对象的类型
    当一个对象类型被多次使用时,一般使用接口(interface)来描述对象的类型,达到复用的目的

    接口只能给对象指定类型

    接口声明后,直接使用接口名称作为对象的类型

  • 接口与类型别名的区别

    相同点:接口与类型别名都可以为对象指定类型

    不同点:接口只能给对象指定类型,但是类型别名可以给任意类型指定别名

    例如:

    //类型别名可以为任意类型指定类型,而在这里接口则不行,接口只能为对象指定类型
    type Animal = number | string;
    let kitty1: Animal = 14;
    let kitty2: Animal = "14";
    

六、类型断言 as

1、什么是类型断言

类型断言(Type Assertion)是 TypeScript 中开发者主动告知编译器某个值的具体类型的语法,用于覆盖编译器的默认类型推断。它不会改变运行时类型,仅在编译阶段生效,类似其他语言中的“类型转换”,但更强调开发者的主动声明

2、类型断言的语法

  • 尖括号语法​:<Type>value
    let strLength = (<string>someValue).length;
    
  • as 语法 value as Type(推荐)
    let strLength = (someValue as string).length;
    

注意​: 在 JSX 或 React 中,尖括号语法可能与标签冲突,因此统一建议使用 as 语法

断言类型需与实际类型存在兼容关系(子类型或父类型)。若需绕过限制,可通过双重断言(先断言为 unknown 或 any)实现,但需谨慎

const num = 1 as unknown as string; // 绕过类型检查(可能引发运行时错误)

3、核心使用案例

  • 处理 anyunknown 类型
    当变量类型为 anyunknown 时,断言可明确具体类型以安全操作

    const data: unknown = "hello";
    const length = (data as string).length; // 断言为 string 类型
    
  • 联合类型的类型收窄
    当联合类型需要访问某个分支的特有属性时,断言可指定具体类型

    interface Cat { run(): void; }
    interface Fish { swim(): void; }
    function isFish(animal: Cat | Fish) {
    	if (typeof (animal as Fish).swim === "function") return true; // 断言为 Fish 类型
    }
    
  • 操作 DOM 元素
    获取 DOM 元素时,断言具体类型以访问属性或方法

    const input = document.getElementById("username") as HTMLInputElement;
    input.value = "new value"; // 直接操作输入框的 value
    
  • 绕过对象字面量严格检查
    当对象字面量包含多余属性时,通过断言忽略严格检查

    /**
    * 释义:目标类型 { x: number } 要求obj对象必须包含 x 属性且类型为 number,
    * 而原始对象 { x: 0, y: 0 } 满足此要求(且 y 属性被视作多余属性)
    * TypeScript 允许忽略多余属性,只要目标类型的必填属性存在且类型匹配,因此断言合法
    **/
    const obj = { x: 0, y: 0 } as { x: number }; // 忽略多余的 y 属性
    
  • 非空断言
    非空断言是 TypeScript 中的一个特性,允许你告诉编译器某个值不会是 null 或 undefined。
    非空断言使用!后缀操作符:

    明确知道变量不为空时,即使 TypeScript 认为可能返回 null,你明确告诉它不会

    //示例1:
    // 即使 TypeScript 认为 getElementById 可能返回 null,你明确告诉它不会
    const element = document.getElementById("id")!; 
    
    
    //示例2:对象属性访问
    interface User {
    name: string;
    age?: number;
    }
    
    const user: User = { name: "Alice" };
    const age = user.age!.toString(); // 告诉编译器 age 存在
    
    //示例3:函数调用后
    function getUserName(): string | undefined {
     /* ... */
    }
    
    const name = getUserName()!.toUpperCase(); // 告诉编译器返回值不为 undefined
    
  • const 断言​:创建不可变类型

    const arr = [1, 2, 3] as const; // 类型为 readonly [1, 2, 3]
    
  • 双重断言(Double Assertion)
    什么是双重断言:
    在TypeScript中,双重断言是指对一个值进行两次类型断言的操作,通常用于满足特定的类型需求,但这种技巧可能带来风险,因为它会绕过类型检查,导致潜在的运行时错误。
    双重断言通常通过两次使用 as 关键字实现,例如:

    const value: any = "Hello, TypeScript";
    const strValue = (value as unknown) as string; // 双重断言
    

    双重断言的作用:
    解决类型不兼容问题:当需要强制将一个类型转换为另一个不兼容的类型时,双重断言可以绕过编译器的类型检查。例如,将一个 Event 类型的对象断言为 HTMLElement 类型:

    function handler(event: Event) {
    	const element = (event as any) as HTMLElement; // 双重断言
    }
    

    在上述代码中,Event 和 HTMLElement 类型不兼容,直接断言会报错,因此使用双重断言绕过类型检查。

    双重断言的风险:
    绕过类型安全:双重断言会告诉编译器“相信我,我知道我在做什么”,但如果断言错误,可能会导致运行时错误。例如,将一个非函数值断言为函数并调用:

    const value: any = "not a function";
    const func = (value as unknown) as () => void; // 双重断言
    func(); // 运行时错误:value is not a function
    

七、类型守卫

1. 类型守卫的定义

类型守卫是 TypeScript 中用于在运行时缩小变量类型范围的一种技术。通过条件判断(如 typeof、instanceof、in 操作符或自定义类型谓词),TypeScript 编译器能够确定变量在特定代码块中的具体类型,从而避免不必要的类型错误或 any 的滥用。

2. 类型守卫的核心作用

解决联合类型的不确定性:当变量类型为联合类型(如 string | number)时,类型守卫可以明确当前分支下的实际类型。
提升代码安全性:确保在特定代码路径中只能访问目标类型的属性和方法。
替代 any 的粗暴类型转换:通过显式检查实现更安全的类型操作。

3. 类型守卫的常见实现方式

3.1、typeof 类型守卫

适用于原始类型(string、number、boolean、symbol、bigint、undefined、function)。
示例:

function printLength(value: string | number) {
	if (typeof value === "string") {
		console.log(value.length); 	   // ✅ 类型收窄为 string,安全访问 length
	} else {
		console.log(value.toFixed(2)); // ✅ 类型收窄为 number,安全调用 toFixed
	}
}

3.2、instanceof 类型守卫

适用于对象类型(如类实例或构造函数)。
instanceof 是 JavaScript 和 TypeScript 中的一个运算符,用于检查一个对象是否是某个构造函数(类)的实例,或属于其原型链上的某个类。它通过比较对象的原型链与构造函数的 prototype 属性来判断类型关系,是面向对象编程中实现类型检查和多态的重要工具。
示例:

class Dog {
	bark() { console.log("Woof!"); }
}
class Cat {
	meow() { console.log("Meow!"); }
}

function makeSound(animal: Dog | Cat) {
	//判断变量 animal 是否是类 Dog 的实例
	if (animal instanceof Dog) {
		animal.bark(); // ✅ 类型收窄为 Dog
	} else {
		animal.meow(); // ✅ 类型收窄为 Cat
	}
}
  • 原型链追溯
    通过 proto 属性向上查找原型链,直到匹配到构造函数的 prototype。
    类比:
    对象 dog 的原型链:dog → Dog.prototype → Animal.prototype → Object.prototype。
    instanceof 会检查构造函数的 prototype 是否存在于对象的原型链中。

  • 与 typeof 的区别

    运算符 检查目标 适用场景
    typeof 原始类型(如 string、number) 基础类型检查
    instanceof 对象类型(类、构造函数) 面向对象编程中的实例判断

3.3、in 操作符类型守卫

检查对象是否包含特定属性(适用于接口或类)。
示例:

interface Bird {
    fly(): void;
}
interface Fish {
    swim(): void;
}

function move(animal: Bird | Fish) {
    //检查对象 animal 是否包含 fly 属性
    if ("fly" in animal) {
        animal.fly(); // ✅ 类型收窄为 Bird
    } else {
        animal.swim(); // ✅ 类型收窄为 Fish
    }
}

3.4、自定义类型谓词(Type Predicate)

通过函数返回布尔值并声明类型谓词,实现更灵活的类型检查。
语法:function isType(param: any): param is TargetType
示例:

interface Admin {
	role: "admin";
	permissions: string[];
}
interface User {
	role: "user";
	preferences: Record<string, unknown>;
}
	
/​**​
* 自定义类型守卫函数:检查用户是否为管理员(Admin)
* 
* @param user - 待检查的用户对象(基础类型为 User)
* @returns 返回一个类型谓词(type predicate):
*   - true  : 表示 user 是 Admin 类型
*   - false : 表示 user 不是 Admin 类型
* 
* @remarks
* - TypeScript ​编译时并不知道user具体是User类型还是Admin类型​!因为 user 被声明为 User | Admin
* - 此时,我们就要将这个user对象具体下来,user要么是User类型,要么是admin类型,这就是user的类型收窄
* 
* - 1. ​运行时检查
* - isAdmin(user) 的代码会在运行时检查 user.role === "admin",并返回布尔值。
* - 如果返回 true,说明此时 user ​实际上​ 是 Admin 类型。
* - 2. 编译时类型映射
* - user is Admin 的作用是 ​将运行时的检查结果映射到编译时的类型系统​:
* - 如果return user.role === "admin"的结果是返回 true,TypeScript 将 user 的类型收窄为 Admin。即:user的类型不再是原先的 User | Admin 而是现在的Admin
* - 如果return user.role === "admin"的结果是返回 false,收窄为 User。即:user的类型不再是原先的 User | Admin 而是现在的User
* 
* 
** /
function isAdmin(user: Admin | User): user is Admin {
	return user.role === "admin"; // 返回 true 时,user 类型收窄为 Admin
}

function handleUser(user: Admin | User) {
	// 如果isAdmin(user)===true,此时user的类型为Admin
	if (isAdmin(user)) {
		console.log(user.permissions); // ✅ 类型收窄为 Admin
	} else {
		console.log(user.preferences); // ✅ 类型收窄为 User
	}
}

3.5、可辨识联合类型(Discriminated Unions)

通过公共属性(如 type 字段)区分不同类型,结合类型守卫实现安全访问。
示例:

function parseInput(input: unknown) {
    if (typeof input === "string") {
        return input.toUpperCase(); 	// ✅ 类型收窄为 string
    } else if (Array.isArray(input)) {
        return input.map(item => item.toString()); // ✅ 类型收窄为 any[](需进一步处理)
    }
    throw new Error("Unsupported input type");
}

4. 类型守卫与 unknown 类型的结合

unknown 是比 any 更安全的类型,但必须通过类型守卫才能访问其属性。
示例:

function parseInput(input: unknown) {
	if (typeof input === "string") {
		return input.toUpperCase(); // ✅ 类型收窄为 string
	} else if (Array.isArray(input)) {
		return input.map(item => item.toString()); // ✅ 类型收窄为 any[](需进一步处理)
	}
	throw new Error("Unsupported input type");
}

5.类型守卫的底层原理

控制流分析(Control Flow Analysis):TypeScript 编译器通过分析 if、switch 等语句的分支逻辑,自动缩小变量的类型范围。
类型谓词标记:自定义类型谓词函数通过 param is TargetType 显式告诉编译器类型收窄的结果。

6. 类型守卫的常见误区

忽略非目标类型的处理:未覆盖所有联合类型分支时,可能导致类型推断错误。

function logValue(value: string | number) {
	if (typeof value === "string") {
    	console.log(value.length); // ✅ 正确
  	}
  	// ❌ 错误:未处理 number 类型,可能导致后续逻辑问题
}

过度依赖 any 转换:类型守卫应优先于 as 断言,避免隐式类型风险。

// ❌ 不推荐:直接断言为 string 可能导致运行时错误
function unsafeHandle(value: unknown) {
	const str = value as string; // 危险!
	console.log(str.toUpperCase());
}

7. 类型守卫的最佳实践

优先使用内置类型守卫:优先选择 typeof、instanceof、in 等内置守卫。
封装复杂逻辑:对复杂类型检查封装为自定义类型谓词函数。
保持代码可读性:通过清晰的分支逻辑和注释说明类型收窄的原因。
结合 strict 模式:在 tsconfig.json 中启用 "strict": true,强制类型检查。

8.类型守卫总结

类型守卫是 TypeScript 类型系统中的核心工具,通过显式的类型检查和编译器的智能推断,解决了联合类型的不确定性问题,提升了代码的安全性和可维护性。无论是处理原始类型、对象类型,还是复杂场景下的类型区分,类型守卫都能提供清晰的解决方案。

关键点:
✅ 类型收窄:通过条件判断缩小变量类型范围。
✅ 安全访问:确保在特定分支中只访问目标类型的成员。
✅ 灵活扩展:支持自定义逻辑和可辨识联合类型等高级用法。

八、装饰器(Decorators)

描述

TypeScript 装饰器是一种元编程工具,允许开发者在不修改原始代码的情况下增强类、方法、属性或参数的行为

定义

装饰器是一种特殊函数,通过 @expression 语法附加到类、方法、属性等元素上,用于动态修改其行为或添加元数据

启用装饰器

在 tsconfig.json 中开启实验性支持:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true  // 支持反射元数据
  }
}

类装饰器

  • ​作用​: 修改类构造函数或原型,添加元数据或扩展功能。

  • 示例​: 为类添加属性或密封类定义:

    function Sealed(constructor: Function) {
    	Object.seal(constructor);
    	Object.seal(constructor.prototype);
    }
    
    @Sealed
    class Greeter {
    	greeting: string;
    	constructor(message: string) { this.greeting = message; }
    }
    

方法装饰器

  • ​作用​: 拦截方法调用,添加日志、缓存、限流等功能。

  • 示例​: 记录方法执行时间:

    function timing(target: any, key: string, descriptor: PropertyDescriptor) {
    	const originalMethod = descriptor.value;
    	descriptor.value = function (...args: any[]) {
    		const start = performance.now();
    		const result = originalMethod.apply(this, args);
    		console.log(`${key} 执行耗时:${performance.now() - start}ms`);
    		return result;
    	};
    }
    
    class Timer {
    	@timing
    	slowOperation() { /* 耗时逻辑 */ }
    }
    

属性装饰器

  • ​作用​: 动态初始化属性或添加元数据。

  • 示例​: 自动绑定方法的 this 上下文:

    function autobind(target: any, propertyKey: string) {
    	const descriptor: PropertyDescriptor = {
    		get() { return this[propertyKey].bind(this); }
    	};
    	Object.defineProperty(target, propertyKey, descriptor);
    }
    
    class Button {
    	@autobind
    	handleClick() { console.log(this); }
    }
    

​参数装饰器

  • ​作用​: 验证参数或记录参数信息。
  • 示例​: 强制参数为正数:
    function positive(target: any, key: string, index: number) {
    	const originalMethod = target[key];
    	target[key] = function (...args: any[]) {
    		if (args[index] < 0) throw new Error("参数必须为正数");
    		return originalMethod.apply(this, args);
    	};
    }
    
    class Calculator {
    	add(@positive a: number, b: number) { return a + b; }
    }
    

访问器装饰器

  • 作用​: 控制属性的读写权限。
  • 示例​: 设置只读属性:
    function ReadOnly(target: any, key: string, descriptor: PropertyDescriptor){
    	descriptor.writable = false;
    }
    class Circle {
    	private _radius: number;
    	@ReadOnly
    	get radius() { return this._radius; }
    }
    

注意事项

  1. 执行顺序
    类内部装饰器(属性→方法→参数)先于类装饰器执行。
    多个装饰器按从下到上顺序执行

  2. 潜在风险​
    ​实验性特性​:装饰器尚未成为 ECMAScript 标准,未来语法可能变化。
    ​滥用风险​:过度使用会降低代码可读性,需结合具体场景权衡。

总结

TypeScript 装饰器通过声明式语法实现代码增强,适用于日志验证元数据管理等场景。合理使用可提升代码复用性,但需注意其实验性状态和潜在维护成本。实际开发中可结合框架(如 NestJSTypeORM)和工具库(如 reflect-metadata)充分发挥其价值。

九、TypeScript 循环遍历

遍历注意事项

  • 类型安全: 在遍历对象时,建议使用 key in obj hasOwnProperty 检查避免原型污染。
  • 性能: for 循环通常性能最佳,forEach 次之,for...in 最慢(需类型检查)。
  • 不可变数据: 遍历时如需修改数据,注意使用 map/filter 返回新数组而非直接修改原数据。

选择合适的循环遍历方法

  • 需要索引且可以中断: for 循环是最佳选择。
  • 只需要元素值且可以中断: for...of 循环是最佳选择。
  • 对每个元素执行操作且不需要返回值(副作用): forEach() 是首选。
  • 需要将数组转换为另一个新数组: map() 是首选。
  • 需要从数组中筛选出符合条件的元素生成新数组: filter() 是首选。
  • 需要将数组归纳为单个值(求和、计数、扁平化、分组等): reduce() 是最佳选择。
  • 需要检查数组中是否存在满足条件的元素(或所有元素是否都满足条件),并可以提前终止: some() 或 every() 是最佳选择。

1、for 循环

遍历目标: 对于大规模数据遍历(如数组、类数组),for 循环是最佳选择。for 循环的性能通常优于 for...of 或高阶函数(如 mapforEach),它直接操作内存地址(索引),避免了for...of的迭代器开销
适用场景: 高性能需求、需要索引的场景,
返回值: 索引(i)
特点: 支持 breakcontinuereturn,控制流程更灵活。

const arr: number[] = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]); // 输出: 1, 2, 3
}

2、for...of 循环

遍历目标: 用于遍历可迭代对象(如数组字符串MapSet 等)的值。它是一种简洁、现代的遍历方式避免了传统for循环的索引操作或 for...in 的原型链问题,性能较好。
适用场景: for...of 循环可以使代码更简洁易读,遍历无需索引,适合需要值的场景。
返回值: 值(value)
特点: 支持 breakcontinue,控制流程更灵活,且可以与 async/await 结合使用,方便地处理异步操作

const arr: number[] = [1, 2, 3];
for (const value of arr) {
	console.log(value); // 输出: 1, 2, 3
}

3、for...in 循环

遍历目标:

  • 主要用于遍历对象的属性键(包括自身属性和继承属性,它会遍历对象原型链上的可枚举属性)
    1、如不需用到从原型链继承的属性可以用hasOwnProperty 过滤
    2、hasOwnProperty 可以检查属性是否为对象自身所有
  • for...in 不推荐用于数组遍历​
    1、遍历顺序不固定(非索引顺序)
    2、若对象原型链被修改(如 Object.prototype 添加方法),for...in 会遍历到这些新增属性
    3、索引类型为字符串(如"0"而非数字 0)))

适用场景: 适用于需要获取对象自身及其原型链上的所有可枚举属性键(键名为字符串类型)的场景。(性能较差)
返回值: 值(value)

const obj = { a: 1, b: '2', c: true };
/**
* 1. for...in 循环 案例1
* 
* Object.prototype.hasOwnProperty.call(obj, key)其作用是:
* 通过调用 Object 原型上的原生 hasOwnProperty 方法,绕过对象自身可能被覆盖的 hasOwnProperty 实现,
* 从而准确判断 obj 是否直接包含 key 属性(即直接定义在对象上的属性,而非从原型链继承的属性)
**/
for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
        const value = obj[key as keyof typeof obj]; // 类型安全访问
        console.log(`key: ${key}, Value: ${value}`); //输出: key: a, Value: 1 , key: b, Value: 2,key: c, Value: true
    }
}

/**
* 2. for...in 循环,过滤原型链属性 案例2
* 
* hasOwnProperty 是 JavaScript 中用于检查对象是否具有某个自身属性(而非继承自原型链的属性)的方法。它的核心用途是避免在遍历对象属性时意外访问到原型链上的属性
* hasOwnProperty 返回一个布尔值,表示对象是否直接拥有指定的属性(而非从原型链继承)
* 在 for...in 循环中,默认会遍历对象及其原型链上的所有可枚举属性。通过 hasOwnProperty 可以过滤掉继承的属性
**/
const person = {
    name: 'Alice',
    age: 25,
  };

// 检查对象自身属性
console.log(person.hasOwnProperty('name')); // 输出:true
console.log(person.hasOwnProperty('age')); // 输出:true
console.log(person.hasOwnProperty('toString')); // 输出:false(因为toString 继承自原型链。toString 是 Object.prototype 上的方法,所有对象(包括 person)默认继承自 Object.prototype)

for (const key in person) {
    if (person.hasOwnProperty(key)) {
        const value = person[key as keyof typeof person]; // key需要显式类型断言为person的属性
        console.log(`${key}: ${value}}`); // 仅输出自身属性
    }
}


/**
* 2. for...in 循环,过滤原型链属性 案例3
* 
**/
function Parent() {}
Parent.prototype.sharedMethod = function() {}; //从原型链上添加一个sharedMethod 函数
 
const child = new Parent();
child.name = 'Child';
 
// 遍历对象属性(包括原型链)
for (const key in child) {
  console.log(key); // 输出: name, sharedMethod
}
 
// 安全遍历:仅输出自身属性
for (const key in child) {
  if (child.hasOwnProperty(key)) {
    console.log(key); // 输出: name
  }
}

4、forEach 遍历

遍历目标: forEach 方法的主要遍历目标是数组。它适用于任何类型的数组,包括:

  • 基本类型数组:number[], string[], boolean[] 等。
  • 对象数组:Person[], Product[] 等,其中每个元素都是一个对象。
  • 混合类型数组: 虽然不常见,但 TypeScript 的元组类型允许数组中包含不同类型的元素

适用场景: forEach 适用于以下场景:

  • 对每个元素执行副作用操作: 当你需要对数组中的每个元素执行某种操作,并且不关心返回值时,forEach 是一个不错的选择。例如,更新 DOM 元素、发送网络请求、记录日志等。
  • 不需要返回值: 如果你只是需要遍历数组并对每个元素进行处理,而不需要生成新的数组,forEach 比 map 更合适,因为 map 会返回一个新数组。
  • 简单遍历逻辑: 当遍历逻辑相对简单,不需要复杂的条件判断或中断遍历时,forEach 可以使代码更简洁。
  • 类型安全的操作: TypeScript 会对 forEach 的回调函数进行类型检查,确保操作是类型安全的。这对于处理对象数组特别有用。

不适用场景: 虽然 forEach 在很多情况下非常有用,但在以下场景中可能不是最佳选择:

  • 需要提前终止遍历: forEach 无法中途中断遍历。如果需要提前终止遍历,可以使用普通的 for 循环或 for...of 循环配合 break 语句。
  • 需要异步操作: forEach 不会等待异步操作完成。如果需要在 forEach 中执行异步操作,考虑使用 for...of 循环配合 await
  • 需要返回新数组: 如果需要返回一个新数组,请使用 map 方法

返回值: 没有返回值
语法:

/**
*​ callback:它是一个回调函数,会为数组中的每个元素执行一次,接收以下参数:
* 	currentValue:当前处理的元素值
* 	index(可选):当前元素的索引
* 	array(可选):被遍历的数组本身
* thisArg(可选):用于在执行 callback 函数时,将其作为 this 关键字的值(通常很少使用)
* 
**/ 
array.forEach(callback(currentValue, index, array), thisArg);

经典案例:
案例1:遍历数组并打印元素

const fruits: string[] = ['apple', 'banana', 'cherry'];
 
fruits.forEach((fruit) => {
    console.log(fruit);
});

案例2:更新对象数组中的属性

interface Product {
    id: number;
    name: string;
    price: number;
}

const products: Product[] = [
    { id: 1, name: 'Laptop', price: 999.99 },
    { id: 2, name: 'Phone', price: 499.99 }
];

// 为每个产品增加 10% 的价格
products.forEach((product) => {
    product.price *= 1.1; // 增加 10%
});

console.log(products);
// 输出:
// [
//   { id: 1, name: 'Laptop', price: 1099.989 },
//   { id: 2, name: 'Phone', price: 549.989 }
// ]

案例3: 操作 DOM 元素

const buttons: HTMLButtonElement[] = [...document.querySelectorAll('button')];

buttons.forEach((button) => {
    button.addEventListener('click', () => {
        console.log('Button clicked!');
    });
});

案例4:遍历数组并执行异步操作
虽然 forEach 本身不支持异步操作,但你可以在 forEach 中调用异步函数。注意,forEach 不会等待异步操作完成。

const urls: string[] = ['https://example.com/1', 'https://example.com/2', 'https://example.com/3'];

const fetchData = async (url: string) => {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
};

urls.forEach((url) => {
    fetchData(url);
});

案例5:遍历 Map 对象
虽然 forEach 主要用于数组,但也可以用于其他可迭代对象,比如 Map

const myMap = new Map<string, number>([['a', 1], ['b', 2], ['c', 3]]);

myMap.forEach((value, key) => {
    console.log(`${key}: ${value}`);
});
// 输出:
// a: 1
// b: 2
// c: 3

5、Map 方法(返回新数组)

简介:
Map 是一种键值对集合,键可以是任意类型(包括对象、函数等),而普通对象的键只能是字符串或 Symbol

遍历目标:
map 方法的主要遍历目标是数组。它可以用于任何类型的数组,包括:

  • 基本类型数组: 如 number[], string[], boolean[] 等。
  • 对象数组: 如 Person[], Product[] 等,其中每个元素都是一个对象。
  • 混合类型数组: 虽然不常见,但 TypeScript 的元组类型允许数组中包含不同类型的元素。

适用场景:

  • 生成新数组: 当你需要基于原数组生成一个新的数组,并且新数组的元素是通过对原数组元素进行某种变换得到时,map 是一个理想的选择
  • 数据转换: 当需要对数组中的每个元素进行某种数据转换时,map 可以非常方便地实现。例如,将对象数组中的某些属性提取出来形成新数组
  • 类型转换: map 可以用于类型转换,例如将一个类型的数组转换为另一个类型的数组
  • 返回复杂对象: map 可以用于返回更复杂的对象,而不仅仅是基本类型。

注意事项:

  • 不可变性: map 方法不会改变原数组,而是返回一个新数组。这符合函数式编程的不可变性原则。
  • 类型安全: TypeScript 会对 map 的回调函数进行类型检查,确保操作是类型安全的。
  • 性能: 对于大型数组,map 方法可能会消耗较多的内存,因为它会创建一个新的数组。

语法:

const map = new Map<KeyType, ValueType>();

经典案例:
案例1:生成新数组

const numbers: number[] = [1, 2, 3, 4, 5];
const doubledNumbers: number[] = numbers.map((number) => number * 2);
console.log(doubledNumbers); // 输出: [2, 4, 6, 8, 10]

案例2:数据转换

interface Product {
    id: number;
    name: string;
    price: number;
}

const products: Product[] = [
    { id: 1, name: 'Laptop', price: 999.99 },
    { id: 2, name: 'Phone', price: 499.99 }
];

const productNames: string[] = products.map((product) => product.name);
console.log(productNames); // 输出: ['Laptop', 'Phone']

案例3:类型转换

const numbers: number[] = [1, 2, 3, 4, 5];
const numbersAsString: string[] = numbers.map((number) => number.toString());
console.log(numbersAsString); // 输出: ['1', '2', '3', '4', '5']

案例4:返回复杂对象

const productsWithDiscount: Product[] = products.map((product) => ({
    ...product,
    price: product.price * 0.9 // 打九折
}));
console.log(productsWithDiscount);
// 输出:
// [
//   { id: 1, name: 'Laptop', price: 899.991 },
//   { id: 2, name: 'Phone', price: 449.991 }
// ]

6、Set方法(唯一值集合)

介绍:
在 TypeScript 中,Set 是一种集合数据结构,用于存储唯一值。遍历 Set 的目标是访问集合中的每一个元素。Set 的遍历适用于需要操作或检查唯一值的场景。以下是 Set 遍历的遍历目标与适用场景的详细说明:
遍历目标:

  • 访问唯一值: Set 中的每个元素都是唯一的,遍历的目标是访问这些唯一的值。
  • 不关心索引: 与数组不同,Set 中的元素没有索引,遍历时不需要考虑索引。
  • 顺序性: 虽然 Set 不保证元素的插入顺序(在某些实现中可能保持顺序,如 ES6 的 Set),但遍历时通常按照插入顺序进行。

适用场景:

  • 去重操作后的遍历: 当需要从一个数组中去除重复值并遍历唯一值时,可以先将数组转换为 Set,然后遍历 Set
  • 快速查找: 如果需要频繁检查某个值是否存在于集合中,可以先将值存储在 Set 中,然后遍历 Set 进行相关操作。
  • 集合运算: 在实现集合运算(如并集、交集、差集)时,可能需要遍历 Set 中的元素
  • 缓存或去重后的数据处理: 在处理大量数据时,可能需要先去重,然后对唯一值进行操作(如统计、转换等)
  • 事件监听或回调管理: 在某些场景中,可能需要管理一组唯一的回调函数或事件监听器,Set 可以用来存储这些唯一项,并在需要时遍历执行

语法:

const set = new Set<ValueType>();

经典案例:
案例1:去重操作后的遍历

const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = new Set(numbers);

uniqueNumbers.forEach(number => {
    console.log(number); // 输出: 1, 2, 3, 4, 5
});

案例2:快速查找

const allowedValues = new Set<string>(['apple', 'banana', 'cherry']);

allowedValues.forEach(value => {
    console.log(`Allowed value: ${value}`);
});

案例3:集合运算

const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);

// 计算并集
const union = new Set([...setA, ...setB]);
union.forEach(value => {
    console.log(value); // 输出: 1, 2, 3, 4
});

案例4:缓存或去重后的数据处理

const words = ['hello', 'world', 'hello', 'typescript', 'world'];
const uniqueWords = new Set(words);

uniqueWords.forEach(word => {
    console.log(word); // 输出: hello, world, typescript
});

案例5:事件监听或回调管理

const callbacks = new Set<() => void>();

callbacks.add(() => console.log('Callback 1'));
callbacks.add(() => console.log('Callback 2'));

callbacks.forEach(callback => {
    callback(); // 输出: Callback 1, Callback 2
});

7、while 循环

使用场景: 当循环的次数在开始时无法确定,但可以根据某个条件来判断是否继续循环时,while 循环非常适用,如: while (true) { ... }
案例1:

// 要求用户输入一个有效的数字。如果用户输入无效(例如,输入了非数字字符),
// 提示用户重新输入,直到用户输入一个有效的数字。
function processNumbers(target: number): number {
    let numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let index: number = 0;
    let sum: number = 0;

    while (index < numbers.length) {
    	// 当遇到偶数时,continue 语句会跳过当前迭代,不累加偶数到 sum 中,直接进入下一次循环
        if (numbers[index] % 2 === 0) {
            index++;
            continue;
        }

        // 如果找到目标数字 target,break 语句会立即退出 while 循环,不再继续检查数组中的其他元素
        if (numbers[index] === target) {
            console.log(`找到目标数字: ${numbers[index]}`);
            break;
        }

        // 累加奇数的值
        sum += numbers[index];
        index++;
    }

	// 如果找到了目标数字并退出循环,return sum 会返回累加的和
    if (index < numbers.length) {
        return sum; // 返回累加和
    } else {
        console.log("未找到目标数字。");
        return -1; // 如果没有找到目标数字,返回 -1
    }
}

const result = processNumbers(5);
console.log(`函数返回值: ${result}`);

案例2:

let count = 0;
while (count < 5) {
  console.log(`While 循环,计数:${count}`);
  count++;
}

8、do while 循环

使用场景: do...while循环会至少执行一次循环体,即使循环条件在第一次检查时就不满足。

// 用户输入验证:确保用户输入有效数据,至少提供一次输入机会。
function getValidNumber(): number {
    let userInput: string; // 用于存储用户的输入
    let isValid: boolean = false; // 是一个布尔值,用于跟踪用户输入是否有效
    let number: number; // 用于存储转换后的数字

    do {
        userInput = prompt("请输入一个有效的数字:");
        number = Number(userInput);
        if (isNaN(number)) {
            console.log("输入无效,请输入一个有效的数字。");
        } else {
            isValid = true; // 如果输入有效,isValid 被设置为 true
        }
    } while (!isValid);

    return number;
}

const validNumber = getValidNumber();
console.log(`你输入的有效数字是: ${validNumber}`);

9、filter 过滤

filter() 方法是数组对象的一个内置方法,用于类型安全的元素筛选,它不会改变原数组,而是返回一个新数组。
基本语法:

/**
* callback:它是一个回调函数,会为数组中的每个元素执行一次。它的返回值决定了当前元素是否应该被包含在新数组中。它接受以下参数:
* 	element:当前正在处理的元素
* 	index(可选):当前元素的索引。
* 	array(可选):调用 filter() 的数组。
*
* thisArg(可选):这个可选参数用于在执行 callback 函数时,将其作为 this 关键字的值。
**/
const newArray = array.filter(callback(element, index, array), thisArg?)

案例1:过滤数字数组

const numbers = [1, 2, 3, 4, 5, 6];

// 过滤出偶数
const evenNumbers = numbers.filter(num => num % 2 === 0);

console.log(evenNumbers); // 输出: [2, 4, 6]

案例2:过滤对象数组

interface Person {
    name: string;
    age: number;
}

const people: Person[] = [
    { name: "Alice", age: 25 },
    { name: "Bob", age: 30 },
    { name: "Charlie", age: 20 },
    { name: "David", age: 35 }
];

// 过滤出年龄大于 25 的人
const adults = people.filter(person => person.age > 25);

console.log(adults);
// 输出: [{ name: "Bob", age: 30 }, { name: "David", age: 35 }]

案例3:使用索引

const numbers = [10, 20, 30, 40, 50];

// 过滤出索引为偶数的元素
const indexedNumbers = numbers.filter((num, index) => index % 2 === 0);

console.log(indexedNumbers); // 输出: [10, 30, 50]

10、reduce :遍历元素将其累积为单个值

简介:
reduce() 是数组(Array)的核心方法,用于通过遍历元素将其累积为单个值。
reduce() 是一个非常强大的数组方法,可以替代许多其他数组操作(如 map + filter 组合),通常性能也更优。

注意事项:

  • 如果提供了 initialValue,reduce 会从第一个元素开始执行
  • 如果没有提供 initialValue,reduce 会从第二个元素开始执行,使用第一个元素作为初始值
  • 空数组调用 reduce 且没有提供初始值会抛出 TypeError

基本语法:

/**
* callback:必选的回调函数,接收以下参数:
* 	accumulator​:累积值,初始为 initialValue 或数组第一个元素。
* 	currentValue:当前处理的元素。
* 	index(可选):当前索引。
* 	array(可选):原数组对象。
* initialValue​(可选):初始累积值,推荐显式指定以确保类型安全
**/
array.reduce<ResultType>(
  callback: (accumulator: ResultType, currentValue: T, index?: number, array?: T[]) => ResultType,
  initialValue: ResultType
): ResultType;

经典案例:
案例1:

const numbers = [1, 2, 3, 4, 5];
const sum1 = numbers.reduce((acc, num) => acc + num); 
console.log(sum1) // 输出15
const sum2 = numbers.reduce((acc, num) => acc + num, 100); // 最后的这个参数100是初始累积值,相当于100+1+2+3+4+5
console.log(sum2) // 输出:115

const product = numbers.reduce<number>((acc, num) => acc * num, 2);  //最后的这个参数2是初始累积值,相当于2*1*2*3*4*5
console.log(product) // 输出:240

案例2:

const numbers = [10, 2, 33, 4, 15];
const max1 = numbers.reduce((acc, curr) => Math.max(acc, curr));
console.log(max1) // 输出: 33

/*
Infinity 是一个特殊的数值,表示正无穷大。它是一个全局属性,可以通过直接访问 Infinity 来使用。
-Infinity 被用作 reduce() 方法的初始值。这样做有几个原因:
1、确保正确性:当数组为空时,reduce() 方法如果没有提供初始值会抛出错误。使用 -Infinity 可以确保即使数组为空,reduce() 也能正常工作(虽然在这种情况下,Math.max(-Infinity) 仍然是 -Infinity)。
2、比较的起点:Math.max(acc, curr) 会在每次迭代中比较当前的累加器值 (acc) 和当前元素 (curr)。使用 -Infinity 作为初始值,可以确保任何数组元素都会比这个初始值大,从而保证第一次比较总是正确的。
3、数学上的正确性:-Infinity 是最小的可能数值,因此任何有限的数值都会比它大。这使得它成为寻找最大值的一个理想的初始值。
*/
const max2 = numbers.reduce((acc, curr) => Math.max(acc, curr), -Infinity);
console.log(max2) // 输出: 33

11、some() 和 every() 方法:检查数组中的元素是否满足某个条件

简介:
这两个方法都用于检查数组中的元素是否满足某个条件,并返回一个布尔值。

  • some(): 如果数组中至少有一个元素通过了回调函数的测试,则返回 true。一旦找到满足条件的元素,就会立即停止遍历。
  • every(): 如果数组中所有元素都通过了回调函数的测试,则返回 true。一旦找到不满足条件的元素,就会立即停止遍历。

优点:

  • 提前终止: 效率高,因为一旦满足/不满足条件就停止。
  • 语义清晰: 明确表达“是否存在某个元素满足条件”或“所有元素是否都满足条件”。

经典案例:

const ages: number[] = [18, 20, 22, 17, 25];

// 检查是否存在未成年人
const hasUnderage: boolean = ages.some(age => age < 18);
console.log(hasUnderage); // 输出: true

// 检查是否所有人都已成年
const allAdults: boolean = ages.every(age => age >= 18);
console.log(allAdults); // 输出: false

十、JavaScript 原生函数(TypeScript 继承)

find():返回第一个符合条件的元素值

未找到值的时候返回undefined

const numbers = [10, 2, 33, 4, 15];
let arr = numbers.find(itme => itme > 4)
console.log(arr) //输出:10

findIndex():返回第一个符合条件的元素索引

未找到值的时候返回-1

const numbers = [10, 2, 33, 4, 15];
let i = numbers.findIndex(itme => itme > 10)
console.log(i) //输出:2

Object.prototype.hasOwnProperty.call()

const obj = { a: 1, b: '2', c: true };
/**
* 1. for...in 循环
* 
* Object.prototype.hasOwnProperty.call(obj, key)其作用是:
* 通过调用 Object 原型上的原生 hasOwnProperty 方法,绕过对象自身可能被覆盖的 hasOwnProperty 实现,
* 从而准确判断 obj 是否直接包含 key 属性(即直接定义在对象上的属性,而非从原型链继承的属性)
**/
for (const key in obj) {
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
    const value = obj[key as keyof typeof obj]; // 类型安全访问
    console.log(key, value); // key: string, value: any (需类型断言)
  }
}

Object.keys():返回对象自身可枚举属性名的数组

/**
* 2. Object.keys + forEach循环
* 
* Object.keys(obj)获取obj对象的所有可枚举自有属性键,并以数组形式返回
* 返回值的示例:[ 'id', 'name', 'age' ]
**/
const obj = { id: 1, name: 'lily', age: 18 }
const keys = Object.keys(obj);
keys.forEach(key => {
    console.log(key) //输出:id name age
}); 

Object.values():返回对象自身可枚举属性值的数组

/**
*  3. Object.values + forEach循环
* 
* Object.values(obj)获取obj对象的所有可枚举自有属性值,
* 返回一个数组,包含对象自身所有可枚举属性(不包含继承属性)的值
* 返回值的示例:[ 1, 'lily', 18 ]
**/
const obj = { id: 1, name: 'lily', age: 18 }
Object.values(user).forEach((item ,index)=>{
    console.log(item) //输出 1 lily 18
})

Object.entries():返回对象自身可枚举键值对的二维数组

/**
*  4. Object.entries + forEach循环
* 
* Object.entries(obj)获取obj对象自身可枚举属性的键值对数组,
* 返回一个由对象 obj 的自身可枚举属性组成的二维数组,每个子数组包含两个元素:[键, 值]
* 返回值的示例:[["a", 1], ["b", "2"],["c",true]]
**/
Object.entries(obj).forEach(([key, value]) => {
  console.log(key, value); // key: string, value: any
});

call()apply()bind()方法

在 TypeScript 中 Function.prototype.call() 和 Function.prototype.apply()和 Function.prototype.bind()都是非常常用的方法,它们的作用一模一样,区别仅在于传入参数形式的不同。

参数传递​

  • call(): 参数逐个传递​(func.call(thisArg, arg1, arg2))
    call传入的参数数量不固定,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数:
  • apply(): 参数以数组形式传递​(func.apply(thisArg, [arg1, arg2]))
    apply接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为 一个带下标的集合,这个集合可以为数组,也可能为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数
  • bind(): ​预设参数并返回新函数,调用时再补充剩余参数

​执行时机​

  • call() , apply(): 立即执行函数。
  • bind(): 返回新函数,需手动调用(适用于延迟执行或事件绑定)
const person = {
  fullName: function(city, country) {
    return this.firstName + " " + this.lastName + ", " + city + ", " + country;
  }
};

const person1 = {
  firstName: "John",
  lastName: "Doe"
};
/*
call方法:逐个传递参数
第一个参数:改变 this 指向,通过第一个参数显式指定函数内部 this 绑定的对象
第二个参数:fullName函数的第一个参数
第三个参数:fullName函数的第二个参数

person.fullName.call(person1, "Seattle", "USA") 此时fullName函数内部的this指向的是person1这个对象,
所以person对象的fullName函数内部中的this.firstName其实就是person1.firstName
所以person对象的fullName函数内部中的this.lastName 其实就是person1.lastName
*/
const result1 = person.fullName.call(person1, "Seattle", "USA");
console.log(result1) //输出:John Doe, Seattle, USA

/*
apply方法:动态数组传递参数
第一个参数:改变 this 指向,通过第一个参数显式指定函数内部 this 绑定的对象
第二个参数:是一个动态数组。
           数组的第一个值是fullName函数的第一个参数
           数组的第二个值是fullName函数的第二个参数

person.fullName.call(person1, "Seattle", "USA") 此时fullName函数内部的this指向的是person1这个对象,
所以person对象的fullName函数内部中的this.firstName其实就是person1.firstName
所以person对象的fullName函数内部中的this.lastName 其实就是person1.lastName
*/
const result2 = person.fullName.apply(person1, ["Seattle", "USA"]);
console.log(result2) //输出:John Doe, Seattle, USA

/*
bind方法:
第一个参数:用于显式绑定函数的执行上下文(this 值)
第二个参数:(可选参数)如果指定值,则是fullName函数的第一个参数
第二个参数:(可选参数)如果指定值,则是fullName函数的第二个参数

person.fullName.bind(person1, "Seattle", "USA") 此时fullName函数内部的this指向的是person1这个对象,
所以person对象的fullName函数内部中的this.firstName其实就是person1.firstName
所以person对象的fullName函数内部中的this.lastName 其实就是person1.lastName

bind方法返回一个可调用的新函数
*/
const result3 = person.fullName.bind(person1, "Seattle", "USA")
console.log(result3())//输出:John Doe, Seattle, USA

//只绑定函数的执行上下文(this 值)为person1
const result4 = person.fullName.bind(person1)
//因为person.fullName.bind(person1) 没有传递fullName函数的第一个和第二个参数,所以在执行bind方法返回的新函数的时候就需要传值了。
console.log(result3("SeattleA", "USA"))//输出:John Doe, Seattle, USA

//绑定函数的执行上下文(this 值)为person1,和预设了fullName函数的第一个参数为"SeattleA"
const result5 = person.fullName.bind(person1, "Seattle")
//因为person.fullName.bind(person1, "Seattle") 没有传递fullName函数的第二个参数,所以在执行bind方法返回的新函数的时候就需要传第二个值了。
console.log(result3("USA"))//输出:John Doe, Seattle, USA

//绑定函数的执行上下文(this 值)为person1,和预设了fullName函数的第一个参数为"SeattleA",和预设了第二个参数为"USA"
const result6 = person.fullName.bind(person1, "Seattle", "USA")
//因为person.fullName.bind(person1, "Seattle", "USA") 传递fullName函数的第一,第二个参数,所以在执行bind方法返回的新函数的时候就不需要传值了。
console.log(result3()) //输出:John Doe, Seattle, USA

参数传递方式对比

方法 ​参数形式 适用场景 TypeScript 类型示例
call() arg1, arg2, ... 参数数量固定时 func.call(thisArg, "a", 1)
apply() [arg1, arg2] 参数动态或已是数组(如 arguments) func.apply(thisArg, ["a", 1])
bind() 预设部分参数,返回新函数 需要多次调用相同上下文 const bound = func.bind(thisArg, "a"); bound(1);

Date() 日期

//创建 Date 对象
const date = new Date();

//获取日期和时间部分
console.log(date.getFullYear());  // 年份
console.log(date.getMonth());     // 月份 (0-11)
console.log(date.getDate());      // 日 (1-31)
console.log(date.getDay());       // 星期几 (0-6, 0是周日)
console.log(date.getHours());     // 小时
console.log(date.getMinutes());   // 分钟
console.log(date.getSeconds());   // 秒
console.log(date.getMilliseconds()); // 毫秒
console.log(date.getTime());      // 时间戳 (毫秒)

//设置日期和时间
date.setFullYear(2024);
date.setMonth(5); // 6月
date.setDate(20);
date.setHours(15);
date.setMinutes(45);
date.setSeconds(30);

// 本地化日期时间字符串
console.log(date.toString());       // "Thu Jun 15 2023 14:30:00 GMT+0800 (中国标准时间)"
console.log(date.toLocaleString()); // "2023/6/15 14:30:00" (根据本地设置)
console.log(date.toLocaleDateString()); // "2023/6/15"
console.log(date.toLocaleTimeString()); // "14:30:00"
 
// 自定义本地化选项
console.log(date.toLocaleDateString('zh-CN', {
  year: 'numeric',
  month: '2-digit',
  day: '2-digit'
})); // "2023/06/15"

date-fns日期第三方库

对于更复杂的格式化需求,可以使用第三方库如 date-fns 或 moment.js( moment.js 现在已不推荐用于新项目):

安装date-fns

  • 安装软件包
    #安装 date-fns 软件包
    npm install date-fns --save
    # or
    yarn add date-fns
    

一、日期格式化函数

将日期转换为可读字符串或相对时间描述。

  • format(date, formatString, [options]): 将日期格式化为指定字符串(如 'yyyy-MM-dd HH:mm:ss')

    import{ format } from 'date-fns'
    
    //format 函数会将传入的 Date 对象视为 本地时间(UTC+8),并按照本地时区进行格式化
    format(new Date(), "yyyy-MM-dd"); // "2025-06-10"
    format(new Date(), "MMMM do, yyyy", { locale: zhCN }); // "六月 10日, 2025"(需导入中文locale)
    
  • formatDistance(date, baseDate, [options]): 计算两个日期的相对时间差(如 "2天前")

    import{ formatDistance,subDays } from 'date-fns'
    import { zhCN } from "date-fns/locale";
    
    formatDistance(subDays(new Date(), 3), new Date(), { locale: zhCN }); // "3天前"
    
    const date = new Date(2025, 5, 5); // 2025-06-05
    const baseDate = new Date(2025, 5, 10); // 2025-06-10
    formatDistance(date, baseDate, { locale: zhCN }); // "5天"
    
    // 添加后缀
    formatDistance(date, baseDate, { 
    	locale: zhCN, 
    	addSuffix: true 
    }); // "5天前"
    
    
    formatDistance(
    	new Date(2025, 5, 10, 14, 29, 30), // 2025-06-10 14:29:30
    	new Date(2025, 5, 10, 14, 30, 0), // 2025-06-10 14:30:00
    	{
        	locale: zhCN, //指定输出文本的语言环境 zhCN 表示简体中文。例如输出如:“30秒”或“1分钟”。
        	includeSeconds: true, //当时间差小于 ​1 分钟时,是否显示秒数。默认行为​:false(时间差 <1 分钟时显示“不到1分钟”)。
        	addSuffix: true //在输出结果后添加“前”或“后”,明确时间方向(过去或未来)。例如:若 dateA 在 dateB 之前 → 添加“前”(如 “30秒前”)。若 dateA 在 dateB 之后 → 添加“后”(如 “30秒后”)。    	
    );  //“半分钟前”
    

    封装工具函数
    场景​:统一处理多语言和业务默认配置。
    案例代码​(参考真实项目封装)

    import { formatDistance } from 'date-fns';
    import { zhCN, enUS } from 'date-fns/locale';
    
    // 封装多语言时间差工具
    export const formatDateDistance = (date, baseDate = new Date(), options = {}) => {
    const lang = options.lang || 'zh-CN';
    const locales = { 'zh-CN': zhCN, 'en': enUS };
    
    return formatDistance(date, baseDate, {
    	locale: locales[lang],
    	addSuffix: true,
    	includeSeconds: false,
    	...options // 允许覆盖默认配置
      });
    };
    
    // 调用示例
    console.log(formatDateDistance(
    	new Date(2025, 5, 10), 
    	new Date(2025, 5, 15),
    	{ lang: 'zh-CN' }
    ));
    // 输出:"5天前"
    

二、日期比较与验证函数

用于判断日期的关系、有效性或特定状态

  • isEqual(date1, date2): 判断两个日期是否严格相等(精确到毫秒)

    import{ isEqual } from 'date-fns'
    isEqual(new Date(2025, 5, 10), new Date(2025, 5, 10)); // true
    
  • isAfter(date, dateToCompare): 检查第一个日期是否在第二个日期之后

    import{ isAfter } from 'date-fns'
    isAfter(new Date(), new Date(2024, 0, 1)); // true(当前日期是2025-06-10晚于2024-01-01)
    
  • isBefore(date, dateToCompare): 检查第一个日期是否在第二个日期之前

    import{ isBefore } from 'date-fns'
    isBefore(new Date(), new Date(2024, 0, 1)); // false(当前日期是2025-06-10晚于2024-01-01)
    
  • isToday(date): 判读日期是否为今天

    import{ isToday } from 'date-fns'
    isToday(new Date()); // true
    
  • isYesterday(date): 判断日期是否为昨天(当前日期的前一天)

    import{ isYesterday } from 'date-fns'
    isYesterday(new Date()); // false
    
  • isTomorrow(date): 判断日期是否为明天(当前日期的后一天)

    import{ isTomorrow } from 'date-fns'
    isTomorrow(new Date()); // false
    
  • isExists(year, month, day): 验证指定年、月、日组合是否有效(例如检查 2023-02-30 是否合法)

  • isValid(date): 检查日期对象或时间戳是否为有效日期(如非法日期返回 false)

    import{ isValid } from 'date-fns'
    isValid(new Date("无效日期")); // false
    
  • isFriday(date): 判断日期是否为星期五

    import{ isFriday } from 'date-fns'
    isFriday(new Date(2025, 5, 13)); // true(2025-06-13是周五)
    
  • isWednesday(date): 判断日期是否为星期三

    import{ isWednesday } from 'date-fns'
    isWednesday(new Date(2025, 5, 13)); // false(2025-06-13是周五)
    

三、时间添加函数

在给定日期上增加指定时间单位。

  • addYears(date, amount): 添加指定年数(如 addYears(now, 1) 返回明年今日)
    //当前日期添加1年
    import{ format,addYears} from 'date-fns'
    format(addYears(new Date(2025, 5, 10), 1), "yyyy-MM-dd HH:mm:ss")//2026-06-10 14:15:59
    
  • addMonths(date, amount): 添加指定月数(自动处理月末溢出,如 1月31日加1个月返回2月28日)
    //当前日期添加1个月
    import{ format,addMonths } from 'date-fns'
    format(addMonths(new Date(2025, 5, 10), 1), "yyyy-MM-dd HH:mm:ss")//输出:2025-07-10 14:15:59
    
  • addDays(date, amount): 添加指定天数
    //当前日期添加1天
    import{ format,addDays } from 'date-fns'
    format(addDays(new Date(2025, 5, 10), 1), "yyyy-MM-dd HH:mm:ss")//输出:2025-06-11 14:15:59
    
  • addHours(date, amount): 添加指定小时数
    //当前日期添加1个小时
    import{ format,addHours } from 'date-fns'
    format(addHours(new Date(2025, 5, 10, 14, 15, 59, 680), 1), "yyyy-MM-dd HH:mm:ss") //输出:2025-06-10 15:15:59
    
  • addMinutes(date, amount): 添加指定分钟数
    //当前日期添加1分钟
    import{ format,addMinutes } from 'date-fns'
    format(addMinutes(new Date(2025, 5, 10, 14, 15, 59, 680), 1), "yyyy-MM-dd HH:mm:ss")//输出:2025-06-10 14:16:59
    
  • addSeconds(date, amount): 添加指定秒数
    //当前日期添加1秒钟
    import{ format,addSeconds } from 'date-fns'
    format(addSeconds(new Date(2025, 5, 10, 14, 15, 59, 680), 1), "yyyy-MM-dd HH:mm:ss")//输出:2025-06-10 14:16:00
    
  • addMilliseconds(date, amount): 添加指定毫秒数
    //当前日期添加1毫秒
    import{ format,addMilliseconds } from 'date-fns'
    format(addMilliseconds(new Date(2025, 5, 10, 14, 15, 59, 680), 1), "yyyy-MM-dd HH:mm:ss.SSS")//输出:2025-06-10 14:15:59.681
    
  • addWeeks(date, amount): 添加指定周数(等同于 addDays(date, amount * 7))
    //当前日期添加1周
    import{ format,addWeeks } from 'date-fns'
    format(addWeeks(new Date(2025, 5, 10, 14, 15, 59, 680), 1), "yyyy-MM-dd HH:mm:ss")//输出:2025-06-17 14:15:59
    

​四、时间减去函数​

在给定日期上减少指定时间单位。

  • ​​subMonths(date, amount): 减去指定月数

    import{ format,subMonths } from 'date-fns'
    
    //当前时间为2025年6月13日 00:23:06
    console.log(format(subMonths(new Date(), 1),"yyyy-MM-dd HH:mm:ss")); // 输出:2025-05-13 00:23:06
    
  • ​​subDays(date, amount): 减去指定天数

    import{ format,subDays } from 'date-fns'
    
    //当前时间是:2025年6月13日 00:29:55
    console.log(format(subDays(new Date(), 1), "yyyy-MM-dd HH:mm:ss")); //输出:2025-06-12 00:29:55
    
  • ​​​subHours(date, amount): 减去指定小时数

    import{ format,subHours } from 'date-fns'
    
    //当前时间是:2025年6月13日 00:29:55
    console.log(format(subHours(new Date(), 1), "yyyy-MM-dd HH:mm:ss")); //输出:2025-06-12 23:29:55
    
  • ​​subMinutes(date, amount): 减去指定分钟数

    import{ format,subMinutes } from 'date-fns'
    
    //当前时间是:2025年6月13日 00:29:55
    console.log(format(subMinutes(new Date(), 1), "yyyy-MM-dd HH:mm:ss")); //输出:2025-06-13 00:28:55
    
  • subWeeks(date, amount): 减去指定周数(等同于 subDays(date, amount * 7))

    import{ format,subWeeks } from 'date-fns'
    
    //当前时间是:2025年6月13日 00:29:55
    console.log(format(subWeeks(new Date(), 1), "yyyy-MM-dd HH:mm:ss")); //输出:2025-06-06 00:29:55
    

​五、时间差异计算函数​

计算两个日期之间的差值。

  • differenceInYears(dateLeft, dateRight):计算两个日期之间相差的完整年数(忽略月、日)
    //differenceInYears(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInYears } from 'date-fns';
    const date1 = new Date(2025, 0, 1); // 2025-01-01
    const date2 = new Date(2030, 0, 1); // 2030-01-01
    const years = differenceInYears(date2, date1); // 5
    
    注意​: 若 dateRight 早于 dateLeft,返回负值
  • differenceInQuarters(dateLeft, dateRight): 计算两个日期之间相差的完整季度数(1季度=3个月)
    //differenceInQuarters(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInQuarters } from 'date-fns';
    const date1 = new Date(2025, 0, 1); // 2025-Q1
    const date2 = new Date(2026, 3, 1); // 2026-Q2
    const quarters = differenceInQuarters(date2, date1); // 5(2025Q1→2026Q2共5个季度)
    
    注意​: 结果向下取整,忽略不足一季度的天数
  • differenceInMonths(dateLeft, dateRight): 计算两个日期之间相差的完整月数(忽略日)
    //differenceInMonths(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInMonths } from 'date-fns';
    const date1 = new Date(2025, 0, 15); // 2025-01-15
    const date2 = new Date(2025, 5, 10); // 2025-06-10
    const months = differenceInMonths(date2, date1); // 4(1月→6月,但不足5个月)
    
    注意​: 结果基于日历月,不考虑月份天数差异
  • differenceInWeeks(dateLeft, dateRight): 计算两个日期之间相差的完整周数(7天为1周)
    //differenceInWeeks(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInWeeks } from 'date-fns';
    const date1 = new Date(2025, 0, 1); 
    const date2 = new Date(2025, 0, 22); // 21天后
    const weeks = differenceInWeeks(date2, date1); // 3(21÷7=3)
    
    应用场景​: 常用于生成年度周列表(如日历控件)
  • differenceInDays(dateLeft, dateRight): 计算两个日期之间相差的整天数(24小时为1天)
    //differenceInDays(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInDays } from 'date-fns';
    const date1 = new Date(2025, 0, 1, 12); // 1月1日中午
    const date2 = new Date(2025, 0, 3, 6);  // 1月3日早晨
    const days = differenceInDays(date2, date1); // 1(不足48小时按1天算)
    
    注意​: 结果按24小时制向下取整
  • differenceInHours(dateLeft, dateRight): 计算两个日期之间相差的整小时数
    //differenceInHours(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInHours} from 'date-fns';
    const date1 = new Date(2025, 0, 1, 8, 30); 
    const date2 = new Date(2025, 0, 1, 14, 45); 
    const hours = differenceInHours(date2, date1); // 6(14:45 - 8:30 = 6小时15分,取整为6)
    
  • differenceInMinutes(dateLeft, dateRight): 计算两个日期之间相差的整分钟数。
    //differenceInMinutes(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInMinutes } from 'date-fns';
    const date1 = new Date(2025, 0, 1, 12, 0);
    const date2 = new Date(2025, 0, 1, 12, 45, 30); 
    const minutes = differenceInMinutes(date2, date1); // 45
    
  • differenceInSeconds(dateLeft, dateRight): 计算两个日期之间相差的整秒数。
    //differenceInSeconds(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInSeconds } from 'date-fns';
    const date1 = new Date(2025, 0, 1, 0, 0, 0);
    const date2 = new Date(2025, 0, 1, 0, 1, 30); 
    const seconds = differenceInSeconds(date2, date1); // 90
    
    应用场景​: 倒计时、超时控制
  • differenceInMilliseconds(dateLeft, dateRight): 计算两个日期之间相差的毫秒数(最高精度)
    //differenceInMilliseconds(dateLeft: Date | number, dateRight: Date | number): number
    import { differenceInMilliseconds } from 'date-fns';
    const date1 = new Date(2025, 0, 1, 0, 0, 0, 0);
    const date2 = new Date(2025, 0, 1, 0, 0, 1, 500); 
    const ms = differenceInMilliseconds(date2, date1); // 1500
    
    注意​: 常用于性能计时或高精度时间间隔

​六、时间范围函数​

获取日期的开始或结束时间点。

  • startOfDay(date): 返回指定日期的当天开始时间(00:00:00)
    import{ format,startOfDay } from 'date-fns'
    
    //startOfDay是按 UTC 时间 计算的,而不是本地时间(UTC+8)
    //format默认是按 本地时间(UTC+8) 计算的,而不是UTC时间
    //注意:月份是从0开始计算的,5表示6月
    format(startOfDay(new Date(2025, 5, 10, 14, 15, 59, 680)), "yyyy-MM-dd HH:mm:ss.SSS") //输出:2025-06-10 00:00:00.000
    
  • endOfDay(date): 返回指定日期的当天结束时间(23:59:59.999)
    import{ format,endOfDay} from 'date-fns'
    
    //startOfDay是按 UTC 时间 计算的,而不是本地时间(UTC+8)
    //format默认是按 本地时间(UTC+8) 计算的,而不是UTC时间	
    //注意:月份是从0开始计算的,5表示6月
    format(endOfDay(new Date(2025, 5, 10, 14, 15, 59, 680)), "yyyy-MM-dd HH:mm:ss.SSS")//输出:2025-06-10 23:59:59.999
    
  • startOfMonth(date): 返回指定日期所在月份的起始时间(当月第一天 00:00:00)
    import{ format,startOfMonth} from 'date-fns'
    
    //startOfMonth是按 UTC 时间 计算的,而不是本地时间(UTC+8)
    //format默认是按 本地时间(UTC+8) 计算的,而不是UTC时间
    //注意:月份是从0开始计算的,5表示6月
    format(startOfMonth(new Date(2025, 5, 10, 14, 15, 59, 680)), "yyyy-MM-dd HH:mm:ss.SSS")//输出:2025-06-01 00:00:00.000
    
  • endOfMonth(date): 返回指定日期所在月份的结束时间(当月最后一天 23:59:59.999)
    import{ format,endOfMonth} from 'date-fns'
    
    //endOfMonth是按 UTC 时间 计算的,而不是本地时间(UTC+8)
    //format默认是按 本地时间(UTC+8) 计算的,而不是UTC时间
    //注意:月份是从0开始计算的,5表示6月
    format(endOfMonth(new Date(2025, 5, 10, 14, 15, 59, 680)), "yyyy-MM-dd HH:mm:ss.SSS")//输出:2025-06-30 23:59:59.999
    

​七、日期组件获取函数​

从日期中提取特定时间单位的值。

  • getYear(date): 获取年份(四位数,如 2025)

    import{ getYear } from 'date-fns'
    getYear(new Date(2025, 5, 10, 14, 15, 59)); //输出:2025
    

    注意:不同于原生 Date.getYear()(返回年份减1900),此函数返回完整年份

  • getMonth(date): 获取月份(0-11,0表示一月)

    import{ getMonth } from 'date-fns'
    getMonth(new Date(2025, 5, 10, 14, 15, 59)); //输出:5  (注意这个5是指6月,月份是从0开始的)
    
  • getDate(date): 获取日期在月份中的天数(1-31)

    import{ getDate } from 'date-fns'
    //getDate(date: Date | number): number
    getDate(new Date(2025, 5, 10, 14, 15, 59)); //输出:10
    
  • getHours(date): 获取小时(0-23)

    import{ getHours } from 'date-fns'
    getHours(new Date(2025, 5, 10, 14, 15, 59));//输出:14
    
  • getMinutes(date): 获取分钟(0-59)

    import{ getMinutes } from 'date-fns'
    getMinutes(new Date(2025, 5, 10, 14, 15, 59));//输出:15
    
  • getSeconds(date): 获取秒数(0-59)

    import{ getSeconds } from 'date-fns'
    getSeconds(new Date(2025, 5, 10, 14, 15, 59)); //输出:59
    
  • getMilliseconds(date): 获取毫秒数(0-999)

    import{ getMilliseconds } from 'date-fns'
    getMilliseconds(new Date(2025, 5, 10, 14, 15, 59, 680)); //输出:680
    
  • getISOWeek(date): 获取日期在 ISO 周系统中的周数(1-53)

    import{ getISOWeek } from 'date-fns'
    
    //getISOWeek(date: Date | number): number	
    getISOWeek(new Date(2025, 5, 10)); // 输出:24
    

​八、其他工具函数​

辅助性日期转换或计算。

  • weeksToDays(weeks): 将周数转换为天数(如 weeksToDays(2) 返回 14)

    import{ weeksToDays } from 'date-fns'
    weeksToDays(2); // 14
    
  • parse(dateString, formatString, baseDate): 解析字符串为日期对象,需指定格式

    import{ parse } from 'date-fns'
    parse("2025-06-10", "yyyy-MM-dd", new Date()); // Date对象 ,如果日期解析失败则返回:Invalid Date
    
  • parseISO(isoString): 解析 ISO 8601 格式字符串为日期对象(自动处理时区)

    import{ parseISO } from 'date-fns'
    parseISO("2025-06-10T14:30:00Z"); // Date对象(UTC时间)
    
  • max(datesArray): 返回日期数组中的最晚日期
    案例1:

    import{ format,max} from 'date-fns' 
    
    //在 JavaScript 中,new Date(2025, 0, 10) 创建的日期对象基于 ​本地时区​(如北京时间 UTC+8)
    //本地时间:2025-01-10 00:00:00(北京时间)。
    //转换为 UTC 时间:减去时区偏移(8小时)→ 2025-01-09T16:00:00.000Z
    const dates = [new Date(2025, 0, 10), new Date(2025, 5, 10)];
    const maxDate = max(dates);
    //使用 format 函数将结果转换为本地时间字符串,避免 UTC 转换:
    format(maxDate, "yyyy-MM-dd HH:mm:ss"); // 输出:2025-06-10 00:00:00(本地时间)
    

    案例2:

    import { parseISO, max, min, format } from "date-fns"
    
    //UTC 转本地时区:
    //当你用 format 输出时,date-fns 会将 UTC 时间的 00:00:00 转换为你的本地时区时间(比如 UTC+8),
    //因此:
    //"2025-01-12" → UTC 00:00:00 → 本地时间 08:00:00(UTC+8)
    //"2025-05-12" → UTC 00:00:00 → 本地时间 08:00:00(UTC+8)
    //为什么输出 08:00:00?
    //即使你只比较日期部分,max 仍然会返回完整的 Date 对象(包含时间部分)。由于你的本地时区是UTC+8,format 会将 UTC 的 00:00:00 显示为 08:00:00
    //解决方法:将日期显式设置时间为同一时区。使用 parseISO 或 parse 函数时指定时区:
    const dates = ["2025-01-12", "2025-05-12"].map(d => parseISO(d)) //map(d => parseISO(d))表示将数组中的日期转换为UTC时间
    const maxDate = max(dates);
    
    
    //使用 format 函数将结果转换为本地时间字符串,避免 UTC 转换:
    console.log(format(maxDate, "yyyy-MM-dd HH:mm:ss")); // 输出:2025-05-12 00:00:00
    
  • min(datesArray): 返回日期数组中的最早日期
    案例1:

    import { format, max, parseISO } from "date-fns";
    
    //在 JavaScript 中,new Date(2025, 0, 10) 创建的日期对象基于 ​本地时区​(如北京时间 UTC+8)
    //本地时间:2025-01-10 00:00:00(北京时间)。
    //转换为 UTC 时间:减去时区偏移(8小时)→ 2025-01-09T16:00:00.000Z
    const dates = [new Date(2025, 0, 10), new Date(2025, 5, 10)];
    const minDate = min(dates);
    //使用 format 函数将结果转换为本地时间字符串,避免 UTC 转换:
    format(minDate, "yyyy-MM-dd HH:mm:ss"); // 输出:2025-01-10 00:00:00(本地时间)
    

    案例2:

    import { parseISO, max, min, format } from "date-fns"
    
    //UTC 转本地时区:
    //当你用 format 输出时,date-fns 会将 UTC 时间的 00:00:00 转换为你的本地时区时间(比如 UTC+8),
    //因此:
    //"2025-01-12" → UTC 00:00:00 → 本地时间 08:00:00(UTC+8)
    //"2025-05-12" → UTC 00:00:00 → 本地时间 08:00:00(UTC+8)
    //为什么输出 08:00:00?
    //即使你只比较日期部分,max 仍然会返回完整的 Date 对象(包含时间部分)。由于你的本地时区是UTC+8,format 会将 UTC 的 00:00:00 显示为 08:00:00
    //解决方法:将日期显式设置时间为同一时区。使用 parseISO 或 parse 函数时指定时区:
    const dates = ["2025-01-12", "2025-05-12"].map(d => parseISO(d)) //map(d => parseISO(d))表示将数组中的日期转换为UTC时间
    const minDate = min(dates);
    
    
    //使用 format 函数将结果转换为本地时间字符串,避免 UTC 转换:
    console.log(format(minDate, "yyyy-MM-dd HH:mm:ss")); // 输出:2025-01-12 00:00:00
    

posted on 2025-11-26 16:38  潇湘一夜雨  阅读(86)  评论(0)    收藏  举报

导航