1.Uint8Array
作用是创建一个长度为 16 的 8 位无符号整数数组,用于存储原始二进制数据
const buffer = new Uint8Array(16)
通过构造函数创建了一个 `Uint8Array` 实例,参数 `16` 表示数组的长度为 16
- 该数组共有 16 个元素,每个元素是 1 字节(8 位);
- 总占用内存为 `16 × 1字节 = 16字节`;
- 初始状态下,所有元素默认值为 `0`。
Uint8Array是JavaScript 中的一种类型化数组(TypedArray) ,专门用于存储 8 位(1 字节)的无符号整数(取值范围:0~255)
- 长度固定(创建后不可动态改变);
- 每个元素占用固定内存(1 字节);
- 直接操作二进制数据,性能更高,适合处理原始字节流。
crypto.getRandomValues(buffer); 是一行用于生成加密安全的随机数据的代码,在生成 UUID 的场景中,它负责为 UUID 提供基础的随机字节。
crypto
是浏览器提供的 Web Crypto API 的全局对象,专门用于处理加密、解密、签名、生成随机数等安全相关操作,提供的随机数生成能力比普通的 Math.random() 更安全。
getRandomValues() 方法
是 crypto 对象的核心方法之一,作用是:向传入的 “二进制缓冲区” 填充加密安全的随机数值。
- 生成的随机数不可预测(安全性远高于
Math.random(),后者可能被恶意预测); - 直接修改传入的缓冲区(不会返回新对象),效率高;
- 只能处理 “类型化数组”(如
Uint8Array、Uint16Array等,用于存储二进制数据)。
buffer 参数
这里是之前创建的 Uint8Array(16) 实例(16 字节的 8 位无符号整数数组)。调用 getRandomValues(buffer) 后,这个 16 字节的数组会被随机填充 0~255 之间的整数(每个元素对应 1 字节的随机数据),覆盖初始的全 0 值。
UUID 的结构基础
UUID 是 128 位(16 字节)的唯一标识符,格式为xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx(共 36 个字符),其中:
M代表版本号(4 位),位于第 6 个字节的高 4 位;N代表变体标识(2-3 位),位于第 8 个字节的高 2-3 位;- 其他部分为随机数(版本 4 UUID 的核心)。
buffer[6] = (buffer[6] & 0x0f) | 0x40;
-
buffer[6]:16 字节数组中的第 7 个字节(UUID 格式中对应xxxx-xxxx-Mxxx-...的M所在字节),原本存储的是随机值(0~255)。 -
buffer[6] & 0x0f:0x0f是十六进制,二进制为00001111;- 与运算(
&)会清空该字节的高 4 位,只保留低 4 位(随机值),避免破坏低 4 位的随机性。
-
| 0x40:0x40是十六进制,二进制为01000000;- 或运算(
|)会将该字节的第 4 位(从 0 开始数第 6 位)设置为 1,最终高 4 位变为0100(对应十六进制4),标识版本为 4。
0x0f 是一个十六进制表示的整数
-
前缀
0x:表示这是一个十六进制数(基数为 16),是编程语言中(如 JavaScript、C、Java 等)表示十六进制的标准语法。 -
数值
0f:十六进制中,0-9对应十进制的0-9,a-f(或A-F)对应十进制的10-15。因此0x0f中,0和f是十六进制数字,整体表示:- 十进制:
0×16¹ + 15×16⁰ = 15 - 二进制:
00001111(每个十六进制数字对应 4 位二进制,0→0000,f→1111)
- 十进制:
“高 4 位” “低 4 位”
在计算机中,一个 “字节(Byte)” 由 8 个 “位(Bit)” 组成(1 Byte = 8 Bit)。“高 4 位” 和 “低 4 位” 是针对这 8 位二进制数的左右划分:
- 低 4 位:指 8 位二进制数中右边的 4 位(编号通常为第 0~3 位);
- 高 4 位:指 8 位二进制数中左边的 4 位(编号通常为第 4~7 位)。
以一个 8 位二进制数 1011 0101
- 左边的
1011是高 4 位(对应十六进制的b); - 右边的
0101是低 4 位(对应十六进制的5);
在 buffer[6] = (buffer[6] & 0x0f) | 0x40 中
buffer[6]是一个字节(8 位),假设原始值为1011 0101(高 4 位1011,低 4 位0101);0x0f是0000 1111(高 4 位全 0,低 4 位全 1),与运算后只保留低 4 位0101;0x40是0100 0000(高 4 位的第 4 位为 1,其余位 0),或运算后高 4 位被固定为0100;- 最终结果为
0100 0101(高 4 位0100是版本号,低 4 位0101保留原始随机值)。
8 位二进制数从左到右,前 4 位是高 4 位,后 4 位是低 4 位。
将 16 字节的二进制数组(buffer)转换为符合 UUID 格式的字符串
Array.from(buffer)
-
作用:将
buffer(Uint8Array类型的 16 字节数组)转换为普通的 JavaScript 数组。Array.from(buffer).map((byte, index) => { const h = byte.toString(16).padStart(2, '0') switch (index) { case 1: case 2: case 3: case 4: return `-${h}`; } }).join('').toLowerCase()遍历数组中的每个字节(
byte),根据其索引(index)进行格式化处理,返回一个新的字符串数组。-
byte.toString(16):将字节的十进制值转为十六进制字符串(例如:255 →'ff',15 →'f',10 →'a'); -
.padStart(2, '0'):确保十六进制字符串长度为 2 位,不足则在前面补 0(例如:15 →'f'补全为'0f',255 →'ff'保持不变)。每个字节必须对应 2 位十六进制(1 字节 = 8 位 = 2 个十六进制位)
-
switch (index) {
case 4: case 6: case 8: case 10: return -${h};
在指定索引位置插入连字符(-),让最终字符串符合 UUID 的格式规范:UUID 固定为 36 个字符,格式为 8-4-4-4-12(即分 5 段,用 4 个连字符分隔),例如:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。
索引与连字符的对应关系:
-
16 个字节对应 32 个十六进制位(每个字节 2 位),需要在第 8、12、16、20 位后插入连字符(共 4 个)。
-
字节索引(0~15)与连字符位置的对应:
- 第 4 个字节(
index=4):对应第 8 个十六进制位(4×2=8),之后插入-→ 返回-${hex}; - 第 6 个字节(
index=6):对应第 12 个十六进制位(6×2=12),之后插入-→ 返回-${hex}; - 第 8 个字节(
index=8):对应第 16 个十六进制位(8×2=16),之后插入-→ 返回-${hex}; - 第 10 个字节(
index=10):对应第 20 个十六进制位(10×2=20),之后插入-→ 返回-${hex};
- 第 4 个字节(
.join('')
- 作用:将
map返回的字符串数组拼接成一个完整的字符串。 - 例如:经过
map处理后的数组可能是['f8','1d','4f','ae','-7d','ec','-11','d0','-a7','65','-00','a0','c9','1e','6b','f6'],join('')后得到'f81d4fae-7dec-11d0-a765-00a0c91e6bf6'。
.toLowerCase()
- 作用:将字符串中的所有十六进制字符转为小写(UUID 规范通常使用小写,例如
FF→ff)
2.JSON.parse
作用:将 JSON 格式的字符串转换为对应的 JavaScript 对象或值。
场景:后端接口会传入一大串字符串,前端需要将这些数据渲染到页面上,就要进行转换。
如:
// JSON 格式的字符串
const jsonString = '{"name":"张三","age":25,"hobbies":["读书","运动"]}';
// 接收一个符合 JSON 格式的字符串作为参数,将其转换为 JavaScript 中的对应数据类型(对象、数组、字符串、数字、布尔值或 null)
const obj = JSON.parse(jsonString);
console.log(obj.name); // 输出: "张三"
console.log(obj.age); // 输出: 25
console.log(obj.hobbies[0]); // 输出: "读书"
如果传入的字符串不符合 JSON 格式规范,会抛出 SyntaxError 异常。
const jsonString = '{"name":"张三", age:25}'; // 这里 age 没有用双引号
try {
// 解析 JSON 字符串
const obj = JSON.parse(jsonString);
} catch (error) {
// 捕获并处理异常
if (error instanceof SyntaxError) {
console.error('JSON 格式错误:', error.message);
} else {
// 处理其他可能的异常
console.error('发生未知错误:', error);
}
}
用户友好方式:
function safeParseJson(jsonString, defaultValue = {}) {
try {
return jsonString ? JSON.parse(jsonString) : defaultValue;
} catch (error) {
// 捕获并处理异常
if (error instanceof SyntaxError) {
console.error('JSON 格式错误:', error.message);
} else {
// 处理其他可能的异常
console.error('发生未知错误:', error);
}
return defaultValue; // 解析失败时返回默认值
}
}
// 使用示例
const data = safeParseJson('获取到的数据', { name: '未知用户' });
JSON.parse() 是 JavaScript 中将 JSON 字符串 "还原" 为可操作的数据结构的关键方法,是前后端数据交互和数据存储时的常用工具。
在实际开发中,接口返回的 JSON 数据可能是对象({})或数组([]),因此解析后通常需要用 Array.isArray() 判断是否为数组,再进行对应处理。
场景:JSON必须是数组格式,但是后端传入的字符串解析后的json不是数组格式。
- JSON 格式是一个总称,包含了对象、数组、基础类型三种表现形式。
- JSON 数组格式是 JSON 格式的子集,仅指用
[]包裹的有序数据列表。
判断是否是数组,不是数组自动加入[].进行格式化显示。将数组数据带有[]展示出来。
let jsonData
try {
//解析成json
jsonData = JSON.parse(inputValue)
// 检查是否为数组
if (!Array.isArray(jsonData)) {
return new Promise((resolve) => {
ElMessageBox.confirm(
'输入的JSON必须是数组格式(节点集合),是否进行格式转换?',
'格式错误',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}
).then(() => {
// 用数组包裹对象并格式化显示
textareaValue.value = JSON.stringify([jsonData], null, 2)
resolve(true);
}).catch(() => {
console.log('用户取消格式转换')
resolve(false)
})
})
}
return true
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e)
ElMessage.error(`输入内容不是合法的JSON格式!\n错误原因:${errorMsg}`)
return false
}
3.JSON.stringify()
将 JavaScript 值转换为 JSON 字符串的内置方法,对象放入一个数组中,然后转换为格式化的 JSON 字符串,最终结果会是一个包含原对象的 JSON 数组字符串。
// 第一个参数 `[jsonData]`:要转换的 JavaScript 值(这里将 `jsonData` 包装在数组中)
// replacer 参数,为 `null` 表示转换所有可序列化的属性
// 第三个参数 `2`: space 参数,表示使用 2 个空格进行缩进,使输出的 JSON 字符串更易读
JSON.stringify([jsonData], null, 2)
jsonData是{name: "John", age: 30}
[
{
"name": "John",
"age": 30
}
]
将非数组值转为数组格式如果原始数据不是数组,但你希望输出为 JSON 数组(包裹在 [] 中),可以手动将其放入数组,这种写法会强制将结果包裹在数组中,即使原数据不是数组类型。
const data = { id: 1, name: "Alice" };
// 手动包裹为数组
const jsonArray = JSON.stringify([data], null, 2);
console.log(jsonArray);
// 输出:
// [
// {
// "id": 1,
// "name": "Alice"
// }
// ]
JSON.stringify(value[, replacer[, space]])
参数说明
value(必需):要转换为 JSON 字符串的值(可以是对象、数组、字符串、数字、布尔值或 null)
replacer(可选):
- 若为函数:对每个键值对进行处理,返回值将被序列化
- 若为数组:仅包含数组中列出的属性才会被序列化
- 若为 null 或未提供:所有可序列化的属性都会被包含
space(可选):
- 用于格式化输出的缩进空格数(1-10 之间的数字)
- 若为字符串:将用该字符串作为缩进(最多前 10 个字符)
- 若为 0、null 或未提供:输出将没有缩进和空格
// 带格式化
console.log(JSON.stringify(obj, null, 2));
// 输出格式化的JSON:
// {
// "name": "Alice",
// "age": 25,
// "isStudent": false
// }
// 使用replacer函数过滤属性
console.log(JSON.stringify(obj, (key, value) => {
return key === "age" ? undefined : value; // 排除age属性
}));
{"name":"Alice","isStudent":false}
const numbers = [1, 2, 3, 4, 5];
// 只保留偶数
const evenNumbersJson = JSON.stringify(numbers, (key, value) => {
// 注意:数组的索引会作为key,元素值作为value
return typeof value === 'number' && value % 2 === 0 ? value : undefined;
}, 2);
console.log(evenNumbersJson);
// 输出: [null, 2, null, 4, null]
// 注意:数组空位会被转为null
4.extends 继承
extends 关键字用于实现类的继承,是 ES6(ECMAScript 2015)引入的面向对象编程特性,用于创建一个子类并继承父类的属性和方法。
class 子类名 extends 父类名 {
// 子类的构造函数和方法
}
1.子类会自动获得父类中定义的所有非私有属性和方法(ES6 中通过 # 定义私有成员,子类无法直接访问)。
2.子类可以在父类基础上添加新的属性和方法,或重写(覆盖)父类的方法
3.JavaScript 不支持多继承,一个子类只能直接继承一个父类,但可以通过其他方式(如混入 mixin)实现类似多继承的效果。
// 继承关系: A是子类(派生类),B是父类(基类)。A会自动获得B中定义的属性和方法(除了私有成员)。
// 代码复用:通过继承,A不需要重新实现 B 中已有的功能,可以直接使用或根据需要进行重写。
// 扩展功能: A可以在B的基础上添加新的属性和方法,实现特定于自身的功能。
class A extends B
// 父类
class B {
constructor(name) {
this.name = name;
}
// 父类的方法
speak() {
console.log(`${this.name} 发出声音`);
}
}
// 子类继承父类
class A extends B {
// 重写父类的方法
speak() {
console.log(`${this.name} 汪汪叫`);
}
// 子类新增的方法
fetch() {
console.log(`${this.name} 叼来了球`);
}
}
// 使用子类
const a = new A("旺财");
a.speak(); // 输出:"旺财 汪汪叫"(调用重写后的方法)
a.fetch(); // 输出:"旺财 叼来了球"(调用子类新增方法)
super关键字:在子类的构造函数中,必须先调用 super()才能使用 this,super()用于调用父类的构造函数。
class A extends B {
constructor(name, color) {
super(name); // 调用父类构造函数
this.color = color; // 子类新增属性
}
}
5.constructor构造函数
constructor 是类被实例化(使用 new 关键字创建对象)时自动调用的方法,用于初始化实例的属性和执行初始化逻辑。
class Person {
// 构造函数
constructor(name, age) {
// 初始化实例属性
this.name = name;
this.age = age;
}
// 类的其他方法
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
// 创建实例时会自动调用 constructor
const person = new Person("Alice", 30);
person.sayHello(); // 输出: Hello, I'm Alice
const instance = new AssignmentNode(someNodes);
// 当通过 `new AssignmentNode(...)` 创建实例时,此时会自动调用上面的 constructor 方法
super(...) 用于调用父类的构造函数,将参数传递给父类。访问和调用父类(超类)的方法,主要在继承场景中使用。
在子类的 constructor 中,super() 用于调用父类的构造函数:
//父类
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
//子类 基础父类 Animal
class Dog extends Animal {
//new 时 接收传入值,constructor 会被自动调用。
constructor(name, breed) {
// 必须先调用 super() 才能使用 this
super(name); // 调用父类的 constructor
this.breed = breed; // 子类自己的属性
}
bark() {
console.log(`${this.name} is barking`);
}
}
const dog = new Dog("Buddy", "Golden Retriever");
dog.eat(); // 输出: Buddy is eating
dog.bark(); // 输出: Buddy is barking
class Cat extends Animal {
constructor(name) {
super(name);
}
eat() {
// 调用父类的 eat 方法
super.eat();
console.log("Cats like fish");
}
}
const cat = new Cat("Whiskers");
cat.eat();
// 输出:
// Whiskers is eating
// Cats like fish
6.?. 被称为可选链操作符
作用是安全地访问嵌套对象的属性或方法,避免因中间某个属性不存在而导致的 TypeError 错误,当访问一个可能为 null 或 undefined 的对象的属性时,使用 ?. 可以简化判断逻辑。如果链中的某个环节为 null 或 undefined,表达式会直接返回 undefined,而不会抛出错误。
?. 不能用于赋值操作,例如 user?.name = "Bob" 会报错。
const user = {
name: "Alice",
address: {
city: "Beijing"
}
};
要访问 `user.address.city`,需要先判断 `user` 和 `address` 是否存在:
const city = user && user.address && user.address.city;
可选链
const city = user?.address?.city;
const user = { name: "Alice" };
const city = user?.address?.city; // undefined(不会报错)
const arr = null;
const first = arr?.[0]; // undefined
const obj = {
func: () => "Hello"
};
const result = obj.func?.(); // "Hello"
const obj2 = {};
const result2 = obj2.func?.(); // undefined(不会报错)
7.对象解构
用于从对象或数组中提取属性 / 元素并赋值给变量。它能大幅简化代码,尤其适合处理复杂数据结构。
const user = {
name: "张三",
age: 25,
gender: "男"
};
// 解构赋值
const { name, age, gender } = user;
console.log(name); // "张三"
console.log(age); // 25
console.log(gender); // "男"
等价于传统写法:
const name = user.name;
const age = user.age;
const gender = user.gender;
const { a, d, c } = table[index]
等价于更繁琐的写法:
const a = table[index].a;
const d = table[index].d;
const c = table[index].c;
想使用属性名作为变量名,可以用 新变量名: 原属性名
const user = { name: "李四", age: 30 };
// 将 name 重命名为 userName,age 重命名为 userAge
const { name: userName, age: userAge } = user;
console.log(userName); // "李四"
console.log(userAge); // 30
设置默认值
const user = { name: "王五" };
// age 不存在时,默认值为 18
const { name, age = 18 } = user;
console.log(age); // 18(因为 user.age 未定义)
解构嵌套对象
const user = {
name: "赵六",
info: {
height: 180,
weight: 70,
address: { city: "北京" }
}
};
// 解构嵌套属性
const {
name,
info: {
height,
address: { city }
}
} = user;
console.log(name); // "赵六"
console.log(height); // 180
console.log(city); // "北京"
解构函数参数
// 传统写法
function V(user) {
console.log(user.name, user.age);
}
// 解构写法(更简洁)
function V({ name, age }) {
console.log(name, age);
}
V({ name: "常常", age: 1 }); // 输出:常常 1
可结合默认值使用(防止参数未传递时报错)
// 参数默认值 + 解构默认值
function printUser({ name = "匿名", age = 0 } = {}) {
console.log(name, age);
}
printUser(); // 输出:匿名 0(未传参时使用默认值)
数组解构
const [a, b, c] = [10, 20, 30];
console.log(a); // 10
console.log(b); // 20
// 跳过元素
const [x, , y] = [1, 2, 3];
console.log(x); // 1
console.log(y); // 3
// 剩余元素(...)
const [first, ...rest] = [1, 2, 3, 4];
console.log(rest); // [2, 3, 4]
8.includes()
适合用于简单的包含关系检测
数组中的 includes() 用法
array.includes(searchElement[, fromIndex])
-
searchElement:必需,要查找的元素 -
fromIndex:可选,开始查找的索引位置(默认从 0 开始)const fruits = ['apple', 'banana', 'orange']; // 检测是否包含 'banana' console.log(fruits.includes('banana')); // true // 检测是否包含 'grape' console.log(fruits.includes('grape')); // false // 从索引 2 开始查找 'orange' console.log(fruits.includes('orange', 2)); // true // 从索引 3 开始查找(超出数组长度,返回 false) console.log(fruits.includes('apple', 3)); // false
字符串中的 includes() 用法
string.includes(searchString[, position])
-
searchString:必需,要查找的子字符串 -
position:可选,开始查找的位置(默认从 0 开始)const str = 'Hello, world!'; // 检测是否包含 'world' console.log(str.includes('world')); // true // 检测是否包含 'foo' console.log(str.includes('foo')); // false // 从索引 7 开始查找 'world' console.log(str.includes('world', 7)); // true // 从索引 8 开始查找 console.log(str.includes('world', 8)); // false
区分大小写
['Apple', 'banana'].includes('apple'); // false(大小写不同)
对于引用类型的值(如对象),includes() 检测的是内存地址是否相同
const obj = { name: 'test' };
const arr = [obj];
console.log(arr.includes(obj)); // true
console.log(arr.includes({ name: 'test' })); // false(新对象,内存地址不同)
检测 NaN(与 indexOf() 不同,indexOf() 无法检测 NaN)
const nums = [1, 2, NaN, 4];
console.log(nums.includes(NaN)); // true
console.log(nums.indexOf(NaN)); // -1(无法检测)
9.createSerialQueue
用于创建串行任务队列的工具函数,核心作用是保证异步任务按顺序执行,前一个任务完成后再执行下一个,避免并发操作导致的状态混乱或数据不一致,必须按顺序排队通过,不能并行抢道。
- 串行执行:将多个异步任务(如 API 请求、数据处理)按添加顺序依次执行,解决异步操作 “并行无序” 的问题。
- 状态管理:维护任务队列的执行状态(如 “等待中”“执行中”“已完成”),避免重复执行或任务冲突。
- 异步协调:提供接口(如
enqueue添加任务、queueComplete等待队列结束),方便开发者控制任务流程。
业务场景
1.连续的 API 请求(依赖前一次结果)
例如 “创建订单→扣减库存→更新” 三步操作,后一步需要前一步的结果。
-并行调用 API,扣减库存可能因没有订单 ID 而失败,用串行队列保证顺序,前一个 API 成功后再执行下一个。
2.高频触发的操作
例如 用户快速点击按钮,多次触发逻辑,可能导致数据覆盖。
-并行执行会导致请求顺序与到达服务器的顺序不一致,最终数据可能出错。用串行队列将多次保存请求排队,按点击顺序依次执行,避免冲突。
3.操作同一 DOM / 数据
例如 多个异步任务同时修改同一个 DOM 元素(如更新列表数据),可能导致 UI 闪烁或数据错乱。
-串行执行任务,确保前一个修改完成后再进行下一个。
4.事务性操作
例如 业务逻辑执行和数据标签更新必须按顺序执行,且需等待两者都完成后再进行下一步。
-如果标签更新在逻辑执行前完成,会使用旧数据;如果并行执行,可能导致数据状态不一致。
createSerialQueue使用
//定义一个名为createSerialQueue的函数,接收一个可选参数 initialTask(初始任务,可选)
//创建并返回一个串行任务队列的控制工具
function createSerialQueue(initialTask) {
// 初始化队列和状态变量,用于存储待执行的任务队列.如果传入了 `initialTask`(初始任务),
则队列初始化为包含该任务的数组;否则初始化为空数组。
let queue = initialTask ? [initialTask] : [];
let isProcessing = false; // 是否正在执行任务,防止并发处理任务(同一时间只允许一个任务执行)
// 处理队列:按顺序执行任务
async function processQueue() {
// 若 isProcessing 为 true(正在执行任务),则直接返回(避免重复处理)
// 若 queue.length === 0(队列中无任务),则直接返回(无任务可执行)
// 作用:确保同一时间只有一个任务在执行,且队列空时不处理
if (isProcessing || queue.length === 0) return;
// 将 isProcessing 设为 true,标记 “正在执行任务”。
// 作用:锁定队列,防止其他任务并行执行。
isProcessing = true;
//queue.shift()移除并返回队列中的第一个任务(FIFO 先进先出原则,按添加顺序取出任务,保证串行执行。
const task = queue.shift();
// 若任务执行失败(抛出错误),则在 `catch` 中打印错误日志。
// 确保单个任务失败不会阻断整个队列,且错误可被捕获。
try {
await task(); // 执行任务
} catch (error) {
console.error("任务执行失败:", error);
} finally {
isProcessing = false;
processQueue(); // 继续执行下一个任务
}
}
// 添加任务到队列,定义 enqueue方法,接收一个 `task` 参数
function enqueue(task) {
// 将传入的 `task` 添加到队列末尾(`push` 方法)。
// 按调用顺序将任务加入队列,保证执行顺序。
queue.push(task);
//// 触发队列处理,添加任务后立即触发执行(若队列未在执行中,则开始执行;若正在执行,则后续由 `processQueue` 递归处理
processQueue();
//返回一个 Promise 对象,允许外部通过 `await` 等待 “当前添加的任务” 执行完成。
return new Promise(resolve => {
// 可等待当前任务完成(可选),用于轮询检查任务是否已执行完成
const check = () => {
//`!isProcessing`:队列未在执行任务(当前任务已完成);
// `!queue.includes(task)`:当前任务已从队列中移除(已执行)。
//若满足条件,调用 `resolve()` 结束 Promise(通知外部 “当前任务已完成”)
if (!isProcessing && !queue.includes(task)) resolve();
// 若不满足条件,通过 `setTimeout` 延迟 10ms 后再次调用 `check`(轮询)。
//作用:持续检查任务状态,直到任务完成。
else setTimeout(check, 10);
};
check();
});
}
// 等待队列中所有任务完成,用于等待队列中**所有任务**执行完成。
function queueComplete() {
// 返回一个 Promise 对象,当队列中所有任务完成后 resolve。
return new Promise(resolve => {
//定义内部函数 `check`,用于轮询检查队列是否为空且无任务执行。
const check = () => {
if (!isProcessing && queue.length === 0) resolve();
//若不满足条件,延迟 10ms 后再次轮询检查。
else setTimeout(check, 10);
};
check();
});
}
//函数返回一个对象,包含 `enqueue`(添加任务)和 `queueComplete`(等待所有任务完成)两个方法。
//作用:暴露队列的操作接口,供外部使用。
return { enqueue, queueComplete };
}
使用步骤
1.创建队列:初始化时可传入一个初始任务
const { enqueue, queueComplete } = createSerialQueue(initialTask);
2.添加任务:通过 enqueue 方法添加后续任务
// 添加任务1
await enqueue(async () => {
const order = await OrderAPI(data);
return order.id; // 返回结果供后续任务使用
});
// 添加任务2
await enqueue(async (orderId) => { // 支持接收前一个任务的结果
await reduceStockAPI(orderId, quantity);
});
3.等待队列完成:通过 queueComplete 等待所有任务执行完毕
await queueComplete();
简单逻辑 :
- 向队列中添加异步任务enqueue
- 等待队列中所有任务执行完毕queueComplete
const { enqueue, queueComplete } = createSerialQueue(() => {
console.log("所有任务完成,执行收尾操作"); // 创建队列,指定收尾操作(如更新数据标签)
});
// 添加任务1
enqueue(async () => {
console.log("任务1开始");
await delay(1000); // 模拟异步操作
console.log("任务1完成");
});
// 添加任务2
enqueue(async () => {
console.log("任务2开始");
await delay(500); // 模拟异步操作
console.log("任务2完成");
});
// 等待所有任务完成
queueComplete().then(() => {
console.log("外部:队列已全部执行完毕");
});
执行顺序如下:
-
调用
enqueue添加任务 1,此时isRunning = false,触发processQueue。 -
processQueue取出任务 1 执行,打印 “任务 1 开始”→ 等待 1 秒 → 打印 “任务 1 完成”,通过resolve通知任务 1 完成。 -
finally中递归调用processQueue,此时队列中有任务 2,取出执行:打印 “任务 2 开始”→ 等待 0.5 秒 → 打印 “任务 2 完成”。 -
再次递归调用
processQueue,发现队列为空:- 标记
isRunning = false; - 执行
updateName,打印 “所有任务完成,执行收尾操作”; - 调用
resolveQueueComplete,触发queueComplete()的回调,打印 “外部:队列已全部执行完毕”; - 重置
queueCompletePromise,供下次添加任务使用。
- 标记
核心含义
- 串行执行保证:通过队列 + 递归,强制任务按添加顺序依次执行,解决异步任务并行导致的 “顺序混乱” 问题(例如:前一个任务的结果是后一个任务的依赖)。
- 自动收尾机制:所有任务完成后,会自动执行
updateName函数(参数传入),适合用于 “批量任务执行完毕后统一更新数据 / UI” 的场景(如示例中更新#Name标签)。 - 外部可感知状态:通过
queueComplete()返回的 Promise,外部可以等待整个队列执行完毕后再进行后续操作(如关闭加载动画、提示用户 “全部完成”)。 - 错误隔离:单个任务执行失败(
catch捕获错误)不会阻断整个队列,只会通过reject通知该任务的调用者,队列会继续执行下一个任务。
10.for...of
用于遍历可迭代对象的循环语法,更简洁且专注于迭代元素本身。
-
遍历可迭代对象可迭代对象包括:数组(Array)、字符串(String)、Map、Set、类数组对象(如
arguments、DOM 集合)等。 -
直接获取元素值循环变量直接指向当前迭代的元素值,而非索引或键名(区别于
for循环的索引和for...in的键名)。 -
无法直接获取索引若需要索引,需配合
Array.prototype.entries()使用。 -
不可中途修改迭代对象的长度不会像传统
for循环那样因修改数组长度导致逻辑错误。for (const 元素变量 of 可迭代对象) { // 循环体:对每个元素执行操作 } const arr = ['a', 'b', 'c']; for (const item of arr) { console.log(item); // 依次输出:'a'、'b'、'c' } const str = 'hello'; for (const char of str) { console.log(char); // 依次输出:'h'、'e'、'l'、'l'、'o' } const map = new Map(); map.set('name', 'Alice'); map.set('age', 20); for (const [key, value] of map) { console.log(key, value); // 依次输出:'name' 'Alice'、'age' 20 } const fruits = ['apple', 'banana']; for (const [index, fruit] of fruits.entries()) { console.log(index, fruit); // 输出:0 'apple'、1 'banana' }
11.Object.assign
用于对象合并的方法,它可以将一个或多个源对象的可枚举属性复制到目标对象,并返回合并后的目标对象。
运用场景:开发工具类、组件时的配置合并。
// 默认配置
const defaultConfig = {
width: 300,
height: 200,
title: '提示',
};
// 用户传入的配置
const userConfig = {
title: '标题',
height: 400,
};
// 合并配置(用户配置覆盖默认配置,其他属性保留默认)
const finalConfig = Object.assign({}, defaultConfig, userConfig);
// {
// width: 300, // 保留默认
// height: 400, // 合并后的数据
// title: '标题', // 合并后的数据
// }
对象的浅拷贝(避免引用污染)
const user = { name: '张三', age: 20, address: { city: '北京' } };
// 浅拷贝用户信息(基本类型属性独立,引用类型仍共享地址)
const userCopy = Object.assign({}, user);
// 修改副本的基本类型属性,不影响原对象
userCopy.name = '李四';
console.log(user.name); // 张三(原对象不受影响)
//引用类型属性(如 address)是浅拷贝,修改会相互影响
userCopy.address.city = '上海';
console.log(user.address.city); // 上海(原对象被修改)
批量为对象添加属性 / 方法
class Product {
constructor(data) {
// 批量添加属性到实例(id、name、price 从 data 来,默认添加 stock 属性)
Object.assign(this, data, { stock: 0 });
}
// 批量添加原型方法
static initMethods() {
Object.assign(Product.prototype, {
increaseStock(num) { this.stock += num; },
decreaseStock(num) { this.stock -= num; }
});
}
}
// 初始化原型方法
Product.initMethods();
// 创建实例
const phone = new Product({ id: 1, name: '手机', price: 5000 });
console.log(phone); // { id:1, name:'手机', price:5000, stock:0 }
phone.increaseStock(10);
console.log(phone.stock); // 10
状态更新(Vue 中的不可变数据处理)
// 初始状态
this.state = {
user: { name: '张三', age: 20 },
settings: { theme: 'light', notifications: true }
};
// 更新用户年龄,同时保留其他状态(不可变更新)
this.setState(prevState => ({
user: Object.assign({}, prevState.user, { age: 21 })
// 新 user 对象 = 复制原 user 属性 + 更新 age
}));
合并多个数据源
// 接口1:基本信息
const baseInfo = { id: 1, name: '张三' };
// 接口2:偏好设置
const preferences = { theme: 'dark', language: 'zh-CN' };
// 接口3:权限数据
const permissions = { canEdit: true, canDelete: false };
// 合并为完整用户对象
const user = Object.assign({}, baseInfo, preferences, permissions);
console.log(user);
// {
// id: 1,
// name: '张三',
// theme: 'dark',
// language: 'zh-CN',
// canEdit: true,
// canDelete: false
// }
特性
覆盖规则:若源对象与目标对象有同名属性,后面的源对象属性会覆盖前面的(包括目标对象自身的)。
只复制可枚举属性:继承的属性、不可枚举属性(enumerable: false)不会被复制。
浅拷贝:若属性值是对象(引用类型),复制的是引用地址,而非深拷贝对象本身。
忽略 null 和 undefined:若源对象是 null 或 undefined,会被跳过(不报错)。
处理原始值作为源对象:原始值(字符串、数字、布尔值)会被转换为包装对象,其内置属性(如字符串的索引)会被复制。
const target = Object.assign({}, 'abc');
console.log(target); // { '0': 'a', '1': 'b', '2': 'c' }
12.继承+多态
继承和多态是面向对象编程(OOP)的核心概念,用于的目的是实现代码复用和灵活扩展。
继承
核心是:让一个对象(或类)可以复用另一个对象(或类)的属性和方法,同时可以扩展自身的特性。 子类可以通过 “重写” 父类方法,在复用基础上定制自己的逻辑。
// 父类(基类):定义通用属性和方法
class Animal {
constructor(name) {
this.name = name; // 共享属性
}
eat() { // 共享方法
console.log(`${this.name}在吃东西`);
}
}
// 子类(派生类):继承父类,并扩展自身特性
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数,必须在this之前
this.breed = breed; // 子类特有属性
}
bark() { // 子类特有方法
console.log(`${this.name}在汪汪叫`);
}
// 重写父类方法(扩展或修改)
eat() {
super.eat(); // 调用父类方法
console.log(`${this.name}喜欢吃骨头`); // 子类特有逻辑
}
}
// 使用
const dog = new Dog('旺财', '金毛');
dog.eat(); // 输出:"旺财在吃东西" → "旺财喜欢吃骨头"(先调用父类,再执行子类)
dog.bark(); // 输出:"旺财在汪汪叫"(子类特有方法)
多态
核心是:不同的对象(子类)对同一方法(来自父类或接口)做出不同的响应,即 “同一接口,多种实现”。
当多个子类继承自同一个父类,且都重写了父类的某个方法时,调用这个方法会根据对象的实际类型执行不同的逻辑。
// 父类
class Shape {
//作为所有 “形状” 的基类,定义了一个抽象方法 `getArea()`
//父类自身不实现具体的面积计算逻辑(仅抛出错误),而是强制要求所有子类必须重写 `getArea()` 方法,这是一种 “接口约定”,确保所有形状都有统一的 “计算面积” 能力。
//这种设计称为 “抽象类”(或 “接口类”),用于约束子类的行为,奠定 “多态” 的基础。
getArea() { // 定义“计算面积”的接口(父类方法,由子类实现)
throw new Error('子类必须实现getArea方法');
}
}
// 子类1:圆形通过 `extends Shape` 继承父类,必须实现 `getArea()` 方法(否则使用父类方法会报错)
class Circle extends Shape {
constructor(radius) {
super();// 调用父类构造函数
this.radius = radius;// 存储圆的半径
}
// 实现父类的getArea方法(多态体现:圆形的面积计算)
getArea() {
return Math.PI * this.radius **2; //圆的半径,计算并返回面积。
}
}
// 子类2:矩形
class Rectangle extends Shape {
constructor(width, height) {
super();// 调用父类构造函数
this.width = width;
this.height = height;
}
// 实现父类的getArea方法(多态体现:矩形的面积计算)
getArea() {
return this.width * this.height;
}
}
// 业务逻辑:统一处理“形状”,无需关心具体类型
function calculateTotalArea(shapes) {
// 接收一个 “形状数组”,遍历数组并调用每个形状的 `getArea()` 方法,累加得到总面积。
return shapes.reduce((total, shape) => total + shape.getArea(), 0);
}
// 使用
const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);
// 传入不同类型的对象,但调用同一方法getArea,得到不同结果
console.log(calculateTotalArea([circle, rectangle]));
// 计算:(π×5²) + (4×6) → 约78.54 + 24 = 102.54
函数没有用 if-else 或 switch 判断Circle 还是 Rectangle,而是直接调用 shape.getArea(),因为所有形状都继承自 Shape 父类,并强制实现了 getArea() 方法(多态的基础)。无论 shape 是哪种具体类型,只要有 getArea() 方法,就能被正确计算,这就是 “同一接口,多种实现”。
调用者只需关注父类定义的 “接口”(如 getArea),无需关心具体是哪个子类的实现,降低耦合度。
函数不关心数组中的元素是 Circle 还是 Rectangle,只需要它们有 getArea() 方法(遵循 Shape 接口)。
当调用 shape.getArea() 时,会自动根据 shape 的实际类型(圆形 / 矩形)执行对应的计算逻辑 ——同一方法,不同实现,这就是多态。
如果需要新增形状(如三角形 Triangle),只需创建 Triangle extends Shape 并实现 getArea() 方法,无需修改 calculateTotalArea 函数 —— 符合 “开闭原则”(对扩展开放,对修改关闭)。
通过 “继承” 建立了形状的层级关系,通过 “多态” 实现了不同形状对同一方法,的差异化处理,最终实现了 “统一逻辑处理不同对象” 的灵活设计。如不同数据的格式化等场景中非常常见,如表单验证中,“验证” 方法对手机号、邮箱、身份证有不同的校验逻辑。
继承与多态的结合场景
// 父类:验证器基类(定义接口)
class Validator {
validate(value) { // 验证接口,由子类实现
throw new Error('子类必须实现validate方法');
}
}
// 子类1:手机号验证器
class PhoneValidator extends Validator {
validate(value) {
return /^1[3-9]\d{9}$/.test(value);
}
}
// 子类2:邮箱验证器
class EmailValidator extends Validator {
validate(value) {
return /^[^\s@]+@[^\s@]+.[^\s@]+$/.test(value);
}
}
// 业务逻辑:统一验证表单字段
function validateField(validator, value) {
return validator.validate(value); // 多态:根据validator类型执行不同验证
}
// 使用
const phoneValidator = new PhoneValidator();
const emailValidator = new EmailValidator();
console.log(validateField(phoneValidator, '13800138000')); // true(手机号有效)
console.log(validateField(emailValidator, 'test@example.com')); // true(邮箱有效)
继承 :PhoneValidator 和 EmailValidator 继承 Validator,保证都有 validate 方法。
多态 :validateField 函数无需区分具体是哪种验证器,只需调用 validate 方法,即可得到正确结果。
权限控制组件(多态权限校验)
场景:系统中不同用户角色(普通用户、管理员、超级管理员)有不同的操作权限,需根据角色动态控制按钮是否显示。
// 父类:基础权限验证器
class Permission {
// 抽象方法:校验是否有权限(多态接口)
hasPermission(action) {
throw new Error('子类必须实现hasPermission方法');
}
}
// 子类1:普通用户权限
class UserPermission extends Permission {
hasPermission(action) {
// 普通用户只能查看和创建
const allowed = ['view', 'create'];
return allowed.includes(action);
}
}
// 子类2:管理员权限
class AdminPermission extends Permission {
hasPermission(action) {
// 管理员可以查看、创建、编辑
const allowed = ['view', 'create', 'edit'];
return allowed.includes(action);
}
}
// 子类3:超级管理员权限
class SuperAdminPermission extends Permission {
hasPermission(action) {
// 超级管理员拥有所有权限
return true;
}
}
// 按钮组件:根据权限显示
class ActionButton {
constructor(label, action, permission) {
this.label = label;
this.action = action;
this.permission = permission;
}
render() {
// 调用权限验证器的统一接口(多态)
if (this.permission.hasPermission(this.action)) {
return `<button>${this.label}</button>`;
}
return ''; // 无权限则不渲染
}
}
// 使用:不同角色看到的按钮不同
const userPerm = new UserPermission();
const adminPerm = new AdminPermission();
const superPerm = new SuperAdminPermission();
// 普通用户的按钮
const userButtons = [
new ActionButton('查看', 'view', userPerm),
new ActionButton('编辑', 'edit', userPerm) // 无权限,不显示
];
// 管理员的按钮
const adminButtons = [
new ActionButton('编辑', 'edit', adminPerm),
new ActionButton('删除', 'delete', adminPerm) // 无权限,不显示
];
// 渲染按钮
console.log(userButtons.map(btn => btn.render())); // 只显示“查看”按钮
console.log(adminButtons.map(btn => btn.render())); // 只显示“编辑”按钮
数据验证器(多态验证逻辑)
// 父类:基础验证器(定义接口)
class Validator {
// 抽象方法:子类必须实现(多态接口)
validate(value) {
throw new Error('子类必须实现validate方法');
}
// 共享的错误提示方法
getErrorMsg() {
return this.errorMsg || '验证失败';
}
}
// 子类1:手机号验证器
class PhoneValidator extends Validator {
constructor() {
super();
this.errorMsg = '请输入有效的手机号';
}
validate(value) {
return /^1[3-9]\d{9}$/.test(value); // 手机号规则
}
}
// 子类2:邮箱验证器
class EmailValidator extends Validator {
constructor() {
super();
this.errorMsg = '请输入有效的邮箱';
}
validate(value) {
return /^[^\s@]+@[^\s@]+.[^\s@]+$/.test(value); // 邮箱规则
}
}
// 子类3:密码强度验证器
class PasswordValidator extends Validator {
constructor() {
super();
this.errorMsg = '密码需包含字母和数字,长度≥6';
}
validate(value) {
return /^(?=.*[a-zA-Z])(?=.*\d).{6,}$/.test(value); // 密码规则
}
}
// 业务逻辑:统一验证表单字段(依赖多态)
function validateForm(fields) {
const errors = [];
fields.forEach(({ value, validator }) => {
if (!validator.validate(value)) { // 统一调用validate,自动适配不同规则
errors.push(validator.getErrorMsg());
}
});
return errors;
}
// 使用:验证表单
const formData = [
{ value: '13800138000', validator: new PhoneValidator() },
{ value: 'test@example.com', validator: new EmailValidator() },
{ value: '123456', validator: new PasswordValidator() } // 密码不含字母,验证失败
];
const errors = validateForm(formData);
console.log(errors); // 输出:["密码需包含字母和数字,长度≥6"]
13.组件的根作用域
eventBus.on('update:runType', (newVal) => {
c.value = newVal
if (newVal) {
props.a.selfStatus = 'init'
props.a.isPrev = false
console.log("已开启")
}
})
console.log("已开启") 打印多次
在 Vue 组件(尤其是 Composition API + <script setup> 语法)中, “根作用域” 指的是组件脚本的 “顶层代码区域” —— 即不在任何函数、生命周期钩子、计算属性或响应式监听内部,直接写在 <script setup> 标签下的代码区域。组件初始化时会执行一次的 “最外层代码” ,是组件逻辑的 “入口起点”。
- 当组件被初始化(如首次渲染、被复用、父组件重新渲染导致子组件重新创建等)时,根作用域中的代码会重新执行一遍。
console.log("已开启")不在任何函数(如eventBus.on的回调、setup函数的内部逻辑等)中,而是直接写在组件的根级作用域(即setup函数的顶层,或<script setup>的顶层)。
导致多次打印:
- 组件被多次创建:如果该组件在页面中被多次使用(例如循环渲染多个节点),每个实例初始化时都会执行一次该打印。
- 父组件重新渲染:当父组件因状态变化重新渲染时,子组件可能被销毁后重新创建,导致根作用域代码再次执行。
- 热重载触发:在开发环境中,修改代码后触发热重载(HMR),组件会重新初始化,也会导致打印执行。
正确做法:将副作用代码放在 “有明确触发时机” 的地方,如:
“副作用代码” 指会影响外部环境或重复执行有问题的代码(如 console.log、接口请求、修改 DOM 等)。
- 生命周期钩子(
onMounted/onUpdated,仅特定阶段执行); - 事件回调(如
eventBus.on/onClick,仅事件触发时执行); - 响应式监听(
watch,仅依赖变化时执行)。
14.键盘操作监听
// 组件挂载时注册事件
onMounted(() => window.addEventListener('keydown', handleKeydown))
// 组件卸载时移除事件
onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
//--------------DOM操作-------------------------------------------
//键盘事件处理逻辑
const handleKeydown = (e) => {
//如果按下了 Ctrl 或 Command 键,直接返回,不执行自定义逻辑
if (e.ctrlKey || e.metaKey) {
return// 让浏览器执行默认行为(如 Ctrl+C 复制)
}
//忽略大小写(默认监听小写,如按 Shift+R 会触发大写,这里统一处理)
const key = e.key.toLowerCase()
// 快捷键映射:键名
const keyMap = {
r: () => toggleMenu('R'),
t: () => toggleMenu('T'),
c: onClear
}
// 存在对应操作则执行(阻止默认行为避免冲突)
if (keyMap[key]) {
e.preventDefault()
keyMap[key]()
}
}
?.细节处理
const s = data?.sl || {};
安全地从 data 中获取 sl 属性
data?.sl 使用可选链操作符 ?.,若 data 为 null/undefined,则直接返回 undefined,避免报错。
|| {} 用于默认值处理:若 data?.sl 为假值(undefined、null、空对象等),则 s 赋值为空对象 {},确保后续遍历安全。
15.throw new Error
是 JavaScript 中用于主动抛出错误的语法,它的核心作用是在代码执行过程中发现异常情况时,中断当前执行流程并传递错误信息
当代码检测到不符合预期的情况(如无效输入、数据异常、逻辑错误等),可以通过 throw new Error('错误信息') 主动创建一个错误对象并抛出。
if (b === 0) {
// 检测到除数为0,主动抛出错误
throw new Error('除数不能为0');
}
错误抛出后,当前函数的后续代码会立即停止执行,错误会沿着调用栈向上传播,直到被 try/catch 捕获,否则会导致程序崩溃。
new Error('错误信息') 中的字符串参数用于描述错误原因,便于开发者调试或向用户展示错误提示。错误对象还包含 stack 属性,记录错误发生的调用栈信息。
运用场景
参数校验:对函数输入的合法性进行检查,若不符合要求则抛出错误,避免后续代码因无效参数产生更复杂的问题。
数据有效性检查:在处理接口返回数据、解析 JSON 等场景中,若数据格式不符合预期,主动抛出错误以提示数据异常
if (!data || !data.list) {
throw new Error('接口返回数据格式错误,缺少list字段');
}
业务逻辑异常:业务流程违反规则时抛出错误,例如“权限不够” 等场景
配合 try/catch 处理错误:抛出的错误通常需要被 try/catch 捕获,以优雅地处理异常,避免程序崩溃。
try {
调用可能抛出错误的函数
} catch (error) {
// 捕获错误并处理(如提示用户、记录日志等)
console.error('操作失败:', error.message);
}
- 明确标记错误场景,提高代码的可维护性;
- 提前阻断无效流程,避免错误扩散;
- 传递错误信息,便于调试和用户提示。
16.是否为数组
Array.isArray(value) 是 JavaScript 中用于判断一个值是否为数组的内置方法
- 参数
value:需要检测的变量或表达式 - 返回值:布尔值(
true表示是数组,false表示不是)
Array.isArray([]); // true(空数组)
Array.isArray([1, 2, 3]); // true(非空数组)
Array.isArray(new Array()); // true(通过构造函数创建的数组)
Array.isArray("array"); // false(字符串不是数组)
Array.isArray(null); // false(null不是数组)
Array.isArray({ length: 3 }); // false(类数组对象不是数组)
使用typeof 对数组返回 'object',无法区分数组和普通对象
typeof []; // 'object'(不准确)
运用场景
数组方法调用前的校验:当需要调用 map、forEach、filter 等数组特有的方法时,先判断变量是否为数组,避免报错
接口数据格式验证:处理后端返回数据时,验证数组类型的字段是否符合预期
if (Array.isArray(result.data)) {
renderUsers(result.data); // 渲染用户列表
} else {
console.error('接口返回的数据不是数组');
}
函数参数类型校验:限制函数参数必须为数组,确保逻辑正确性
if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
throw new Error('参数必须是数组');
}
处理类数组对象:区分真正的数组和类数组对象(如 arguments、NodeList),避免误用数组方法
const nodeList = document.querySelectorAll('div'); // 类数组对象
if (Array.isArray(nodeList)) {
// 不会执行,因为 nodeList 不是数组
} else {
// 转换为真正的数组后再处理
const arr = Array.from(nodeList);
}
Array.isArray() 是判断数组类型的最优方案,它解决了其他判断方式的局限性,确保在各种场景下都能准确识别数组。
17.数组的 map() 方法
用于遍历数组并对每个元素进行处理,最终返回一个新的数组,不会改变原数组。
const newArray = array.map((currentValue, index, array) => {
// 对 currentValue 进行处理
return 处理后的值;
});
-
参数:
currentValue:当前遍历到的元素(必选)index:当前元素的索引(可选)array:原数组本身(可选)
-
返回值:一个新数组,包含每个元素经过处理后的结果
- 不改变原数组:
map()会返回全新的数组,原数组的元素不会被修改(除非在回调中主动修改引用类型的属性)。 - 长度不变:返回的新数组长度与原数组完全一致(每个元素都会被处理并返回结果)。
- 回调必 return:如果回调函数没有
return,新数组会充满undefined。
- `numbers` 是**原数组**,调用 `map()` 方法对它进行遍历处理
- `num` 是**当前遍历到的元素的别名**(可以任意命名,比如 `item`、`element` 等),代表数组中的每个元素
- `doubled` 是 `map()` 方法**返回的新数组**,包含了所有元素经过处理(这里是乘以 2)后的结果
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]
console.log(numbers); // [1, 2, 3, 4](原数组不变)
`map()` 逐个 “取出” 原数组 `numbers` 中的元素(用 `num` 临时指代),对每个 `num` 执行 `num * 2` 操作,然后把所有计算结果按顺序装进一个新的 “容器”,这个容器就是 `doubled`。
整个过程中,原数组 `numbers` 不会被修改,`doubled` 是一个全新的数组,两者相互独立。
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
];
const userNames = users.map(user => user.name);
console.log(userNames); // ['张三', '李四']
const fruits = ['苹果', '香蕉', '橙子'];
const indexedFruits = fruits.map((fruit, index) => `${index + 1}. ${fruit}`);
console.log(indexedFruits); // ['1. 苹果', '2. 香蕉', '3. 橙子']
运用场景
数据转换:将数组中的元素转换为另一种形式(如格式转换、单位转换等)
// 价格单位转换:分 → 元
const pricesInCents = [100, 200, 300];
const pricesInYuan = pricesInCents.map(cent => cent / 100); // [1, 2, 3]
提取数据:从对象数组中提取特定字段,形成新的数组(常用于数据筛选或展示)
const apiResponse = {
data: [
{ id: 101, name: 'Alice' },
{ id: 102, name: 'Bob' }
]
};
const userIds = apiResponse.data.map(user => user.id); // [101, 102]
格式化数据:对数据进行格式化处理,使其符合前端展示或后续逻辑的需求。
// 格式化日期(假设原日期为时间戳)
const timestamps = [1620000000000, 1630000000000];
const formattedDates = timestamps.map(ts => {
return new Date(ts).toLocaleDateString();
});
渲染列表:框架中,map() 常用于遍历数据生成列表元素
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
批量处理数据:对数组中的每个元素执行相同的操作(如添加属性、修改状态等)
// 为每个商品添加默认折扣属性
const products = [
{ name: '手机', price: 5000 },
{ name: '电脑', price: 8000 }
];
const discountedProducts = products.map(product => ({
...product,
discount: 0.9 // 统一打9折
}));
- `products` 是原数组,包含两个商品对象
- `product` 是遍历过程中每个商品对象的临时别名
- `map` 方法对每个 `product` 进行处理,生成新的对象
- `...product` 复制了原商品对象的所有属性(`name` 和 `price`)
- `discount: 0.9` 是给每个新对象新增的属性
[
{ name: '手机', price: 5000, discount: 0.9 },
{ name: '电脑', price: 8000, discount: 0.9 }
]
18.... 被称为扩展运算符
它的核心作用是 **“展开” 集合(数组、对象等)中的元素 **,将集合中的内容逐个提取并使用。
在数组中
数组是引用类型,直接赋值会导致修改新数组时影响原数组。使用 ... 可以创建原数组的浅拷贝
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // 复制 arr1 的元素到 arr2
arr2.push(4);
console.log(arr1); // [1, 2, 3](原数组不受影响)
console.log(arr2); // [1, 2, 3, 4](新数组被修改)
合并数组
将多个数组的元素合并到一个新数组中
const arr1 = [1, 2];
const arr2 = [3, 4];
const merged = [...arr1, ...arr2, 5]; // 合并 arr1、arr2 并添加新元素
console.log(merged); // [1, 2, 3, 4, 5]
作为函数参数传递
将数组元素 “展开” 为函数的独立参数
const numbers = [10, 20, 30];
// 不使用 ... 时,需要手动取元素:Math.max(numbers[0], numbers[1], numbers[2])
const max = Math.max(...numbers); // 等价于 Math.max(10, 20, 30)
console.log(max); // 30
在对象中
复制对象
复制原对象的所有可枚举属性到新对象,避免直接修改原对象
const user = { name: '张三', age: 20 };
const userCopy = { ...user }; // 复制 user 的属性到 userCopy
userCopy.age = 21;
console.log(user); // { name: '张三', age: 20 }(原对象不变)
console.log(userCopy); // { name: '张三', age: 21 }(新对象被修改)
合并对象(覆盖同名属性)
合并多个对象的属性,后出现的属性会覆盖前面的同名属性
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 }; // 合并 obj1 和 obj2
console.log(merged); // { a: 1, b: 3, c: 4 }(obj2 的 b 覆盖了 obj1 的 b)
扩展对象(新增 / 修改属性)
在复制原对象的基础上,新增或修改特定属性
const product = { name: '手机', price: 5000 };
// 复制原属性,并新增 discount 属性,修改 price 属性
const discountedProduct = { ...product, discount: 0.9, price: 4500 };
console.log(discountedProduct);
// { name: '手机', price: 4500, discount: 0.9 }
运用场景
处理数组 / 对象时避免修改原数据:开发中常需要 “基于原数据生成新数据”(如 React/Vue 中的状态更新),... 可以安全地复制并修改数据,不影响原数据:
// Vue 中更新响应式对象
const user = ref({ name: '张三', age: 20 });
// 基于原对象创建新对象(触发响应式更新)
user.value = { ...user.value, age: 21 };
函数参数的灵活传递:当函数参数数量不确定时,用 ... 将数组展开为参数
// 求和函数(支持任意数量的参数)
function sum(...nums) { // 这里的 ... 是剩余参数,与扩展运算符类似但用途不同
return nums.reduce((a, b) => a + b, 0);
}
const numbers = [1, 2, 3, 4];
console.log(sum(...numbers)); // 等价于 sum(1, 2, 3, 4),结果为 10
组件 props 传递:Vue 中,用 ... 批量传递 props 给子组件
19.find()
find() 是数组方法,用于遍历数组,返回第一个满足回调函数条件的元素(如果没有找到则返回 undefined)
const result = array.find((currentValue, index, array) => {
// 判断条件
return 布尔值; // true 表示找到目标元素,false 表示继续查找
});
-
参数:
currentValue:当前遍历到的元素(必选)index:当前元素的索引(可选)array:原数组本身(可选)
-
返回值:
- 第一个满足条件的元素(找到时)
undefined(未找到时)
根据自定义条件从数组中定位唯一或第一个符合要求的元素,尤其适合处理对象数组
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '张三' }
];
// 查找第一个 name 为 '张三' 的用户
const firstZhang = users.find(user => user.name === '张三');
console.log(firstZhang); // { id: 1, name: '张三' }(只返回第一个匹配项)
const products = [
{ name: '手机', price: 5000 },
{ name: '耳机', price: 800 },
{ name: '电脑', price: 8000 }
];
// 查找第一个价格超过 1000 的商品
const expensiveItem = products.find(item => item.price > 1000);
console.log(expensiveItem); // { name: '手机', price: 5000 }
**查找并返回第一个符合条件的节点**
const firstNode = allNodes.value.find(node => !node.config.ParentId || node.config.ParentId === '' || node.config.ParentId === '0' )
1. **核心方法**:`allNodes.value.find(...)`
- `allNodes.value` 是一个响应式数组(存储节点列表)
- `find()` 是数组方法,用于遍历数组,返回**第一个满足回调函数条件**的元素(如果没有找到则返回 `undefined`)
-
查找条件:
node => !node.config.ParentId || node.config.ParentId === '' || node.config.ParentId === '0'对数组中的每个node(节点)进行判断,满足以下任一条件即匹配:!node.config.ParentId:ParentId不存在、为null、undefined或0( falsy 值)node.config.ParentId === '':ParentId是空字符串node.config.ParentId === '0':ParentId是字符串'0'(通常表示 “无父节点”)
-
结果赋值:
const firstNode最终将找到的第一个符合条件的节点赋值给firstNode。运用场景
根据唯一标识查找元素:在包含唯一 ID 的对象数组中,通过 ID 快速定位元素(常用于数据详情展示、编辑等场景)
// 从用户列表中查找 ID 为 2 的用户 function getUserById(users, targetId) { return users.find(user => user.id === targetId); } const users = [{ id: 1 }, { id: 2 }, { id: 3 }]; console.log(getUserById(users, 2)); // { id: 2 }
验证数组中是否存在符合条件的元素:结合条件判断,确认数组中是否存在特定元素(虽然 some() 更适合纯判断,但 find() 可以同时获取该元素)。
const scores = [85, 92, 60, 77];
// 检查是否有不及格的分数(<60),并获取第一个不及格的分数
const failedScore = scores.find(score => score < 60);
if (failedScore !== undefined) {
console.log(`有不及格的分数:${failedScore}`);
}
处理表单数据:在表单选项数组中,根据用户选择的值查找对应的选项详情。
const options = [
{ value: '1', label: '选项一' },
{ value: '2', label: '选项二' }
];
// 用户选择了 value 为 '2' 的选项,查找对应的 label
const selectedOption = options.find(opt => opt.value === '2');
console.log(selectedOption.label); // '选项二'
20.static:静态方法
静态方法, 无需实例化类,直接通过类名调用。普通方法(非静态)需要先创建类的实例(用new关键字)才能调用。
当方法 / 属性不属于某个具体实例,而是属于类本身,或者不需要依赖实例的状态(属性)时,就用 static
```
class LogicManager {
// 静态方法(带static)
static create() {
return new LogicManager();
}
// 普通方法(不带static)
run() {
console.log("执行逻辑");
}
}
const manager = LogicManager.create(); // 调用静态方法:直接通过类名调用
const manager = new LogicManager(); // 调用普通方法:先创建实例
manager.run(); // 正确:通过实例调用普通方法
```
静态方法通常用于与类本身相关、但不依赖实例状态的操作,比如格式化数据、验证参数等通用操作,不需要实例化就能直接使用,静态方法就像类自带的 “工具箱”,直接用类本身就能打开工具箱使用里面的工具。
运用场景
```
class DateUtils {
// 静态方法:格式化日期(不需要实例属性,纯工具功能)
static format(date, format = "yyyy-MM-dd") {
// 逻辑:把date转成指定格式的字符串
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return format.replace("yyyy", year).replace("MM", month).replace("dd", day);
}
}
// 使用:直接通过类名调用,不需要实例化
const today = new Date();
console.log(DateUtils.format(today)); // 输出:2025-10-16(当前日期)
```
这个 `format` 方法只需要传入 `date` 参数就能工作,不需要 `DateUtils` 的实例属性(因为它根本没有实例属性)。
如果做成实例方法,反而麻烦:必须先 `new DateUtils()` 才能调用,但这个实例毫无意义(没有任何属性)。
当需要封装 “创建实例的复杂逻辑” 时(比如先请求数据再创建实例),适合用静态方法作为 “工厂”
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法:依赖实例属性(比如打印用户信息)
introduce() {
console.log(`我是${this.name},${this.age}岁`);
}
// 静态工厂方法:从后端数据创建User实例
static async createFromApi(userId) {
// 1. 先调用接口获取用户数据(复杂逻辑)
const res = await fetch(`/api/user/${userId}`);
const data = await res.json(); // 假设返回 { name: "张三", age: 20 }
// 2. 创建并返回实例
return new User(data.name, data.age);
}
}
// 使用:通过静态方法创建实例,无需手动传参
const user = await User.createFromApi(123);
user.introduce(); // 输出:我是张三,20岁
`createFromApi` 的作用是 “创建 `User` 实例”,但它本身不需要依赖任何已存在的 `User` 实例
如果某个属性是 “类的全局配置”,所有实例共享且不会因实例不同而变化,就用静态属性。
class GameRole {
// 静态属性:所有角色共享的默认生命值
static DEFAULT_HP = 100;
constructor(name) {
this.name = name;
this.hp = GameRole.DEFAULT_HP; // 实例初始化时用静态属性
}
}
// 使用:直接通过类访问静态属性
console.log(GameRole.DEFAULT_HP); // 输出:100
// 所有实例初始化时都用这个默认值
const role1 = new GameRole("战士");
const role2 = new GameRole("法师");
console.log(role1.hp); // 100
console.log(role2.hp); // 100
- `DEFAULT_HP` 是 “所有角色的默认值”,不属于某个具体角色(实例),而是属于 `GameRole` 类本身。
- 如果做成实例属性,每个实例都会重复存储这个值,浪费内存,且修改时需要逐个改,而静态属性改一次全类生效。
如果某个类只能有一个实例(比如全局缓存管理器、全局事件总线),用静态方法控制实例创建。
class CacheManager {
// 静态属性:存储唯一实例
static instance;
// 私有构造函数:防止外部直接new
constructor() {
this.cache = new Map();
}
// 静态方法:获取唯一实例
static getInstance() {
if (!CacheManager.instance) {
CacheManager.instance = new CacheManager(); // 只创建一次
}
return CacheManager.instance;
}
// 实例方法:操作缓存(依赖实例的cache属性)
set(key, value) {
this.cache.set(key, value);
}
}
// 使用:通过静态方法获取唯一实例
const cache1 = CacheManager.getInstance();
const cache2 = CacheManager.getInstance();
console.log(cache1 === cache2); // 输出:true(同一个实例)
cache1.set("name", "张三");
console.log(cache2.cache.get("name")); // 输出:张三(共享数据)
如果方法需要访问实例的属性(每个实例不同的状态),就必须用实例方法,不能用 static
class Person {
constructor(name) {
this.name = name; // 每个实例的name不同
}
// 错误:static方法无法访问this.name(this指向类,不是实例)
static sayName() {
console.log(this.name); // 输出:undefined(类本身没有name属性)
}
// 正确:实例方法才能访问实例的name
sayName() {
console.log(this.name);
}
}
const p = new Person("张三");
p.sayName(); // 正确:输出“张三”
Person.sayName(); // 错误:输出undefined
需要访问实例属性的方法(必须是实例方法)
class Person {
constructor(name, age) {
this.name = name; // 实例属性(每个实例不同)
this.age = age; // 实例属性(每个实例不同)
}
// 这个方法需要访问实例的name和age(依赖实例属性)
getInfo() {
// 必须知道“当前实例的name和age”才能工作
return `姓名:${this.name},年龄:${this.age}`;
}
}
const p1 = new Person("张三", 20);
console.log(p1.getInfo()); // 依赖p1的name和age → 输出:姓名:张三,年龄:20
这里的`getInfo`方法**必须访问实例的`this.name`和`this.age`**,如果脱离具体实例(比如直接用`Person.getInfo()`),`this.name`根本不存在,方法就会失效。所以它必须是实例方法,不能加`static`
不需要访问实例属性的方法(可以是静态方法)
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 这个方法不需要访问任何实例的属性(不依赖this.xxx)
static isAdult(age) {
// 只需要传入的age参数,就能判断是否成年
return age >= 18;
}
}
// 调用时不需要实例,直接用类名
console.log(Person.isAdult(20)); // 输出:true(只靠参数20就能工作)
console.log(Person.isAdult(15)); // 输出:false(只靠参数15就能工作)
这里的`isAdult`方法**完全不关心 “某个 Person 实例的`this.age`是多少”** ,它只需要外部传入一个`age`参数就能执行逻辑。有没有实例、实例的`age`是多少,对它来说毫无影响。所以它适合加`static`,作为静态方法。
| 实例方法(无 static) | 是(必须用this.xxx) | 具体实例的this.name、this.age等 | getInfo(需要知道 “当前实例的姓名年龄”) |
|---|---|---|---|
| 静态方法(有 static) | 否(完全不用this.xxx) | 传入的参数或自身逻辑 | isAdult(只需要传入的年龄参数) |
静态方法内部,必须用 new 创建实例
class LogicManager {
constructor(data, options) {
this.nodes = data; // 初始化实例属性
this.transactionId = options.transactionId;
}
// 实例方法:依赖实例属性(必须通过实例调用)
Run() {
console.log("执行逻辑,节点数量:", this.nodes.length);
}
static Create(data) {
// 必须用 new 才能创建实例,否则无法初始化属性和绑定方法
return new LogicManager(data, { transactionId: "xxx" });
}
}
// 调用静态方法 Create 获取实例(内部已用 new)
const manager = LogicManager.Create([...]);
// 只有通过实例才能调用 Run 方法(因为 Run 依赖 this.nodes)
manager.Run(); // 正确:输出实例的 nodes 数量
静态方法 Create 内部,必须用 new 创建实例
21.实例化
实例化是指根据 “类(Class)” 这个模板,创建出具体对象(Object)的过程。类是对一类事物的抽象描述(比如 “汽车” 类定义了汽车有轮子、能行驶),而实例化就是造出一辆具体的汽车(比如 “我的红色特斯拉”)。
将抽象模板变为具体实体:类只定义了 “有什么属性和方法”,实例化后才会有具体的属性值和可执行的方法。
保存独立状态:每个实例(对象)有自己的属性值,互不干扰(比如两辆车可以有不同的颜色、速度)
支撑方法运行:类中的实例方法(非静态方法)依赖实例的属性,必须通过实例才能调用(因为方法逻辑需要具体的属性值)。
// 定义一个“人”的类(模板)
class Person {
// 构造函数:实例化时初始化属性
constructor(name, age) {
this.name = name; // 实例属性:名字(每个实例有自己的名字)
this.age = age; // 实例属性:年龄(每个实例有自己的年龄)
}
// 实例方法:依赖实例属性(必须通过实例调用)
sayHello() {
console.log(`我叫${this.name},今年${this.age}岁`);
}
}
// 实例化:根据Person类创建具体的人(对象)
const zhangsan = new Person("张三", 20); // 第一个实例
const lisi = new Person("李四", 30); // 第二个实例
// 调用实例方法(每个实例的方法操作自己的属性)
zhangsan.sayHello(); // 输出:我叫张三,今年20岁
lisi.sayHello(); // 输出:我叫李四,今年30岁
类(Person):只定义了 “人应该有名字、年龄,能打招呼”,但没有具体的名字和年龄
实例化(new Person(...)) :创建出zhangsan和lisi两个具体的 “人”,各自有自己的name和age。
实例方法(sayHello):依赖实例的name和age,必须通过实例(zhangsan或lisi)才能调用,且输出结果由实例自己的属性决定。
运用场景
比如电商系统中,“用户” 类需要实例化为多个用户对象(user1、user2),每个用户有自己的账号、购物车,操作互不影响。
22.async/await
1. async 函数的本质:返回 Promise
async` 关键字修饰的函数,其返回值必然是一个 Promise 对象:
async function demo() {
return "hello"; // 等价于 return Promise.resolve("hello")
}
demo().then(res => console.log(res)); // 输出:hello
async function demo2() {
throw new Error("出错了"); // 等价于 return Promise.reject(new Error("出错了"))
}
demo2().catch(err => console.log(err.message)); // 输出:出错了
async function demo3() {
return Promise.resolve(100); // 直接返回该 Promise
}
demo3().then(res => console.log(res)); // 输出:1
2. await 的作用:暂停等待 Promise 结果
await 只能在 async 函数内部使用,作用是等待一个 Promise 完成,并获取其 resolve 的值。具体行为:
- 若
await后是 Promise 对象:暂停当前async函数执行,直到 Promise 状态变为resolved,然后继续执行,返回resolve的值; - 若
await后是普通值(非 Promise):会被自动包装为Promise.resolve(普通值),相当于 “等待 0 秒”,直接返回该值; - 若
await后是rejected的 Promise:会抛出异常(需用try/catch捕获,否则会导致整个async函数返回的 Promise 变为rejected)。
async function getResult() {
// 等待普通值(自动包装为 Promise)
const a = await 10;
console.log(a); // 10
// 等待 Promise.resolve
const b = await Promise.resolve("test");
console.log(b); // "test"
// 等待 Promise.reject(会抛出异常)
try {
const c = await Promise.reject(new Error("失败"));
} catch (err) {
console.log(err.message); // "失败"
}
}
getResult();
await 处理 rejected 状态的 Promise 时,必须捕获错误,否则会导致整个 async 函数的 Promise 失败。常见两种处理方式:
(1)try/catch 捕获(推荐,适合处理多个关联错误)
用 try 包裹 await 操作,catch 捕获所有可能的异常(包括代码运行时错误)。
async function loadData() {
try {
const orders = await fetchOrders(userInfo.id)
return orders;
} catch (err) {
// 任何一步失败,都会进入这里
console.log("流程失败:", err.message);
return null; // 返回默认值,避免外部处理失败
}
}
(2)await + .catch() 单独处理(适合单个异步操作的独立错误)
并行请求两个无关接口,允许其中一个失败
在 await 后直接调用 .catch(),为单个 Promise 绑定错误处理,不影响其他 await 操作。
async function loadIndependentData() {
// 接口1:失败时返回默认空数组
const list1 = await fetchList1().catch(() => []);
// 接口2:失败时返回默认空对象
const list2 = await fetchList2().catch(() => ({}));
return { list1, list2 };
}
// 即使 fetchList1 失败,也会继续执行 fetchList2
若任务 B 依赖任务 A 的结果,直接用 await 依次等待即可(天然顺序执行)。
async function getOrderAfterUser() {
const user = await getUser(); // 等待用户查询完成
const order = await getOrder(user.id); // 用用户ID查订单(依赖用户结果)
return order;
}
// 总耗时 = getUser耗时 + getOrder耗时
并行执行(无依赖)
若任务间无依赖,用 Promise.all() 同时启动所有任务,再 await 结果(总耗时 = 最长任务耗时),效率更高。
同时加载商品列表和分类列表
async function loadParallelData() {
// 同时启动两个异步任务(不等待,直接获取Promise对象)
const promise1 = getGoodsList();
const promise2 = getCategories();
// 等待两个任务都完成(并行执行)
const [goods, categories] = await Promise.all([promise1, promise2]);
return { goods, categories };
}
// 总耗时 = max(getGoodsList耗时, getCategories耗时)
若并行任务中某一个失败,Promise.all() 会立即失败。若需 “允许部分失败”,可用 Promise.allSettled():
async function loadWithPartialFail() {
const [goodsRes, categoriesRes] = await Promise.allSettled([
getGoodsList(),
getCategories()
]);
// 分别处理成功/失败结果
const goods = goodsRes.status === "fulfilled" ? goodsRes.value : [];
const categories = categoriesRes.status === "fulfilled" ? categoriesRes.value : [];
return { goods, categories };
}
循环中的异步处理
在循环中处理异步操作时,for/for...of 循环可配合 await 实现顺序执行;但 forEach/map 等数组方法不行(因为它们不等待 async 回调)。
// 需求:按顺序删除3个ID对应的资源(前一个删除完再删下一个)
const ids = [1, 2, 3];
// 正确:for...of + await(顺序执行)
async function deleteInOrder() {
for (const id of ids) {
await deleteResource(id); // 等待当前删除完成,再执行下一个
console.log(`删除完成:${id}`);
}
}
// 错误:forEach + await(不会顺序执行,会同时启动所有删除)
async function wrongDelete() {
ids.forEach(async (id) => {
await deleteResource(id); // forEach 不等待,会立即执行所有回调
console.log(`删除完成:${id}`); // 输出顺序不确定
});
}
运用场景:
前端 API 请求与数据处理
// 登录接口(返回token)
function login(username, password) {
return fetch("/api/login", { method: "POST", body: { username, password } });
}
// 获取用户信息接口(需要token)
function getUserInfo(token) {
return fetch("/api/user", { headers: { Authorization: token } });
}
// 登录后加载用户信息
async function loginAndLoadUser(username, password) {
try {
const loginRes = await login(username, password);
const { token } = await loginRes.json(); // 解析登录响应
const userRes = await getUserInfo(token);
const userInfo = await userRes.json(); // 解析用户信息响应
renderUserInfo(userInfo); // 渲染页面
} catch (err) {
showError("操作失败:" + err.message); // 统一错误提示
}
}
复杂异步流程控制
当业务逻辑涉及多个异步步骤
async function placeOrder(userId, goodsList) {
try {
// 1. 检查库存
const stockOk = await checkStock(goodsList);
if (!stockOk) throw new Error("部分商品库存不足");
// 2. 计算总价
const totalPrice = await calculatePrice(goodsList);
// 3. 创建订单
const order = await createOrder(userId, goodsList, totalPrice);
// 4. 扣减库存
await deductStock(goodsList);
// 5. 发送订单通知
await sendNotification(userId, order.id);
return order;
} catch (err) {
console.error("下单失败:", err);
// 回滚操作(如恢复库存)
await rollbackStock(goodsList);
throw err; // 向外抛出错误,让调用方处理
}
}
替代 Promise 链式调用
当 Promise 链式调用过长(如 .then().then().then().catch()),代码可读性会下降,async/await 可将其转换为线性代码。
// Promise 链式调用(多层嵌套)
fetchData()
.then(res => process1(res))
.then(res1 => process2(res1))
.then(res2 => process3(res2))
.catch(err => handleError(err));
// async/await 写法(线性代码)
async function processData() {
try {
const res = await fetchData();
const res1 = await process1(res);
const res2 = await process2(res1);
const res3 = await process3(res2);
} catch (err) {
handleError(err);
}
}
await不要滥用:无依赖的异步任务用Promise.all()并行执行,避免用await顺序等待(浪费性能)。- 错误必须捕获:
async函数中未捕获的错误会导致返回的 Promise 失败,可能引发全局错误(如 Node.js 进程崩溃、浏览器控制台报错)。 - 避免在顶级作用域使用
await:await只能在async函数内使用(ES2022 允许模块顶级await,但需注意兼容性)。 async函数作为回调时需谨慎:如setTimeout(async () => { ... }, 1000)中,若内部有未捕获的错误,会成为 “未处理的 Promise 拒绝”。