JS场景应用

210 阅读36分钟

1.Uint8Array

作用是创建一个长度为 168 位无符号整数数组,用于存储原始二进制数据
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(),后者可能被恶意预测);
  • 直接修改传入的缓冲区(不会返回新对象),效率高;
  • 只能处理 “类型化数组”(如 Uint8ArrayUint16Array 等,用于存储二进制数据)。

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 是一个十六进制表示的整数

  1. 前缀 0x:表示这是一个十六进制数(基数为 16),是编程语言中(如 JavaScript、C、Java 等)表示十六进制的标准语法。

  2. 数值 0f:十六进制中,0-9 对应十进制的 0-9a-f(或 A-F)对应十进制的 10-15。因此 0x0f 中,0 和 f 是十六进制数字,整体表示:

    • 十进制:0×16¹ + 15×16⁰ = 15
    • 二进制:00001111(每个十六进制数字对应 4 位二进制,00000f1111

“高 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)

  • 作用:将bufferUint8Array类型的 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}

.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 规范通常使用小写,例如FFff

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("外部:队列已全部执行完毕");
});

执行顺序如下:
  1. 调用 enqueue 添加任务 1,此时 isRunning = false,触发 processQueue

  2. processQueue 取出任务 1 执行,打印 “任务 1 开始”→ 等待 1 秒 → 打印 “任务 1 完成”,通过 resolve 通知任务 1 完成。

  3. finally 中递归调用 processQueue,此时队列中有任务 2,取出执行:打印 “任务 2 开始”→ 等待 0.5 秒 → 打印 “任务 2 完成”。

  4. 再次递归调用 processQueue,发现队列为空:

    • 标记 isRunning = false
    • 执行 updateName,打印 “所有任务完成,执行收尾操作”;
    • 调用 resolveQueueComplete,触发 queueComplete() 的回调,打印 “外部:队列已全部执行完毕”;
    • 重置 queueCompletePromise,供下次添加任务使用。

核心含义

  1. 串行执行保证:通过队列 + 递归,强制任务按添加顺序依次执行,解决异步任务并行导致的 “顺序混乱” 问题(例如:前一个任务的结果是后一个任务的依赖)。
  2. 自动收尾机制:所有任务完成后,会自动执行 updateName 函数(参数传入),适合用于 “批量任务执行完毕后统一更新数据 / UI” 的场景(如示例中更新 #Name 标签)。
  3. 外部可感知状态:通过 queueComplete() 返回的 Promise,外部可以等待整个队列执行完毕后再进行后续操作(如关闭加载动画、提示用户 “全部完成”)。
  4. 错误隔离:单个任务执行失败(catch 捕获错误)不会阻断整个队列,只会通过 reject 通知该任务的调用者,队列会继续执行下一个任务。

10.for...of

用于遍历可迭代对象的循环语法,更简洁且专注于迭代元素本身。

  1. 遍历可迭代对象可迭代对象包括:数组(Array)、字符串(String)、Map、Set、类数组对象(如 arguments、DOM 集合)等。

  2. 直接获取元素值循环变量直接指向当前迭代的元素值,而非索引或键名(区别于 for 循环的索引和 for...in 的键名)。

  3. 无法直接获取索引若需要索引,需配合 Array.prototype.entries() 使用。

  4. 不可中途修改迭代对象的长度不会像传统 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> 标签下的代码区域。组件初始化时会执行一次的 “最外层代码” ,是组件逻辑的 “入口起点”。

  1. 当组件被初始化(如首次渲染、被复用、父组件重新渲染导致子组件重新创建等)时,根作用域中的代码会重新执行一遍
  2. console.log("已开启") 不在任何函数(如 eventBus.on 的回调、setup 函数的内部逻辑等)中,而是直接写在组件的根级作用域(即 setup 函数的顶层,或 <script setup> 的顶层)。

导致多次打印:

  • 组件被多次创建:如果该组件在页面中被多次使用(例如循环渲染多个节点),每个实例初始化时都会执行一次该打印。
  • 父组件重新渲染:当父组件因状态变化重新渲染时,子组件可能被销毁后重新创建,导致根作用域代码再次执行。
  • 热重载触发:在开发环境中,修改代码后触发热重载(HMR),组件会重新初始化,也会导致打印执行。

正确做法:将副作用代码放在 “有明确触发时机” 的地方,如:

“副作用代码” 指会影响外部环境或重复执行有问题的代码(如 console.log、接口请求、修改 DOM 等)。

  • 生命周期钩子(onMounted/onUpdated,仅特定阶段执行);
  • 事件回调(如 eventBus.on/onClick,仅事件触发时执行);
  • 响应式监听(watch,仅依赖变化时执行)。

14.键盘操作监听

724c0ca5-adba-4fde-8cf5-5fbec3649507.png

  // 组件挂载时注册事件
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 为假值(undefinednull、空对象等),则 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); // falsenull不是数组)
Array.isArray({ length: 3 }); // false(类数组对象不是数组)

使用typeof 对数组返回 'object',无法区分数组和普通对象

typeof []; // 'object'(不准确)

运用场景

数组方法调用前的校验:当需要调用 mapforEachfilter 等数组特有的方法时,先判断变量是否为数组,避免报错

接口数据格式验证:处理后端返回数据时,验证数组类型的字段是否符合预期

  if (Array.isArray(result.data)) {
    renderUsers(result.data); // 渲染用户列表
  } else {
    console.error('接口返回的数据不是数组');
  }

函数参数类型校验:限制函数参数必须为数组,确保逻辑正确性

  if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
    throw new Error('参数必须是数组');
  }

处理类数组对象:区分真正的数组和类数组对象(如 argumentsNodeList),避免误用数组方法

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:原数组本身(可选)
  • 返回值:一个新数组,包含每个元素经过处理后的结果

  1. 不改变原数组map() 会返回全新的数组,原数组的元素不会被修改(除非在回调中主动修改引用类型的属性)。
  2. 长度不变:返回的新数组长度与原数组完全一致(每个元素都会被处理并返回结果)。
  3. 回调必 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`
  1. 查找条件node => !node.config.ParentId || node.config.ParentId === '' || node.config.ParentId === '0'对数组中的每个 node(节点)进行判断,满足以下任一条件即匹配:

    • !node.config.ParentIdParentId 不存在、为 nullundefined 或 0( falsy 值)
    • node.config.ParentId === ''ParentId 是空字符串
    • node.config.ParentId === '0'ParentId 是字符串 '0'(通常表示 “无父节点”)
  2. 结果赋值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.namethis.agegetInfo(需要知道 “当前实例的姓名年龄”)
静态方法(有 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(...)) :创建出zhangsanlisi两个具体的 “人”,各自有自己的nameage

实例方法(sayHello):依赖实例的nameage,必须通过实例(zhangsanlisi)才能调用,且输出结果由实例自己的属性决定。

运用场景

比如电商系统中,“用户” 类需要实例化为多个用户对象(user1user2),每个用户有自己的账号、购物车,操作互不影响。

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);
  }
}
  1. await 不要滥用:无依赖的异步任务用 Promise.all() 并行执行,避免用 await 顺序等待(浪费性能)。
  2. 错误必须捕获async 函数中未捕获的错误会导致返回的 Promise 失败,可能引发全局错误(如 Node.js 进程崩溃、浏览器控制台报错)。
  3. 避免在顶级作用域使用 awaitawait 只能在 async 函数内使用(ES2022 允许模块顶级 await,但需注意兼容性)。
  4. async 函数作为回调时需谨慎:如 setTimeout(async () => { ... }, 1000) 中,若内部有未捕获的错误,会成为 “未处理的 Promise 拒绝”。