本文将详细讲解JavaScript的基础语法,包括变量、数据类型、运算符、表达式等内容,适合初学者阅读。

1.JavaScript 的数据类型有哪些?基本数据类型和引用数据类型的区别是什么?

一、数据类型

  1. 基本数据类型:Number、String、Boolean、Null、Undefined、Symbol(ES6)、BigInt(ES2020)。
  2. 引用数据类型:Object(含Array、Function、Date、RegExp等)。

二、核心区别

维度 基本数据类型 引用数据类型
存储 栈内存直接存值 栈存地址,堆内存存值
赋值 值拷贝,变量独立 地址拷贝,指向同一对象
比较 比较值是否相等 比较引用地址是否相同
可变性 不可变(修改生成新值) 可变(直接修改内部属性)

2.如何判断一个变量的数据类型?typeof、instanceof、Object.prototype.toString () 的区别是什么?

一、判断变量数据类型的常用方法

常用方法有三种:typeofinstanceofObject.prototype.toString()

二、各方法特点及区别

1. typeof

  • 作用:检测基本数据类型(返回字符串)。
  • 返回值:如 "number""string""boolean""undefined""symbol""bigint""function"(函数)、"object"(对象/数组/null等)。
  • 局限
    • null 会被误判为 "object"(历史遗留问题)。
    • 数组、日期等引用类型均返回 "object",无法区分。

2. instanceof

  • 作用:检测对象是否为某个构造函数的实例(基于原型链),返回布尔值。
  • 示例[1,2] instanceof Array → truenew Date() instanceof Date → true
  • 局限
    • 无法检测基本数据类型(非对象)。
    • 跨 iframe 实例检测可能失效(原型链不同)。
    • 所有对象都是 Object 的实例(如 [] instanceof Object → true)。

3. Object.prototype.toString()

  • 作用:返回统一格式的类型字符串 [object 类型],最准确。
  • 示例
    • Object.prototype.toString.call(123) → "[object Number]"
    • Object.prototype.toString.call([]) → "[object Array]"
    • Object.prototype.toString.call(null) → "[object Null]"
  • 优势:可准确区分所有类型(包括 nullundefined 及各种引用类型)。

三、核心区别对比

方法 检测范围 返回形式 典型局限
typeof 基本类型为主 字符串(如”number”) 无法区分对象/数组/null
instanceof 引用类型(基于原型链) 布尔值 不支持基本类型,跨环境可能失效
Object.prototype.toString() 所有类型 统一格式字符串(如”[object Array]”) 无明显局限,需调用 call() 绑定目标

3.简述 JavaScript 中的变量提升(Hoisting)现象,函数声明和变量声明的提升优先级有何不同?

一、变量提升(Hoisting)现象

变量提升是 JavaScript 引擎的一种预编译机制:在代码执行前,引擎会将变量声明函数声明提升到其所在作用域的顶部(但初始化/赋值不会被提升)。
这意味着可以在声明前使用这些变量或函数(但可能导致不符合预期的结果)。

二、函数声明与变量声明的提升优先级

  1. 函数声明的提升:整个函数体(包括定义)会被完整提升到作用域顶部。
    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
       foo(); // 输出 "hello"(函数声明被提升,可提前调用)
    function foo() { console.log("hello"); }
    ```

    2. **变量声明的提升**:仅声明被提升,初始化/赋值留在原地(`var` 声明会被提升到作用域顶部,`let/const` 有“暂时性死区”,声明前使用会报错)。
    例(`var`):

    ```javascript
    console.log(a); // 输出 undefined(声明被提升,赋值未提升)
    var a = 10;
    ```

    3. **优先级差异**:
    函数声明的提升优先级**高于变量声明**。若同名的函数声明和变量声明存在于同一作用域,变量声明不会覆盖函数声明(除非变量被赋值)。
    例:

    ```javascript
    console.log(foo); // 输出函数体(函数声明优先提升)
    var foo = 10;
    function foo() {}
    ```

    简言之:函数声明整体提升,优先级高于变量声明;变量声明仅提升声明部分,初始化留在原位。

    ## 4.什么是作用域?JavaScript 中的作用域类型(全局作用域、函数作用域、块级作用域)有哪些?

    ### 什么是作用域?

    作用域是JavaScript中变量、函数和对象的可访问范围,决定了标识符(变量名、函数名等)在代码中的可见性和生命周期,用于隔离不同范围的变量,避免命名冲突。

    ### JavaScript 中的作用域类型

    #### 1. 全局作用域

    - 定义:在所有函数和代码块之外声明的变量/函数,拥有全局作用域。
    - 特点:在程序的任何位置都可访问;浏览器环境中,全局变量默认挂载在`window`对象上(Node.js中挂载在`global`上)。
    - 示例:

    ```javascript
    const globalVar = "全局变量"; // 全局作用域
    function foo() {
    console.log(globalVar); // 可访问全局变量
    }
    ```

    #### 2. 函数作用域

    - 定义:在函数内部声明的变量/函数,仅在该函数内部可访问,外部无法直接访问。
    - 特点:每次函数调用会创建一个独立的函数作用域;函数参数也属于函数作用域。
    - 示例:

    ```javascript
    function bar() {
    const funcVar = "函数内变量"; // 函数作用域
    console.log(funcVar); // 可访问
    }
    console.log(funcVar); // 报错:funcVar未定义(外部不可访问)
    ```

    #### 3. 块级作用域(ES6 新增)

    - 定义:由`{}`(如`if`、`for`、`while`、`switch`或独立代码块)包裹的区域,使用`let`或`const`声明的变量具有块级作用域。
    - 特点:变量仅在当前代码块内可访问;`var`声明的变量不具备块级作用域(会泄露到块外)。
    - 示例:

    ```javascript
    if (true) {
    let blockVar = "块内变量"; // 块级作用域
    console.log(blockVar); // 可访问
    }
    console.log(blockVar); // 报错:blockVar未定义(块外不可访问)

5.什么是闭包?闭包的形成条件是什么?有哪些应用场景和潜在问题?

什么是闭包?

闭包是 JavaScript 中的一种特殊现象:当一个内部函数被外部函数之外的变量引用时,内部函数会保留对其声明时所在的外部函数作用域的访问权限——即使外部函数已经执行完毕,内部函数依然能访问外部函数中的变量、参数等。

简单来说,闭包的核心是“函数记住了它诞生的环境,即使离开这个环境也能使用环境中的资源”。
示例:

1
2
3
4
5
6
7
8
9
10
11
function outer() {
const outerVar = "我是外部函数的变量"; // 外部函数的变量
// 内部函数
function inner() {
console.log(outerVar); // 内部函数访问外部函数的变量
}
return inner; // 外部函数返回内部函数(让内部函数被外部引用)
}

const func = outer(); // 执行外部函数,将内部函数赋值给外部变量func
func(); // 输出 "我是外部函数的变量" —— 闭包生效:inner仍能访问outer的变量

闭包的形成条件

闭包的形成必须同时满足以下 3 个条件,缺一不可:

  1. 函数嵌套:存在“外部函数”和“内部函数”的嵌套结构(内部函数定义在外部函数内部);
  2. 作用域引用:内部函数主动引用了外部函数作用域中的变量/参数(若内部函数不引用外部资源,则无闭包);
  3. 外部引用保留:外部函数执行后,内部函数没有被销毁,而是被外部函数之外的变量引用(如返回给外部变量、赋值给全局变量等)。

举例说明闭包的形成条件

闭包形成需同时满足三个条件:函数嵌套、内部函数引用外部变量、外部保留内部函数引用。以下示例清晰体现这三个条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function outer() {
// 条件1:函数嵌套(inner在outer内部定义)
const outerVar = "外部变量"; // 外部函数的变量

function inner() {
// 条件2:内部函数引用外部变量(inner引用outerVar)
console.log(outerVar);
}

// 条件3:外部保留内部函数引用(将inner返回给outer外部)
return inner;
}

// 执行外部函数后,外部变量func持有inner的引用
const func = outer();
func(); // 输出"外部变量",闭包形成(inner仍能访问outer的变量)
```

### 闭包的应用场景

闭包的核心价值是“**隔离变量+保存状态**”,常见应用场景如下:

#### 1. 实现模块化(隔离私有变量)

通过闭包将变量、函数“封装”在外部函数作用域中,仅暴露需要对外使用的接口,避免全局变量污染。
示例(简单模块化):

```javascript
const moduleA = (function() {
// 私有变量(仅内部可访问,外部无法直接修改)
let privateCount = 0;
// 私有函数
function privateAdd() {
privateCount++;
}
// 暴露公开接口(闭包保留对私有变量的访问)
return {
getCount: function() {
return privateCount;
},
addCount: function() {
privateAdd();
}
};
})();

moduleA.addCount();
console.log(moduleA.getCount()); // 输出 1
console.log(moduleA.privateCount); // 输出 undefined(私有变量隔离成功)

2. 防抖(Debounce)与节流(Throttle)

用于控制函数执行频率(如输入框搜索、滚动事件),通过闭包保存“定时器ID”“最后执行时间”等状态,避免状态丢失。
示例(简单防抖):

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(fn, delay) {
let timer = null; // 闭包保存定时器ID
return function(...args) {
clearTimeout(timer); // 每次触发前清除之前的定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

// 用法:输入框停止输入300ms后执行搜索
const search = debounce(() => console.log("搜索..."), 300);
input.addEventListener("input", search);

3. 函数柯里化(Currying)

将多参数函数拆分为一系列单参数函数,通过闭包逐步“固定”参数,返回新函数。
示例:

1
2
3
4
5
6
7
8
9
function add(a) {
return function(b) { // 闭包保留参数a
return a + b;
};
}

const add5 = add(5); // 固定第一个参数为5
console.log(add5(3)); // 输出 8(5+3)
console.log(add(2)(4)); // 输出 6(2+4)

4. 保存循环中的状态

解决“循环中异步函数访问循环变量”的经典问题(如 for(var i=0) 时,异步函数执行时 i 已变成循环结束值)。
示例:

1
2
3
4
5
6
7
8
9
10
11
// 问题代码:所有setTimeout执行时,i已变成3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出 3,3,3
}

// 闭包解决:每次循环创建独立闭包保存当前i
for (var i = 0; i < 3; i++) {
(function(j) { // j接收当前循环的i
setTimeout(() => console.log(j), 100); // 输出 0,1,2
})(i);
}

闭包的潜在问题及解决思路

闭包并非“万能”,不当使用会引发问题,核心问题是内存泄漏

1. 核心问题:内存泄漏

  • 原因:闭包会持续引用外部函数的作用域(包括其中的变量、DOM元素等),导致外部函数的作用域无法被 JavaScript 垃圾回收机制(GC)回收,长期积累会占用过多内存,甚至导致页面卡顿。

  • 示例(危险场景):

    1
    2
    3
    4
    5
    6
    7
    8
    function createClosure() {
    const bigData = new Array(1000000).fill("大内存数据"); // 占用大量内存
    return function() {
    console.log(bigData); // 闭包引用bigData,导致bigData无法被回收
    };
    }

    const unusedFunc = createClosure(); // unusedFunc未被使用,但闭包仍持有bigData

2. 其他问题:变量污染风险

若闭包引用的外部变量是全局变量或长期存在的变量,可能导致变量被意外修改(如多个闭包共享同一外部变量时)。

3. 解决思路

  • 及时解除引用:当闭包不再需要使用时,将引用闭包的变量设为 null,切断闭包与外部的关联,让 GC 能回收外部作用域。

    1
    unusedFunc = null; // 解除闭包引用,bigData可被GC回收
  • 避免不必要的闭包:仅在需要“保存状态”或“隔离变量”时使用闭包,不滥用;

  • 减少闭包引用的变量范围:若仅需外部函数的某个变量,避免引用整个作用域(如通过参数传递而非直接引用外部变量)。

6.简述 JavaScript 中的 this 关键字的指向规则(全局、函数、对象方法、构造函数、箭头函数)

JavaScript 中 this 关键字的指向规则

1. 全局环境中的 this

  • 指向全局对象:浏览器环境中指向 window,Node.js 环境中指向 global

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
      console.log(this === window); // 浏览器中输出true
    ```

    #### 2. 普通函数中的 this

    - 非严格模式:指向全局对象(`window`/`global`)。
    - 严格模式(`"use strict"`):指向 `undefined`。
    - 示例:

    ```javascript
    function foo() {
    console.log(this); // 非严格模式→window;严格模式→undefined
    }
    foo();
    ```

    #### 3. 对象方法中的 this

    - 指向**调用该方法的对象**(即方法名前的对象)。
    - 示例:

    ```javascript
    const obj = {
    name: "test",
    sayName() {
    console.log(this.name); // this指向obj
    }
    };
    obj.sayName(); // 输出"test"(this指向调用者obj)
    ```

    #### 4. 构造函数中的 this

    - 指向**通过 `new` 关键字创建的实例对象**。
    - 示例:

    ```javascript
    function Person(name) {
    this.name = name; // this指向新创建的实例
    }
    const person = new Person("张三");
    console.log(person.name); // 输出"张三"(this绑定到person实例)
    ```

    #### 5. 箭头函数中的 this

    - 无自己的 `this`,继承**外层作用域的 this**(定义时确定,永不改变)。
    - 不受 `call`/`apply`/`bind` 影响。
    - 示例:

    ```javascript
    const obj = {
    foo() {
    const bar = () => {
    console.log(this); // 继承foo的this(指向obj)
    };
    bar();
    }
    };
    obj.foo(); // 输出obj

7.如何改变 this 的指向?call、apply、bind 的区别是什么?

如何改变 this 的指向?

在 JavaScript 中,可通过 call()apply()bind() 三种方法主动改变函数中 this 的指向,它们均为函数的原型方法,需通过函数调用。

call、apply、bind 的区别

1. 基本用法与核心作用

三者的核心作用一致:指定函数执行时 this 指向的对象,但调用方式和执行时机不同。

  • **call()**:
    语法:函数.call(thisArg, arg1, arg2, ...)
    作用:立即执行函数,thisArgthis 指向的对象,后续参数为函数的参数列表(逐个传入)。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
      function sayHi(greeting) {
    console.log(`${greeting}, 我是${this.name}`);
    }
    const person = { name: "张三" };

    sayHi.call(person, "你好"); // 立即执行,this指向person,输出:"你好, 我是张三"
    ```

    - **`apply()`**:
    语法:`函数.apply(thisArg, [arg1, arg2, ...])`
    作用:立即执行函数,`thisArg` 为 `this` 指向的对象,第二个参数为**数组或类数组**(包含函数所需的所有参数)。

    示例:

    ```javascript
    sayHi.apply(person, ["Hello"]); // 立即执行,参数以数组传入,输出:"Hello, 我是张三"
    ```

    - **`bind()`**:
    语法:`const 新函数 = 函数.bind(thisArg, arg1, arg2, ...)`
    作用:**不立即执行函数**,而是返回一个新函数,新函数中 `this` 固定指向 `thisArg`;参数可分多次传入(绑定时分批传入,调用新函数时补充剩余参数)。

    示例:

    ```javascript
    const boundSayHi = sayHi.bind(person, "Hi"); // 返回新函数,this固定为person,预传参数"Hi"
    boundSayHi(); // 后续调用时执行,输出:"Hi, 我是张三"
    ```

    #### 2. 核心区别对比

    | 维度 | `call()` | `apply()` | `bind()` |
    |--------------|---------------------------|---------------------------|-----------------------------|
    | **执行时机** | 立即执行函数 | 立即执行函数 | 不立即执行,返回新函数 |
    | **参数传递** | 参数列表(逐个传入) | 数组/类数组(一次性传入) | 可分多次传入(绑定和调用时)|
    | **返回值** | 函数执行的结果 | 函数执行的结果 | 绑定了 `this` 的新函数 |
    | **典型场景** | 参数数量固定时 | 参数为数组时(如Math.max)| 需要延迟执行时(如事件回调)|

    #### 3. 特殊说明

    - 若 `thisArg` 为 `null` 或 `undefined`,非严格模式下 `this` 会指向全局对象(如浏览器的 `window`),严格模式下仍为 `null`/`undefined`。
    - `bind()` 绑定的 `this` 无法被再次修改(即使使用 `call`/`apply` 也无效)。

    ## 8.什么是原型(Prototype)?简述 JavaScript 的原型链机制

    ### 什么是原型(Prototype)

    在 JavaScript 中,**原型(Prototype)是对象的一个内置属性**,每个对象(除 `null` 外)都有一个原型,用于存储可被该对象及其“后代”对象共享的属性和方法。

    - 函数作为特殊对象,有一个 `prototype` 属性(显式原型),该属性的值是一个对象,称为“原型对象”。当通过函数(构造函数)创建实例时,实例的原型(隐式原型,可通过 `Object.getPrototypeOf(实例)` 访问,早期浏览器用 `__proto__` 表示)会指向构造函数的 `prototype` 属性。
    - 原型对象自身也是对象,因此也有自己的原型,形成层级关系。

    ### JavaScript 的原型链机制

    原型链是 JavaScript 实现继承的核心机制,其本质是**对象通过原型层层关联形成的链式结构**。

    #### 核心逻辑

    当访问一个对象的属性或方法时,JavaScript 引擎会先在**对象自身**查找:

    - 若找到,则直接使用;
    - 若未找到,则自动沿着**对象的原型(`__proto__`)** 向上查找;
    - 若原型中仍未找到,则继续查找**原型的原型**,以此类推,直到找到目标属性/方法,或到达原型链的顶端(`null`),此时返回 `undefined`。

    #### 示例说明

    ```javascript
    // 构造函数
    function Person(name) {
    this.name = name; // 实例自身属性
    }

    // 构造函数的prototype(原型对象),存储共享方法
    Person.prototype.sayHi = function() {
    console.log(`Hi, ${this.name}`);
    };

    // 创建实例
    const person = new Person("张三");

    // 访问实例自身属性:直接找到
    console.log(person.name); // "张三"

    // 访问方法:自身未找到,沿原型链查找
    person.sayHi(); // "Hi, 张三"(在Person.prototype中找到)

    // 访问Object.prototype的方法:继续向上查找
    console.log(person.toString()); // "[object Object]"(在Object.prototype中找到)

    // 原型链顶端:Object.prototype的原型是null
    console.log(Object.getPrototypeOf(Object.prototype)); // null
    ```

    上述示例中,原型链为:`person → Person.prototypeObject.prototypenull`。

    简言之,原型是对象共享属性的载体,原型链是属性查找的路径,通过这种机制,JavaScript 实现了对象间的属性和方法继承。

    ## 9.构造函数、原型对象、实例对象之间的关系是什么?

    ### 构造函数、原型对象、实例对象的关系

    三者通过**显式原型(`prototype`)** 和**隐式原型(`__proto__`)** 形成关联,核心关系可概括为:**构造函数创建实例,原型对象作为中间载体实现实例间的属性共享**。

    #### 1. 构造函数与原型对象的关系

    - 构造函数(如 `function Person() {}`)是用于创建实例的函数,它有一个**显式原型属性(`prototype`)**,该属性的值是一个对象,即“原型对象”。
    - 原型对象有一个**`constructor` 属性**,默认指向对应的构造函数(形成双向引用)。

    示例:

    ```javascript
    function Person() {} // 构造函数
    const proto = Person.prototype; // 原型对象(构造函数的prototype指向原型对象)

    console.log(proto.constructor === Person); // true(原型对象的constructor指向构造函数)
    ```

    #### 2. 构造函数与实例对象的关系

    - 实例对象(如 `const p = new Person()`)是通过 `new` 关键字调用构造函数创建的对象。
    - 实例对象的 `constructor` 属性(继承自原型对象)默认指向创建它的构造函数。

    示例:

    ```javascript
    const p = new Person(); // 实例对象(通过构造函数创建)

    console.log(p.constructor === Person); // true(实例的constructor指向构造函数)
    ```

    #### 3. 实例对象与原型对象的关系

    - 实例对象有一个**隐式原型属性(`__proto__`,标准访问方式为 `Object.getPrototypeOf(实例)`)**,该属性的值**指向创建它的构造函数的原型对象**。
    - 这意味着:实例对象可以直接访问原型对象中定义的属性和方法(通过原型链查找机制)。

    示例:

    ```javascript
    // 实例的隐式原型 指向 构造函数的显式原型(即原型对象)
    console.log(p.__proto__ === Person.prototype); // true
    console.log(Object.getPrototypeOf(p) === Person.prototype); // true(标准写法)
    ```

    #### 总结关系链

构造函数 →(prototype)→ 原型对象
构造函数 ←(constructor)← 原型对象
实例对象 →(__proto__)→ 原型对象
实例对象 ←(new)← 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277

简言之:构造函数通过 `prototype` 关联原型对象,原型对象通过 `constructor` 关联构造函数;实例对象通过 `new` 由构造函数创建,通过 `__proto__` 关联原型对象,从而实现对原型中共享属性的访问。

## 10.什么是继承?JavaScript 中实现继承的方式有哪些(如原型链继承、构造函数继承、组合继承)?

### 什么是继承?

继承是面向对象编程(OOP)的核心概念之一,指**子类(派生类)可以复用父类(基类)的属性和方法,并可扩展自身特性**的机制。其核心价值是减少代码冗余,实现代码复用。

### JavaScript 中实现继承的主要方式

JavaScript 没有传统面向对象语言的“类继承”语法(ES6 的 `class` 本质是原型继承的语法糖),而是基于原型链实现继承,常见方式如下:

#### 1. 原型链继承

**原理**:将子类的原型对象指向父类的实例,使子类实例通过原型链访问父类的属性和方法。

示例:

```javascript
// 父类
function Parent() {
this.parentProp = "父类实例属性";
}
Parent.prototype.parentMethod = function() {
console.log("父类原型方法");
};

// 子类
function Child() {}
// 核心:子类原型指向父类实例,形成原型链
Child.prototype = new Parent();
// 修复子类原型的constructor指向(否则会指向Parent)
Child.prototype.constructor = Child;

// 子类实例
const child = new Child();
console.log(child.parentProp); // 访问父类实例属性 → "父类实例属性"
child.parentMethod(); // 访问父类原型方法 → "父类原型方法"
```

**优点**:简单直观,可继承父类的实例属性和原型方法。
**缺点**:

- 子类所有实例共享父类实例的属性(修改一个实例的属性会影响其他实例);
- 无法在创建子类实例时向父类构造函数传参。

#### 2. 构造函数继承(借用构造函数)

**原理**:在子类构造函数中通过 `call()` 或 `apply()` 调用父类构造函数,强制将父类的属性绑定到子类实例上。

示例:

```javascript
// 父类
function Parent(name) {
this.name = name; // 父类实例属性
this.sayName = function() {
console.log(this.name);
};
}

// 子类
function Child(name, age) {
// 核心:调用父类构造函数,将this指向子类实例
Parent.call(this, name);
this.age = age; // 子类自身属性
}

// 子类实例
const child = new Child("张三", 18);
child.sayName(); // 访问父类构造函数中的方法 → "张三"
console.log(child.age); // 子类自身属性 → 18
```

**优点**:

- 子类实例的属性独立(不共享);
- 可在创建子类实例时向父类构造函数传参。

**缺点**:

- 仅能继承父类构造函数中的属性和方法,无法继承父类原型上的方法;
- 父类方法在每个子类实例中重复创建(函数无法复用,浪费内存)。

#### 3. 组合继承(原型链+构造函数)

**原理**:结合原型链继承和构造函数继承的优点——用**构造函数继承属性**(确保独立性和传参),用**原型链继承方法**(实现函数复用)。

示例:

```javascript
// 父类
function Parent(name) {
this.name = name; // 实例属性(通过构造函数继承)
}
Parent.prototype.sayName = function() { // 原型方法(通过原型链继承)
console.log(this.name);
};

// 子类
function Child(name, age) {
// 1. 构造函数继承:继承父类实例属性,支持传参
Parent.call(this, name);
this.age = age; // 子类自身属性
}

// 2. 原型链继承:继承父类原型方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 子类原型方法
Child.prototype.sayAge = function() {
console.log(this.age);
};

// 子类实例
const child = new Child("张三", 18);
child.sayName(); // 继承父类原型方法 → "张三"
child.sayAge(); // 子类自身方法 → 18
```

**优点**:

- 既继承了父类的实例属性(独立且可传参),又继承了父类的原型方法(函数复用);
- 是 JavaScript 中最常用的继承方式之一。

**缺点**:

- 父类构造函数被调用两次(一次是 `Child.prototype = new Parent()`,一次是 `Parent.call(...)`),导致子类原型上冗余父类实例属性。

#### 其他常见方式(补充)

- **原型式继承**:通过 `Object.create()` 基于现有对象创建新对象,本质是浅拷贝原型(适合简单对象复用)。
- **寄生式继承**:在原型式继承基础上增强新对象(添加属性/方法),但函数复用性差。
- **寄生组合式继承**:解决组合继承中父类构造函数被调用两次的问题(通过 `Object.create(Parent.prototype)` 直接继承父类原型,而非实例),是更优的实现方式。

综上,组合继承是平衡易用性和功能的常用方案,而寄生组合式继承是理论上更完美的实现(ES6 `class extends` 底层采用类似逻辑)。

## 11.ES6 中的 class 关键字如何实现继承?与 ES5 的继承方式有什么区别?

### ES6 中 `class` 关键字实现继承的方式

ES6 引入 `class` 关键字(语法糖,底层仍基于原型链),通过 `extends` 和 `super` 关键字实现继承,语法更接近传统面向对象语言,步骤如下:

#### 1. 基本用法

- 用 `class` 定义父类和子类,子类通过 `extends` 继承父类;
- 子类构造函数中需用 `super()` 调用父类构造函数(否则无法使用 `this`);
- 父类的静态方法/属性会被子类继承(通过 `extends`)。

示例:

```javascript
// 父类
class Parent {
constructor(name) {
this.name = name; // 实例属性
}
// 原型方法
sayName() {
console.log(this.name);
}
// 静态方法(属于类本身)
static staticMethod() {
console.log("父类静态方法");
}
}

// 子类:通过 extends 继承 Parent
class Child extends Parent {
constructor(name, age) {
// 必须先调用 super() 才能使用 this(super 指向父类构造函数)
super(name);
this.age = age; // 子类实例属性
}
// 子类原型方法
sayAge() {
console.log(this.age);
}
}

// 实例化子类
const child = new Child("张三", 18);
child.sayName(); // 继承父类原型方法 → "张三"
child.sayAge(); // 子类自身方法 → 18
Child.staticMethod(); // 继承父类静态方法 → "父类静态方法"
```

#### 2. 核心机制

- **原型链关联**:子类的原型(`Child.prototype`)的隐式原型指向父类的原型(`Parent.prototype`),即 `Object.getPrototypeOf(Child.prototype) === Parent.prototype`,确保子类实例能访问父类原型方法。
- **构造函数继承**:`super()` 等价于在子类构造函数中调用 `Parent.call(this, name)`,实现父类实例属性的继承。
- **静态成员继承**:子类的隐式原型(`Child.__proto__`)指向父类(`Parent`),因此父类的静态方法/属性可被子类直接访问。

### ES6 `class` 继承与 ES5 继承的区别

| 维度 | ES6 `class` 继承 | ES5 继承(如组合继承) |
|---------------------|------------------------------------------|------------------------------------------|
| **语法形式** | 基于 `class`、`extends`、`super` 关键字,声明式语法,更简洁直观。 | 基于构造函数+原型链手动组合(如 `Child.prototype = new Parent()` + `Parent.call(this)`),命令式语法,较繁琐。 |
| **继承本质** | 语法糖,底层仍基于原型链,但引擎做了优化(类似寄生组合式继承,避免父类构造函数被调用两次)。 | 需手动处理原型链关联和构造函数调用,组合继承会导致父类构造函数被调用两次(一次初始化子类原型,一次在子类构造函数中)。 |
| **`super` 关键字** | 必须在子类构造函数中调用 `super()` 才能使用 `this`,且 `super` 可作为函数(调用父类构造)或对象(访问父类原型/静态方法)。 | 无 `super`,需手动用 `Parent.call(this, ...)` 调用父类构造函数,访问父类原型方法需通过 `Parent.prototype`。 |
| **静态成员继承** | 自动继承父类静态方法/属性(通过 `Child.__proto__ = Parent` 实现)。 | 需手动绑定(如 `Child.staticMethod = Parent.staticMethod`),否则无法继承。 |
| **方法定义位置** | 类中定义的方法(非静态)自动挂载到子类原型上,无需手动操作 `prototype`。 | 需手动往 `Child.prototype` 上添加方法(如 `Child.prototype.sayAge = function() {}`)。 |
| **严格模式** | 类内部默认运行在严格模式下(`"use strict"`)。 | 默认非严格模式,需手动声明严格模式。 |
| **可读性与维护性** | 语法更接近传统 OOP 语言,结构清晰,易理解和维护。 | 依赖原型链细节,对新手不友好,易出错(如忘记修复 `constructor` 指向)。 |

简言之,ES6 `class` 继承并未改变 JavaScript 基于原型的本质,但通过语法糖简化了继承实现,减少了手动操作原型链的出错概率,使代码更具可读性和规范性。

## 12.简述 JavaScript 中的垃圾回收机制(GC),常见的垃圾回收算法有哪些?

### 什么是 JavaScript 中的垃圾回收机制(GC)

垃圾回收(Garbage Collection,简称 GC)是 JavaScript 引擎自动管理内存的机制:**引擎会定期找出不再被使用的内存(“垃圾”),并释放这些内存空间**,无需开发者手动操作。其核心目标是防止内存泄漏,确保程序高效运行。

“不再被使用的内存”指的是**无法通过任何引用链访问到的对象**(即没有任何变量、函数等引用该对象)。

### 常见的垃圾回收算法

#### 1. 标记-清除算法(Mark-and-Sweep)

目前大多数 JavaScript 引擎(如 V8、SpiderMonkey)的主流算法,分为“标记”和“清除”两个阶段:

- **标记阶段**:从全局对象(如 `window`)开始,遍历所有可访问的对象(“活动对象”)并做标记;
- **清除阶段**:回收所有未被标记的对象(“非活动对象”),释放其占用的内存。

**优点**:实现简单,能处理循环引用(两个对象互相引用但均无外部引用时,会被标记为非活动对象)。
**缺点**:回收后会产生内存碎片(未使用的内存块分散),可能导致后续分配大对象时无法找到连续内存。

#### 2. 引用计数算法(Reference Counting)

早期部分引擎使用的算法,已逐渐被淘汰,原理是:

- 跟踪每个对象被引用的次数(“引用计数”);
- 当引用次数为 0 时,自动回收该对象。

**缺点**:无法解决“循环引用”问题(如 `a.ref = b; b.ref = a;` 且无其他引用时,两者引用计数均为 1,不会被回收,导致内存泄漏),因此现代引擎极少使用。

#### 3. 标记-整理算法(Mark-and-Compact)

标记-清除算法的改进版,在“标记”后增加“整理”步骤:

- **标记阶段**:与标记-清除一致,标记活动对象;
- **整理阶段**:将所有活动对象向内存空间的一端移动,使空闲内存集中为连续块;
- **清除阶段**:回收边界外的内存。

**优点**:解决了标记-清除的内存碎片问题,适合需要分配大内存的场景。

#### 4. 分代回收算法(Generational Collection)

V8 等现代引擎采用的优化策略,基于“大部分对象存活时间短”的观察,将内存分为两代:

- **新生代(Young Generation)**:存放存活时间短的对象(如临时变量),采用 **Scavenge 算法**(将内存分为两半,复制活动对象到另一半,回收原空间,效率高);
- **老生代(Old Generation)**:存放存活时间长的对象(如全局变量),采用标记-清除+标记-整理算法(兼顾效率和内存连续性)。

**优点**:针对不同生命周期的对象采用不同算法,大幅提升垃圾回收效率。

综上,现代 JavaScript 引擎以标记-清除为基础,结合分代回收、标记-整理等策略,平衡了回收效率和内存利用率,自动为开发者处理内存管理。

## 13.什么是内存泄漏?JavaScript 中常见的内存泄漏场景有哪些(如未清理的定时器、闭包)?

### 什么是内存泄漏?

内存泄漏(Memory Leak)指**程序中不再需要使用的内存未能被垃圾回收机制(GC)正确释放**,导致内存占用持续增加的现象。长期积累会导致程序性能下降(如卡顿、响应缓慢),严重时可能引发进程崩溃。

### JavaScript 中常见的内存泄漏场景

#### 1. 未清理的定时器/计时器(`setTimeout`/`setInterval`)

- **原因**:定时器若引用了外部对象(如DOM元素、大数组),即使该对象已不再需要,只要定时器未被清除(`clearTimeout`/`clearInterval`),引用就会持续存在,导致对象无法被GC回收。
- **示例**:

```javascript
function createTimer() {
const bigData = new Array(1000000).fill('占用大量内存');
// 定时器引用了bigData,若未清理,bigData永远不会被回收
setInterval(() => {
console.log(bigData);
}, 1000);
}
createTimer(); // 调用后,即使无其他引用,bigData因定时器引用而无法释放
```

#### 2. 闭包滥用导致的引用残留

- **原因**:闭包会保留对外部作用域的引用,若闭包被长期持有(如赋值给全局变量),且引用了大对象或DOM元素,这些资源会因闭包的引用而无法释放。
- **示例**:

```javascript
let globalFunc;
function outer() {
const dom = document.getElementById('temp'); // 临时DOM元素
globalFunc = function() {
// 闭包引用dom,即使dom已从页面移除
console.log(dom);
};
}
outer();
document.body.removeChild(document.getElementById('temp')); // 移除DOM
// 但globalFunc仍持有dom的引用,导致dom无法被GC回收
```

#### 3. 未移除的DOM引用

- **原因**:若JavaScript变量保存了DOM元素的引用,即使该DOM已从页面中移除(如`removeChild`),只要JS中的引用未清除,DOM对象就不会被回收(DOM对象同时存在于JS堆和渲染引擎中)。
- **示例**:

```javascript
const elements = [];
function addElement() {
const div = document.createElement('div');
elements.push(div); // 数组保存DOM引用
document.body.appendChild(div);
}
function removeElement() {
const div = elements[0];
document.body.removeChild(div); // 从页面移除DOM
// 但elements数组仍保留div的引用,导致div无法被回收
}
```

#### 4. 意外创建的全局变量

- **原因**:未声明的变量(如`a = 10`而非`let a = 10`)会默认挂载到全局对象(`window`)上,全局变量的生命周期与页面一致,若存储大量数据,会长期占用内存。
- **示例**:

```javascript
function fn() {
// 未声明的变量,自动成为window的属性(全局变量)
largeData = new Array(1000000).fill('大数据');
}
fn(); // 调用后,largeData作为全局变量永远存在,不会被回收
```

#### 5. 未解绑的事件监听器

- **原因**:为DOM元素绑定的事件监听器(如`addEventListener`)若未在元素移除前解绑(`removeEventListener`),监听器函数及它引用的对象会被持续持有,导致内存泄漏。
- **示例**:

```javascript
const btn = document.getElementById('btn');
function handleClick() {
console.log('点击事件');
}
btn.addEventListener('click', handleClick);
// 移除按钮但未解绑事件
document.body.removeChild(btn);
// handleClick及它引用的资源因事件监听关联而无法被回收
```

#### 6. 缓存未设置过期机制

- **原因**:使用对象/Map作为缓存时,若只添加数据而不清理过期数据,缓存会无限增长,占用越来越多内存(尤其缓存大对象时)。
- **示例**:

```javascript
const cache = {};
function setCache(key, value) {
cache[key] = value; // 只存不删,缓存持续膨胀
}
// 频繁调用setCache存储大量数据,无清理逻辑,导致内存泄漏
```

### 总结

内存泄漏的核心是“**无用资源被意外引用而无法释放**”。避免内存泄漏的关键是:及时清理定时器、事件监听和DOM引用,避免闭包长期持有大对象,控制全局变量和缓存的生命周期。

## 14.简述 JavaScript 中的事件流(捕获阶段、目标阶段、冒泡阶段)的概念

### 什么是事件流?

事件流描述的是 **事件在DOM元素中传播的完整过程**。当一个事件(如点击、鼠标移动)在某个DOM元素上触发时,它并非仅在该元素上生效,而是会沿着DOM树的层级结构进行传播。

W3C标准规定,事件流分为三个阶段,按顺序依次执行:**捕获阶段 → 目标阶段 → 冒泡阶段**。

### 事件流三阶段的具体概念

#### 1. 捕获阶段(Capture Phase)

- **定义**:事件从最外层的祖先元素(如 `window`)开始,逐级向下传播到目标元素的父级元素,最终到达目标元素的“前夕”。
- **作用**:捕获阶段的目的是让上层元素有机会提前捕获并处理事件(较少直接使用,通常用于特殊场景如事件委托的底层拦截)。

示例:点击页面中的 `<button>` 元素,捕获阶段的传播路径为:
`window → document → html → body → ... → <button>的父元素`

#### 2. 目标阶段(Target Phase)

- **定义**:事件到达实际触发事件的元素(即“目标元素”),此时事件在目标元素上执行。
- **特点**:目标阶段是事件传播的核心,无论事件监听器注册在捕获阶段还是冒泡阶段,只要绑定在目标元素上,都会在此时触发。

#### 3. 冒泡阶段(Bubble Phase)

- **定义**:事件从目标元素开始,逐级向上传播回最外层的祖先元素(如 `window`),与捕获阶段路径相反。
- **作用**:冒泡阶段是最常用的事件传播方式,允许父元素“感知”子元素的事件(如事件委托机制依赖冒泡实现)。

示例:点击 `<button>` 元素后,冒泡阶段的传播路径为:
`<button> → <button>的父元素 → ... → body → html → document → window`

### 补充说明

- 可通过 `addEventListener` 的第三个参数 `useCapture` 控制事件监听器在哪个阶段触发:`true` 表示在捕获阶段触发,`false`(默认)表示在冒泡阶段触发。
- 可通过 `event.stopPropagation()` 阻止事件继续传播(中断捕获或冒泡),但不会影响目标阶段的执行。

简言之,事件流是事件在DOM树中“自上而下捕获→到达目标→自下而上冒泡”的完整传播过程,这一机制为事件处理提供了灵活的层级控制能力。

## 15.如何阻止事件冒泡和事件默认行为?分别使用什么方法?

### 如何阻止事件冒泡和事件默认行为

#### 1. 阻止事件冒泡(Event Bubbling)

事件冒泡是指事件在目标元素触发后,会向上传播到父元素、祖先元素的过程。阻止事件冒泡可避免上层元素的事件监听器被意外触发。

**使用方法**:调用事件对象(`event`)的 `stopPropagation()` 方法。

**示例**:

```html
<div id="parent" style="padding: 50px; background: pink;">
父元素
<div id="child" style="padding: 30px; background: lightblue;">子元素</div>
</div>

<script>
// 父元素事件监听(冒泡阶段)
document.getElementById('parent').addEventListener('click', () => {
console.log('父元素被点击');
});

// 子元素事件监听(冒泡阶段)
document.getElementById('child').addEventListener('click', (e) => {
console.log('子元素被点击');
e.stopPropagation(); // 阻止事件冒泡到父元素
});
</script>
```

**效果**:点击子元素时,仅输出“子元素被点击”(父元素的事件不会触发)。

#### 2. 阻止事件默认行为(Default Action)

事件默认行为是指浏览器对特定事件的默认处理(如点击链接跳转、表单提交刷新页面、右键弹出菜单等)。阻止默认行为可自定义事件处理逻辑。

**常用方法**:

- 调用事件对象(`event`)的 `preventDefault()` 方法(通用且推荐)。
- 在事件处理函数中返回 `false`(仅在特定场景生效,如原生 `onclick` 或 jQuery 事件)。

**示例1(`preventDefault()`)**:

```html
<a href="https://example.com" id="link">点击链接</a>

<script>
document.getElementById('link').addEventListener('click', (e) => {
e.preventDefault(); // 阻止链接默认跳转行为
console.log('链接被点击,但不跳转');
});
</script>
```

**示例2(返回 `false`)**:

```html
<form id="form">
<button type="submit">提交</button>
</form>

<script>
// 原生onclick中返回false可阻止默认行为
document.getElementById('form').onsubmit = () => {
console.log('表单提交被阻止');
return false; // 阻止表单默认提交刷新
};
</script>
```

### 核心区别

- `stopPropagation()`:仅阻止事件在DOM树中的传播(捕获/冒泡),不影响事件本身的默认行为。
- `preventDefault()`:仅阻止浏览器对事件的默认处理,不影响事件的传播(仍会冒泡/捕获)。

若需同时阻止冒泡和默认行为,需同时调用两个方法。

## 16.JavaScript 中的事件委托(事件代理)是什么?有什么优势?如何实现?

### 什么是事件委托(事件代理)

事件委托(Event Delegation)是**利用事件冒泡机制**,将原本需要绑定在多个子元素上的事件处理逻辑,统一绑定到它们的父元素(或更外层祖先元素)上的技术。

其核心思想是:子元素触发事件后,事件会冒泡到父元素,父元素通过判断事件的“目标元素”(触发事件的子元素),来执行对应的处理逻辑,从而避免给每个子元素单独绑定事件。

### 事件委托的优势

1. **提升性能**
减少事件监听器的数量(从多个子元素变为一个父元素),降低内存占用和浏览器的事件处理开销,尤其适合子元素数量多的场景(如长列表)。

2. **支持动态元素**
新增的子元素无需重新绑定事件(因为事件绑定在父元素上),自动继承事件处理逻辑,避免了动态添加元素后手动绑定事件的麻烦。

3. **简化代码**
统一管理事件逻辑,减少重复代码,提高可维护性。

### 事件委托的实现方法

核心步骤:

1. 在父元素上绑定事件监听器;
2. 通过事件对象的 `target` 属性(触发事件的原始子元素)判断目标元素;
3. 根据目标元素的特征(如标签名、类名)执行对应逻辑。

#### 基础示例(列表项点击事件)

假设有一个 `<ul>` 包含多个 `<li>`,需要给每个 `<li>` 绑定点击事件,可通过事件委托实现:

```html
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>

<script>
// 1. 父元素(ul)绑定事件
document.getElementById('list').addEventListener('click', function(e) {
// 2. 通过e.target获取触发事件的子元素
const target = e.target;

// 3. 判断目标元素是否为li(避免触发父元素自身或其他子元素时误执行)
if (target.tagName === 'LI') {
console.log('点击了:', target.textContent);
}
});

// 动态添加新li(无需重新绑定事件,自动支持点击)
const newLi = document.createElement('li');
newLi.textContent = '新增项目';
document.getElementById('list').appendChild(newLi);
</script>
```

#### 处理嵌套元素的场景

若子元素内部有嵌套标签(如 `<li><span>项目1</span></li>`),点击 `<span>` 时 `e.target` 会指向 `<span>`,而非 `<li>`。此时可通过 `closest()` 方法向上查找最近的目标父元素:

```html
<ul id="list">
<li><span>项目1</span></li>
<li><span>项目2</span></li>
</ul>

<script>
document.getElementById('list').addEventListener('click', function(e) {
// 查找触发元素最近的li祖先(包含自身)
const li = e.target.closest('li');
if (li) { // 确保找到li
console.log('点击了:', li.textContent.trim());
}
});
</script>
```

### 总结

事件委托基于事件冒泡机制,通过父元素统一处理子元素事件,既能提升性能,又能兼容动态元素,是处理大量相似元素事件的最佳实践。实现的关键是准确识别目标元素,通常结合 `e.target` 和元素特征判断(如 `tagName`、`classList`、`closest()` 等)。

## 17.简述 JavaScript 中的异步编程模式,常见的异步方式有哪些(如回调函数、Promise、async/await)?

### 什么是 JavaScript 中的异步编程模式

JavaScript 是单线程语言(同一时间只能执行一段代码),而异步编程模式是**处理耗时操作(如网络请求、文件读写、定时器等)的机制**:让耗时操作在“后台”执行,不阻塞主线程,操作完成后再通过特定方式(如回调、通知)触发结果处理,从而保证程序的流畅运行。

### 常见的异步方式及特点

#### 1. 回调函数(Callbacks)

- **原理**:将一个函数作为参数传递给异步操作(如 `setTimeout`、`fs.readFile`),当异步操作完成后,自动调用该函数处理结果。
- **示例**:

```javascript
// 模拟异步请求
function fetchData(callback) {
setTimeout(() => {
const data = "异步数据";
callback(null, data); // 操作完成后调用回调(第一个参数通常用于错误)
}, 1000);
}

// 调用:传入回调函数处理结果
fetchData((err, data) => {
if (err) { console.error(err); return; }
console.log(data); // 1秒后输出 "异步数据"
});
```

- **优点**:简单直观,是最基础的异步实现方式。
- **缺点**:多层嵌套时会形成“回调地狱”(Callback Hell),代码可读性差、难以维护,且错误处理繁琐。

#### 2. Promise(ES6 新增)

- **原理**:用对象封装异步操作的结果(成功/失败),通过状态(`pending`→`fulfilled`/`rejected`)管理异步流程,支持链式调用。
- **核心方法**:
- `then()`:处理成功结果(`fulfilled` 状态),返回新的 Promise,可链式调用;
- `catch()`:处理错误(`rejected` 状态),捕获链中所有错误;
- `finally()`:无论成功/失败都会执行。
- **示例**:

```javascript
// 用 Promise 封装异步操作
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("异步数据"); // 成功:调用 resolve
} else {
reject(new Error("请求失败")); // 失败:调用 reject
}
}, 1000);
});
}

// 链式调用处理结果
fetchData()
.then(data => {
console.log(data); // 1秒后输出 "异步数据"
return data + "处理后";
})
.then(processedData => {
console.log(processedData); // 输出 "异步数据处理后"
})
.catch(err => {
console.error(err); // 捕获所有错误
});
```

- **优点**:解决回调地狱(链式调用替代嵌套),错误处理统一(`catch` 捕获整个链的错误)。
- **缺点**:仍需通过回调函数(`then`/`catch`)处理结果,复杂场景下链式调用可能过长。

#### 3. async/await(ES2017 新增)

- **原理**:基于 Promise 的语法糖,让异步代码写法更接近同步代码,本质是 `Generator` 函数的简化。
- **核心规则**:
- `async` 修饰的函数返回 Promise 对象;
- `await` 只能在 `async` 函数中使用,后面跟 Promise 对象,会暂停执行直到 Promise 完成(`fulfilled`/`rejected`);
- 错误处理通过 `try/catch` 实现。
- **示例**:

```javascript
// 复用上面的 fetchData(返回 Promise)
async function handleData() {
try {
const data = await fetchData(); // 暂停等待 Promise 完成
console.log(data); // 输出 "异步数据"
const processedData = data + "处理后";
console.log(processedData); // 输出 "异步数据处理后"
} catch (err) {
console.error(err); // 捕获错误
}
}

handleData(); // 执行异步逻辑
```

- **优点**:代码结构清晰(类似同步),可读性和可维护性最优,错误处理直观(`try/catch`)。
- **缺点**:依赖 Promise(`await` 后必须是 Promise),本质仍是异步,不能在顶层代码直接使用 `await`(需包裹在 `async` 函数中)。

### 总结

异步编程模式的演进:**回调函数→Promise→async/await**,逐步解决了前一种方式的缺陷(如嵌套、可读性)。实际开发中,`async/await` 是最推荐的方式(基于 Promise,写法更简洁),而 Promise 是异步编程的基础(`async/await` 依赖它)。

## 18.什么是 Promise?Promise 的三种状态(pending、fulfilled、rejected)及状态变化规则是什么?

### 什么是 Promise

Promise 是 ES6 引入的**异步编程解决方案**,用于封装一个异步操作的最终结果(成功或失败)。它通过对象的形式,将异步操作的“等待中”“已完成”“已失败”三种状态显式化,并提供统一的接口(如 `then`、`catch`)处理结果,避免了回调函数嵌套导致的“回调地狱”。

### Promise 的三种状态

Promise 有且仅有三种状态,状态由异步操作的结果决定:

1. **pending(等待中)**
- 初始状态:Promise 创建后,异步操作未完成时的状态。
- 特征:此时无法确定操作成功或失败,等待异步操作结果。

2. **fulfilled(已完成/成功)**
- 状态含义:异步操作成功完成时的状态。
- 特征:会携带异步操作的成功结果(如请求到的数据)。

3. **rejected(已失败)**
- 状态含义:异步操作失败时的状态。
- 特征:会携带失败原因(如错误对象 `Error`)。

### 状态变化规则

Promise 的状态变化具有**不可逆性**,且只能从初始状态向最终状态转换,具体规则如下:

1. **唯一转换方向**:
- 只能从 `pending` 转换为 `fulfilled`(成功),或从 `pending` 转换为 `rejected`(失败)。
- 一旦转换为 `fulfilled` 或 `rejected`,状态将永久固定,无法再变更(如 `fulfilled` 不能变回 `pending`,也不能转为 `rejected`)。

2. **状态触发方式**:
- 在 Promise 构造函数的回调函数中,通过调用 `resolve(value)` 触发 `pending → fulfilled` 转换(`value` 为成功结果)。
- 通过调用 `reject(reason)` 触发 `pending → rejected` 转换(`reason` 为失败原因,通常是 `Error` 对象)。

3. **示例说明**:

```javascript
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("操作成功"); // pending → fulfilled(状态固定)
reject("无效的失败"); // 无效:状态已变为fulfilled,无法再转换
} else {
reject(new Error("操作失败")); // pending → rejected(状态固定)
}
}, 1000);
});
```

简言之,Promise 的状态是“一次性”的:从等待中开始,最终只能成功或失败,且状态一旦确定就无法更改,这一特性保证了异步操作结果的可靠性。

## 19.Promise 的 then、catch、finally 方法的作用是什么?它们的返回值是什么?

### Promise 的 `then`、`catch`、`finally` 方法的作用及返回值

#### 1. `then` 方法

- **作用**:处理 Promise 的成功或失败结果,是 Promise 链式调用的核心方法。
可接受两个可选参数:
- 第一个参数(`onFulfilled`):当 Promise 状态为 `fulfilled` 时执行,接收成功结果(`resolve` 传递的值)。
- 第二个参数(`onRejected`):当 Promise 状态为 `rejected` 时执行,接收失败原因(`reject` 传递的错误)。
(实际开发中,第二个参数通常用 `catch` 替代,更直观。)

- **返回值**:**一个新的 Promise 对象**(这是链式调用的基础)。新 Promise 的状态由 `onFulfilled`/`onRejected` 的执行结果决定:
- 若回调函数返回**非 Promise 值**(如基本类型、普通对象),新 Promise 状态为 `fulfilled`,值为该返回值。
- 若回调函数返回**另一个 Promise**,新 Promise 的状态和值会“跟随”这个 Promise(同步其状态和结果)。
- 若回调函数**抛出错误**(`throw new Error(...)`),新 Promise 状态为 `rejected`,原因为抛出的错误。

- **示例**:

```javascript
Promise.resolve(10)
.then(
(value) => {
console.log("成功:", value); // 输出 "成功: 10"
return value * 2; // 返回非Promise值
},
(err) => console.error("失败:", err) // 此处不执行(原Promise为fulfilled)
)
.then((value) => {
console.log("链式调用:", value); // 输出 "链式调用: 20"(新Promise为fulfilled)
return Promise.reject(new Error("故意失败")); // 返回rejected状态的Promise
})
.then(
() => console.log("不会执行"),
(err) => console.error("捕获错误:", err.message) // 输出 "捕获错误: 故意失败"
);
```

#### 2. `catch` 方法

- **作用**:专门处理 Promise 链中的错误(`rejected` 状态),相当于 `then(null, onRejected)` 的语法糖。
会捕获链中所有之前未处理的错误(包括 `then` 回调中抛出的错误)。

- **返回值**:**一个新的 Promise 对象**。新 Promise 的状态规则:
- 若 `catch` 回调**正常执行且未抛出错误**,新 Promise 状态为 `fulfilled`,值为回调的返回值(若未返回则为 `undefined`)。
- 若 `catch` 回调**抛出错误**,新 Promise 状态为 `rejected`,原因为抛出的错误。

- **示例**:

```javascript
Promise.reject(new Error("原始错误"))
.catch((err) => {
console.error("捕获错误:", err.message); // 输出 "捕获错误: 原始错误"
return "错误已处理"; // 正常返回,新Promise为fulfilled
})
.then((value) => {
console.log("继续执行:", value); // 输出 "继续执行: 错误已处理"(因catch返回fulfilled)
throw new Error("新错误"); // 抛出新错误
})
.catch((err) => {
console.error("捕获新错误:", err.message); // 输出 "捕获新错误: 新错误"
});
```

#### 3. `finally` 方法

- **作用**:无论 Promise 最终状态是 `fulfilled` 还是 `rejected`,都会执行的回调(通常用于清理操作,如关闭加载动画、释放资源等)。
回调函数**不接收任何参数**(无法获取成功结果或失败原因),仅关注“操作结束”这一事件。

- **返回值**:**一个新的 Promise 对象**。新 Promise 的状态规则:
- 若 `finally` 回调**正常执行**,新 Promise 的状态和值与原 Promise 保持一致(继承原状态和结果)。
- 若 `finally` 回调**抛出错误**,新 Promise 状态为 `rejected`,原因为抛出的错误(覆盖原状态)。

- **示例**:

```javascript
Promise.resolve("操作成功")
.finally(() => {
console.log("操作结束(无论成功失败)"); // 必然执行
// 不影响原状态
})
.then((value) => console.log(value)); // 输出 "操作成功"(继承原fulfilled状态)

Promise.reject(new Error("操作失败"))
.finally(() => {
console.log("操作结束"); // 必然执行
// throw new Error("finally错误"); // 若抛出,新Promise状态变为rejected
})
.catch((err) => console.error(err.message)); // 输出 "操作失败"(继承原rejected状态)
```

### 总结

- `then`:处理成功/失败结果,返回新 Promise 支持链式调用;
- `catch`:专门捕获错误,返回新 Promise(错误处理后可继续链式调用);
- `finally`:无论成功失败都执行,返回新 Promise(默认继承原状态)。

三者的返回值均为新 Promise,这是实现“链式调用”的核心,使异步逻辑更清晰、易维护。

## 20.如何实现 Promise 的串行和并行执行?(如 Promise.all、Promise.race)

### Promise 的串行与并行执行实现方式

#### 一、并行执行(多个 Promise 同时执行,无需等待)

并行执行指多个异步操作同时启动,不需要等待前一个完成即可执行下一个,适用于**无依赖关系**的异步任务(如同时请求多个独立接口)。
JavaScript 提供了多个内置静态方法实现并行控制,核心是 `Promise.all` 和 `Promise.race`。

##### 1. `Promise.all(iterable)`

- **作用**:接收一个可迭代对象(如数组),包含多个 Promise,返回一个新 Promise。
- **规则**:
- 当**所有** Promise 都变为 `fulfilled` 时,新 Promise 变为 `fulfilled`,结果为所有 Promise 结果组成的**数组**(顺序与传入顺序一致)。
- 当**任意一个** Promise 变为 `rejected` 时,新 Promise 立即变为 `rejected`,结果为该失败原因(忽略其他未完成的 Promise)。
- **示例**(并行请求多个接口):

```javascript
// 模拟3个异步请求
const request1 = () => Promise.resolve("数据1");
const request2 = () => Promise.resolve("数据2");
const request3 = () => Promise.resolve("数据3");

// 并行执行
Promise.all([request1(), request2(), request3()])
.then(results => {
console.log("所有请求成功:", results); // 输出 ["数据1", "数据2", "数据3"]
})
.catch(err => {
console.error("有请求失败:", err); // 若任意请求失败,执行此处
});
```

##### 2. `Promise.race(iterable)`

- **作用**:接收一个可迭代对象,返回一个新 Promise,**只关注第一个完成的 Promise**。
- **规则**:
- 当**第一个** Promise 变为 `fulfilled` 时,新 Promise 立即变为 `fulfilled`,结果为该成功值。
- 当**第一个** Promise 变为 `rejected` 时,新 Promise 立即变为 `rejected`,结果为该失败原因。
- **示例**(设置请求超时):

```javascript
// 模拟一个可能超时的请求
const request = () => new Promise((resolve) => {
setTimeout(() => resolve("请求成功"), 2000); // 2秒后成功
});

// 超时控制器(1秒后失败)
const timeout = () => new Promise((_, reject) => {
setTimeout(() => reject(new Error("请求超时")), 1000);
});

// 谁先完成就用谁的结果
Promise.race([request(), timeout()])
.then(result => console.log(result)) // 不会执行(请求比超时慢)
.catch(err => console.error(err.message)); // 输出 "请求超时"(1秒后先触发)
```

#### 二、串行执行(多个 Promise 按顺序执行,前一个完成后再执行下一个)

串行执行指多个异步操作按顺序执行,**必须等待前一个完成后,再执行下一个**,适用于**有依赖关系**的任务(如后一个请求需要前一个的结果作为参数)。
JavaScript 没有内置串行方法,需手动实现,核心思路是用 `reduce` 链式调用 `then`。

##### 实现方式(基于 `reduce`)

利用数组的 `reduce` 方法,将多个异步函数(返回 Promise)按顺序串联:

- 初始值为 `Promise.resolve()`(一个已完成的 Promise,作为链式起点)。
- 每次迭代时,等待前一个 Promise 完成后,再执行当前异步函数,将结果传递给下一个。

**示例**(按顺序请求,后一个依赖前一个结果):

```javascript
// 模拟3个有依赖的异步函数(后一个需要前一个的结果)
const task1 = () => Promise.resolve(10); // 第一步:返回10
const task2 = (prev) => Promise.resolve(prev + 20); // 第二步:用第一步结果+20
const task3 = (prev) => Promise.resolve(prev * 2); // 第三步:用第二步结果×2

// 任务列表(按执行顺序排列)
const tasks = [task1, task2, task3];

// 串行执行:用reduce链式调用then
tasks.reduce((promiseChain, currentTask) => {
// 等待前一个Promise完成后,执行当前任务
return promiseChain.then(prevResult => currentTask(prevResult));
}, Promise.resolve()) // 初始值:已完成的Promise
.then(finalResult => {
console.log("最终结果:", finalResult); // 输出 (10+20)×2=60
});
```

### 总结

- **并行**:用 `Promise.all`(等待所有完成)或 `Promise.race`(等待第一个完成),适合无依赖任务,效率高。
- **串行**:用 `reduce` 链式调用 `then`,适合有依赖任务,按顺序执行。

根据任务间是否有依赖关系选择合适的执行方式,可大幅提升异步代码的效率和可读性。

## 21.async/await 是什么?它如何简化 Promise 的异步代码?使用时需要注意什么?

### 什么是 async/await

async/await 是 ES2017(ES8)引入的**异步编程语法糖**,建立在 Promise 之上,允许以**同步代码的形式编写异步逻辑**。它由两个关键字组成:

- `async`:修饰函数,表明该函数内部包含异步操作,函数执行后**自动返回一个 Promise 对象**(无论是否显式返回)。
- `await`:只能在 `async` 函数内部使用,用于“等待”一个 Promise 对象的状态变更(从 `pending` 到 `fulfilled` 或 `rejected`),并**获取其结果**。

### async/await 如何简化 Promise 的异步代码

Promise 通过 `then` 链式调用解决了回调地狱,但复杂场景下(如多步骤依赖、条件判断),链式调用仍会导致代码冗长、嵌套层次增加。而 async/await 用同步的语法结构表达异步逻辑,使代码更直观、易读。

#### 对比示例

假设需要完成三个依赖的异步操作:

1. 获取用户 ID;
2. 根据 ID 获取用户信息;
3. 根据用户信息获取详细数据。

**用 Promise 实现(链式调用):**

```javascript
// 模拟三个异步函数(返回 Promise)
function getUserId() { return Promise.resolve(1001); }
function getUserInfo(id) { return Promise.resolve({ id, name: "张三" }); }
function getDetail(info) { return Promise.resolve({ ...info, age: 18 }); }

// 链式调用处理依赖
getUserId()
.then(id => {
return getUserInfo(id); // 依赖第一个结果
})
.then(info => {
return getDetail(info); // 依赖第二个结果
})
.then(detail => {
console.log("最终结果:", detail); // { id: 1001, name: "张三", age: 18 }
})
.catch(err => {
console.error("错误:", err);
});
```

**用 async/await 实现(同步风格):**

```javascript
async function handleData() {
try {
const id = await getUserId(); // 等待第一个操作完成
const info = await getUserInfo(id); // 依赖第一个结果,等待完成
const detail = await getDetail(info); // 依赖第二个结果,等待完成
console.log("最终结果:", detail); // { id: 1001, name: "张三", age: 18 }
} catch (err) {
console.error("错误:", err); // 捕获所有步骤的错误
}
}

handleData();
```

**简化核心:**

- 用 `await` 替代 `then` 回调,避免了链式调用的嵌套结构,代码按执行顺序线性排列,更接近同步逻辑的思维习惯。
- 用 `try/catch` 统一处理所有异步操作的错误(替代 `catch` 方法),错误处理更直观。

### 使用 async/await 时的注意事项

#### 1. `await` 只能在 `async` 函数中使用

若在非 `async` 函数中使用 `await`,会直接报错(语法错误)。

```javascript
// 错误示例
function fn() {
await Promise.resolve(1); // SyntaxError: await is only valid in async functions
}

// 正确示例
async function fn() {
await Promise.resolve(1); // 合法
}
```

#### 2. 错误处理必须用 `try/catch`

`await` 后的 Promise 若变为 `rejected` 状态,会抛出异常(类似同步代码的错误),需用 `try/catch` 捕获,否则会导致未处理的Promise拒绝(Uncaught Promise Rejection)。

```javascript
async function fn() {
try {
await Promise.reject(new Error("失败")); // 抛出错误
} catch (err) {
console.error(err.message); // 正确捕获:"失败"
}
}
```

#### 3. `async` 函数返回值默认被包装为 Promise

无论 `async` 函数返回什么值(非 Promise 类型),都会被自动包装为 `fulfilled` 状态的 Promise,结果为返回值;若抛出错误,则包装为 `rejected` 状态的 Promise。

```javascript
async function fn() {
return "成功"; // 等价于 return Promise.resolve("成功")
}
fn().then(value => console.log(value)); // 输出 "成功"

async function errFn() {
throw new Error("出错"); // 等价于 return Promise.reject(...)
}
errFn().catch(err => console.error(err.message)); // 输出 "出错"
```

#### 4. 避免滥用 `await` 导致性能问题

若多个异步操作**无依赖关系**,不应逐个 `await`(会串行执行,浪费时间),而应使用 `Promise.all` 并行执行,再用 `await` 等待所有结果。

**反例(串行执行,效率低):**

```javascript
async function badCase() {
const res1 = await fetch("/api/data1"); // 耗时1s
const res2 = await fetch("/api/data2"); // 耗时1s(需等待前一个完成,总耗时2s)
}
```

**正例(并行执行,效率高):**

```javascript
async function goodCase() {
const promise1 = fetch("/api/data1"); // 立即启动
const promise2 = fetch("/api/data2"); // 立即启动(无需等待)
const [res1, res2] = await Promise.all([promise1, promise2]); // 总耗时≈1s
}
```

#### 5. `await` 后面的表达式不一定是 Promise,但不推荐

若 `await` 后不是 Promise(如普通值、非Promise对象),JavaScript 会自动将其包装为 `fulfilled` 状态的 Promise(值为该表达式结果),但这会导致不必要的性能开销,且不符合设计初衷。

```javascript
async function fn() {
const result = await 123; // 等价于 await Promise.resolve(123)
console.log(result); // 123(可以运行,但不推荐)
}
```

### 总结

async/await 是 Promise 的“语法糖”,通过同步风格的代码简化了异步逻辑的编写,解决了 Promise 链式调用的冗余问题。使用时需注意:`await` 必须在 `async` 函数中、用 `try/catch` 处理错误、避免无意义的串行执行,才能充分发挥其优势。

## 22.简述 JavaScript 中的定时器(setTimeout、setInterval)的工作原理,它们的精度受哪些因素影响?

### JavaScript 定时器(setTimeout、setInterval)的工作原理

JavaScript 是单线程语言,通过“事件循环(Event Loop)”机制处理异步任务,定时器的工作原理基于这一机制,核心流程如下:

#### 1. 基本机制

- **`setTimeout(fn, delay)`**:延迟 `delay` 毫秒后,将回调函数 `fn` 放入“宏任务队列”(等待执行的任务队列)。
- **`setInterval(fn, interval)`**:每隔 `interval` 毫秒,将回调函数 `fn` 放入“宏任务队列”(若前一次回调未执行,可能导致回调堆积)。

#### 2. 执行流程(结合事件循环)

1. 当调用定时器时,JavaScript 引擎会将回调函数和延迟时间交给**浏览器的定时器模块**(由浏览器内核线程管理,非 JS 主线程)。
2. 定时器模块在到达指定延迟时间后,将回调函数添加到“宏任务队列”(此时回调并未立即执行)。
3. JS 主线程优先执行**当前执行栈**中的同步代码,直到执行栈为空。
4. 主线程通过“事件循环”检查宏任务队列,若有等待的回调函数,将其取出并执行。

#### 关键特点

- 定时器的“延迟时间”是**最小延迟**,而非“精确延迟”——回调函数只能在主线程空闲时执行,若主线程被阻塞(如执行耗时同步代码),回调会被推迟。
- `setInterval` 可能导致回调“堆积”:若前一次回调执行时间超过 `interval`,下一次回调会在队列中等待,导致多个回调连续执行。

### 定时器精度的影响因素

定时器的实际执行时间与预期时间可能存在偏差,主要受以下因素影响:

#### 1. 主线程阻塞

JS 主线程若在处理同步代码(如复杂计算、长循环),会阻塞宏任务队列的执行。此时即使定时器到达延迟时间,回调也必须等待主线程空闲,导致延迟变大。
**示例**:

```javascript
// 主线程被阻塞 3 秒
setTimeout(() => {
console.log("定时器回调(预期 1 秒后执行)");
}, 1000);

// 同步代码阻塞主线程
let start = Date.now();
while (Date.now() - start < 3000) {} // 执行 3 秒
// 结果:定时器回调会在 3 秒后才执行(被同步代码阻塞)
```

#### 2. 最小延迟限制(HTML5 标准)

HTML5 标准规定,定时器的最小延迟时间为 **4ms**(嵌套层级较深时):

- 当定时器嵌套调用层级超过 5 层,浏览器会将最小延迟强制改为 4ms(防止恶意代码频繁触发定时器占用资源)。
- 即使设置 `delay` 为 0 或 1ms,实际延迟也可能不低于 4ms。

#### 3. 浏览器后台标签页节流

为节省资源,浏览器会对**后台标签页**(非当前激活标签)的定时器进行节流:

- 延迟时间可能被强制延长(如 Chrome 中后台标签的 `setInterval` 最小间隔为 1000ms)。
- 极端情况下,后台定时器可能被暂停,直到标签页重新激活。

#### 4. 系统时钟精度与浏览器性能

- 操作系统的时钟精度(如毫秒级精度限制)会影响定时器的计时准确性。
- 浏览器性能不足(如内存占用过高、CPU 负载大)时,定时器模块的计时和回调调度会被延迟。

#### 5. 宏任务队列中的其他任务

若宏任务队列中存在其他任务(如用户交互事件、网络请求回调),定时器回调需要排队等待,实际执行时间会被这些任务“插队”延迟。

### 总结

定时器的工作依赖于 JS 单线程的事件循环机制,其精度并非绝对可靠,主要受主线程阻塞、最小延迟限制、浏览器节流策略等因素影响。实际开发中,若需高精度计时(如动画),可优先使用 `requestAnimationFrame`(与浏览器刷新同步)或 `Web Worker`(避免主线程阻塞)。

## 23.如何清除定时器?setTimeout 和 setInterval 的清除方法有什么区别?

### 如何清除定时器

JavaScript 中清除定时器需通过定时器创建时返回的**唯一 ID**(数字类型),并调用对应的清除方法。具体步骤:

1. 调用 `setTimeout` 或 `setInterval` 时,将返回的 ID 保存到变量中;
2. 调用对应的清除方法(`clearTimeout` 或 `clearInterval`),传入该 ID 即可终止定时器。

### setTimeout 和 setInterval 的清除方法及区别

#### 1. setTimeout 的清除:`clearTimeout(timerId)`

- **`setTimeout` 特性**:延迟指定时间后**执行一次回调函数**,执行后自动失效(无需手动清除)。
- **清除作用**:若回调函数**尚未执行**,`clearTimeout` 会阻止其执行;若回调已执行,清除操作无效(因定时器已完成使命)。

**示例**:

```javascript
// 创建定时器,保存返回的ID
const timeoutId = setTimeout(() => {
console.log("这是setTimeout的回调");
}, 1000);

// 清除定时器(若在1秒内调用,会阻止回调执行)
clearTimeout(timeoutId);
```

#### 2. setInterval 的清除:`clearInterval(timerId)`

- **`setInterval` 特性**:每隔指定时间**重复执行回调函数**,若不手动清除,会无限执行。
- **清除作用**:终止后续的重复执行(无论当前是否有回调在执行),彻底停止定时器。

**示例**:

```javascript
// 创建定时器,保存返回的ID
const intervalId = setInterval(() => {
console.log("这是setInterval的回调(每1秒执行一次)");
}, 1000);

// 5秒后清除定时器(停止重复执行)
setTimeout(() => {
clearInterval(intervalId);
console.log("定时器已停止");
}, 5000);
```

#### 核心区别

| 维度 | `setTimeout` + `clearTimeout` | `setInterval` + `clearInterval` |
|---------------------|-------------------------------------------------------|---------------------------------------------------------|
| **清除目标** | 阻止**单次未执行**的回调(已执行则无效)。 | 终止**后续所有重复执行**的回调(无论是否执行过)。 |
| **必要性** | 非必需(回调执行后自动失效,仅在需要取消未执行的回调时使用)。 | 必需(若不清除,会无限重复执行,导致性能问题)。 |
| **清除后状态** | 回调不再执行,定时器生命周期结束。 | 所有后续回调均不再执行,定时器生命周期结束。 |

### 注意事项

- 每个定时器的 ID 是唯一的,清除时需确保传入正确的 ID(若 ID 错误,清除操作无效)。
- 建议在组件卸载、页面关闭等场景主动清除定时器(尤其是 `setInterval`),避免内存泄漏或无效代码执行。
- 若多次调用 `setTimeout`/`setInterval`,会生成多个不同的 ID,需分别清除。

简言之,`clearTimeout` 用于取消单次延迟执行,`clearInterval` 用于终止重复执行,两者需与对应的定时器创建方法配合使用,且依赖正确的 ID 实现清除。

## 24.什么是宏任务(Macro Task)和微任务(Micro Task)?它们的执行顺序是什么?

### 什么是宏任务(Macro Task)和微任务(Micro Task)

宏任务和微任务是 JavaScript 异步任务的两种分类,用于区分不同优先级的异步操作,是事件循环(Event Loop)机制的核心概念。

#### 1. 宏任务(Macro Task)

指**耗时较长、优先级较低**的异步任务,通常由浏览器或 Node.js 环境发起,包含:

- 整体 script 代码(首次执行的同步代码属于第一个宏任务);
- `setTimeout`、`setInterval`、`setImmediate`(Node 环境);
- I/O 操作(如文件读写、网络请求);
- UI 渲染(浏览器环境)。

#### 2. 微任务(Micro Task)

指**耗时较短、优先级较高**的异步任务,通常由 JavaScript 引擎自身发起,包含:

- Promise 的 `then`、`catch`、`finally` 回调;
- `async/await`(本质是 Promise 的语法糖,await 后的代码属于微任务);
- `process.nextTick`(Node 环境,优先级高于其他微任务);
- `MutationObserver`(浏览器环境,监听 DOM 变化的异步回调)。

### 宏任务与微任务的执行顺序

JavaScript 通过“事件循环”机制处理异步任务,执行顺序遵循以下规则:

1. **先执行同步代码**:主线程优先执行当前执行栈中的同步代码,直到执行栈为空。
2. **处理微任务队列**:同步代码执行完后,立即清空**所有微任务**(按队列顺序执行)。
3. **执行一个宏任务**:微任务队列清空后,从宏任务队列中取出**一个**宏任务执行。
4. **重复循环**:执行完该宏任务后,再次清空所有微任务,然后取下一个宏任务,以此类推(事件循环的核心)。

### 示例说明

以下代码可清晰展示执行顺序:

```javascript
// 同步代码(属于第一个宏任务的一部分)
console.log("1. 同步代码开始");

// 宏任务:setTimeout
setTimeout(() => {
console.log("5. 宏任务 setTimeout 执行");
// 宏任务内部的微任务
Promise.resolve().then(() => {
console.log("6. 宏任务内的微任务执行");
});
}, 0);

// 微任务:Promise.then
Promise.resolve().then(() => {
console.log("3. 微任务 Promise.then 执行");
// 微任务内部的宏任务
setTimeout(() => {
console.log("7. 微任务内的宏任务执行");
}, 0);
});

// 同步代码(继续执行)
console.log("2. 同步代码结束");

// 微任务:另一个 Promise.then
Promise.resolve().then(() => {
console.log("4. 第二个微任务执行");
});
```

**执行结果顺序**:

  1. 同步代码开始
  2. 同步代码结束
  3. 微任务 Promise.then 执行
  4. 第二个微任务执行
  5. 宏任务 setTimeout 执行
  6. 宏任务内的微任务执行
  7. 微任务内的宏任务执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91

    **解析**:

    1. 先执行所有同步代码(输出 1、2),执行栈清空。
    2. 处理微任务队列:按顺序执行两个 `Promise.then`(输出 3、4),微任务队列清空。
    3. 执行第一个宏任务(`setTimeout` 回调,输出 5),执行后再次处理其内部的微任务(输出 6)。
    4. 微任务清空后,执行下一个宏任务(微任务内创建的 `setTimeout`,输出 7)。

    ### 核心结论

    - **优先级**:同步代码 > 微任务 > 宏任务。
    - **执行逻辑**:每次执行完一个宏任务后,必须先清空所有微任务,再执行下一个宏任务,形成“宏任务→微任务→宏任务→微任务…”的循环。

    这一机制确保了高优先级的异步操作(如 Promise 回调)能更快响应,提升了代码执行的效率和用户体验。
    在浏览器环境下,宏任务(Macro Task)和微任务(Micro Task)的执行顺序由**事件循环(Event Loop)** 机制严格控制,核心原则是“**先微后宏,一次一宏**”,具体流程如下:

    ### 浏览器环境下的宏任务与微任务类型

    首先明确两类任务的具体包含项(浏览器环境特有):

    - **宏任务**:
    - 整个 `script` 标签的代码(初始执行的同步代码,属于第一个宏任务);
    - `setTimeout`、`setInterval` 的回调;
    - 浏览器的 I/O 操作(如 `fetch` 请求的回调);
    - UI 渲染(浏览器的页面绘制操作,属于特殊宏任务);
    - `requestAnimationFrame`(与UI渲染同步的动画回调)。

    - **微任务**:
    - Promise 的 `then`、`catch`、`finally` 回调;
    - `async/await` 中 `await` 后的代码(本质是 Promise 回调的语法糖);
    - `queueMicrotask()` 注册的回调(专门用于创建微任务);
    - `MutationObserver` 的回调(监听 DOM 变化的异步回调)。

    ### 执行顺序的详细流程

    1. **执行同步代码(第一个宏任务)**:
    浏览器首次加载脚本时,将整个 `script` 代码视为第一个宏任务,优先执行其中的同步代码(执行栈中的代码),直到执行栈为空。

    2. **清空所有微任务**:
    同步代码执行完毕后,立即处理**微任务队列中所有的微任务**(按添加顺序依次执行),直到微任务队列为空。
    - 若微任务执行过程中产生新的微任务(如在 `then` 中又创建了 `Promise.then`),会继续加入微任务队列并**在本轮一并执行**(保证“清空”)。

    3. **执行UI渲染(可选)**:
    微任务队列清空后,浏览器会判断是否需要进行UI渲染(如DOM有更新),若需要则执行一次UI渲染(这一步是浏览器的内部操作,属于宏任务的一部分)。

    4. **执行下一个宏任务**:
    从宏任务队列中取出**一个**宏任务(如 `setTimeout` 回调、`fetch` 响应回调),执行其内部的同步代码。

    5. **重复循环**:
    执行完当前宏任务后,再次回到步骤2(清空所有微任务)→ 步骤3(UI渲染)→ 步骤4(下一个宏任务),形成事件循环。

    ### 示例验证(浏览器环境)

    以下代码可直观展示执行顺序:

    ```javascript
    // 同步代码(属于第一个宏任务)
    console.log("1. 同步代码开始");

    // 宏任务:setTimeout
    setTimeout(() => {
    console.log("5. 宏任务 setTimeout 执行");
    // 宏任务内部的微任务
    Promise.resolve().then(() => {
    console.log("6. 宏任务内的微任务执行");
    });
    }, 0);

    // 微任务:Promise.then
    Promise.resolve().then(() => {
    console.log("3. 微任务 Promise.then 执行");
    // 微任务中创建新的微任务
    queueMicrotask(() => {
    console.log("4. 微任务中新增的微任务");
    });
    });

    // 同步代码(继续执行)
    console.log("2. 同步代码结束");

    // 微任务:MutationObserver(浏览器特有)
    const observer = new MutationObserver(() => {
    console.log("7. 微任务 MutationObserver 执行");
    });
    observer.observe(document.body, { childList: true });
    // 触发DOM变化(同步操作,会让MutationObserver回调加入微任务队列)
    document.body.appendChild(document.createElement("div"));
    ```

    **执行结果(浏览器环境)**:

  8. 同步代码开始
  9. 同步代码结束
  10. 微任务 Promise.then 执行
  11. 微任务中新增的微任务
  12. 微任务 MutationObserver 执行
    // (UI渲染步骤:此时浏览器可能更新DOM,但无输出)
  13. 宏任务 setTimeout 执行
  14. 宏任务内的微任务执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    724
    725
    726
    727
    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    764
    765
    766
    767
    768
    769
    770
    771
    772
    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    783
    784
    785
    786
    787
    788
    789
    790
    791
    792
    793
    794
    795
    796
    797
    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    808
    809
    810
    811
    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    822
    823
    824
    825
    826
    827
    828
    829
    830
    831
    832
    833
    834
    835
    836
    837
    838
    839
    840
    841
    842
    843
    844
    845
    846
    847
    848
    849
    850
    851
    852
    853
    854
    855
    856
    857
    858
    859
    860
    861
    862
    863
    864
    865
    866
    867
    868
    869
    870
    871
    872
    873
    874
    875
    876
    877
    878
    879
    880
    881
    882
    883
    884
    885
    886
    887
    888
    889
    890
    891
    892
    893
    894
    895
    896
    897
    898
    899
    900
    901
    902
    903
    904
    905
    906
    907
    908
    909
    910
    911
    912
    913
    914
    915
    916
    917
    918
    919
    920
    921
    922
    923
    924
    925
    926
    927
    928
    929
    930
    931
    932
    933
    934
    935
    936
    937
    938
    939
    940
    941
    942
    943
    944
    945
    946
    947
    948
    949
    950
    951
    952
    953
    954
    955
    956
    957
    958
    959
    960
    961
    962
    963
    964
    965
    966
    967
    968
    969
    970
    971
    972
    973
    974
    975
    976
    977
    978
    979
    980
    981
    982
    983
    984
    985
    986
    987
    988
    989
    990
    991
    992
    993
    994
    995
    996
    997
    998
    999
    1000
    1001
    1002
    1003
    1004
    1005
    1006
    1007
    1008
    1009
    1010
    1011
    1012
    1013
    1014
    1015
    1016
    1017
    1018
    1019
    1020
    1021
    1022
    1023
    1024
    1025
    1026
    1027
    1028
    1029
    1030
    1031
    1032
    1033
    1034
    1035
    1036
    1037
    1038
    1039
    1040
    1041
    1042
    1043
    1044
    1045
    1046
    1047
    1048
    1049
    1050
    1051
    1052
    1053
    1054
    1055
    1056
    1057
    1058
    1059
    1060
    1061
    1062
    1063
    1064
    1065
    1066
    1067
    1068
    1069
    1070
    1071
    1072
    1073
    1074
    1075
    1076
    1077
    1078
    1079
    1080
    1081
    1082
    1083
    1084
    1085
    1086
    1087
    1088
    1089
    1090
    1091
    1092
    1093
    1094
    1095
    1096
    1097
    1098
    1099
    1100
    1101
    1102
    1103
    1104
    1105
    1106
    1107
    1108
    1109
    1110
    1111
    1112
    1113
    1114
    1115
    1116
    1117
    1118
    1119
    1120
    1121
    1122
    1123
    1124
    1125
    1126
    1127
    1128
    1129
    1130
    1131
    1132
    1133
    1134
    1135
    1136
    1137
    1138
    1139
    1140
    1141
    1142
    1143
    1144
    1145
    1146
    1147
    1148
    1149
    1150
    1151
    1152
    1153
    1154
    1155
    1156
    1157
    1158
    1159
    1160
    1161
    1162
    1163
    1164
    1165
    1166
    1167
    1168
    1169
    1170
    1171
    1172
    1173
    1174
    1175
    1176
    1177
    1178
    1179
    1180
    1181
    1182
    1183
    1184
    1185
    1186
    1187
    1188
    1189
    1190
    1191
    1192
    1193
    1194
    1195
    1196
    1197
    1198
    1199
    1200
    1201
    1202
    1203
    1204
    1205
    1206
    1207
    1208
    1209
    1210
    1211
    1212
    1213
    1214
    1215
    1216
    1217
    1218
    1219
    1220
    1221
    1222
    1223
    1224
    1225
    1226
    1227
    1228
    1229
    1230
    1231
    1232
    1233
    1234
    1235
    1236
    1237
    1238
    1239
    1240
    1241
    1242
    1243
    1244
    1245
    1246
    1247
    1248
    1249
    1250
    1251
    1252
    1253
    1254
    1255
    1256
    1257
    1258
    1259
    1260
    1261
    1262
    1263
    1264
    1265
    1266
    1267
    1268
    1269
    1270
    1271
    1272
    1273
    1274
    1275
    1276

    ### 核心结论

    - **优先级**:同步代码 > 微任务 > UI渲染 > 宏任务(下一个)。
    - **关键规则**:每次执行完**一个宏任务**后,必须先清空**所有微任务**(包括执行中新增的),再处理UI渲染,最后才执行下一个宏任务。

    这一机制确保了高优先级的异步操作(如Promise回调)能更快响应,避免了UI渲染被频繁打断,平衡了性能与用户体验。

    ## 25.常见的宏任务和微任务有哪些?(如 script、setTimeout、Promise.then)

    在 JavaScript 中,宏任务(Macro Task)和微任务(Micro Task)是异步任务的两大分类,其具体类型与运行环境(主要是浏览器)密切相关。以下是**浏览器环境下最常见的宏任务和微任务**,并结合实例说明:

    ### 一、宏任务(Macro Task)

    宏任务是优先级较低的异步任务,执行完一个宏任务后,会先清空所有微任务,再执行下一个宏任务。常见类型包括:

    1. **`script` 整体代码**
    - 页面加载时,整个脚本代码会被视为“第一个宏任务”,其中的同步代码会优先执行。

    2. **`setTimeout` 和 `setInterval` 回调**
    - 定时器触发的回调函数(如 `setTimeout(fn, 1000)` 中的 `fn`)。

    3. **网络请求回调**
    - 如 `fetch`、`XMLHttpRequest` 等请求完成后的回调(`onload`、`onreadystatechange` 等)。

    4. **UI 渲染**
    - 浏览器对 DOM 更新的绘制操作(属于浏览器内部宏任务,执行时机在微任务之后)。

    5. **`requestAnimationFrame` 回调**
    - 与浏览器渲染帧同步的动画回调(每帧执行一次,优先级高于 `setTimeout`)。

    ### 二、微任务(Micro Task)

    微任务是优先级较高的异步任务,会在当前宏任务的同步代码执行完毕后**立即全部执行**(包括执行中新增的微任务)。常见类型包括:

    1. **Promise 的 `then`/`catch`/`finally` 回调**
    - 如 `Promise.resolve().then(fn)` 中的 `fn`,在 Promise 状态变为 `fulfilled` 后触发。

    2. **`async/await` 中 `await` 后的代码**
    - `await` 会暂停函数执行,其后续代码会被包装为微任务(本质是 Promise 回调的语法糖)。

    3. **`queueMicrotask(fn)`**
    - 专门用于创建微任务的 API,等价于 `Promise.resolve().then(fn)`。

    4. **`MutationObserver` 回调**
    - 监听 DOM 变化的异步回调(当 DOM 被修改时触发,用于高效处理 DOM 更新)。

    ### 总结(浏览器环境核心类型)

    | 类型 | 常见任务举例 |
    |----------|-------------------------------------|
    | 宏任务 | `script` 代码、`setTimeout`、`setInterval`、网络请求回调、UI 渲染、`requestAnimationFrame` |
    | 微任务 | Promise 回调(`then`/`catch`/`finally`)、`async/await` 后续代码、`queueMicrotask`、`MutationObserver` |

    记忆要点:

    - 宏任务多与“环境级操作”相关(如定时器、网络请求、页面渲染);
    - 微任务多与“JS 引擎级操作”相关(如 Promise 处理、DOM 监听);
    - 微任务优先级高于宏任务,同一轮事件循环中,微任务会被“一次性清空”后再执行下一个宏任务。

    ## 26.简述 JavaScript 中的数组的常用方法(如 push、pop、shift、unshift、slice、splice、map、filter)

    JavaScript 数组提供了丰富的内置方法,用于操作和处理数组元素。以下是常用方法的分类及核心特点(按功能划分):

    ### 一、修改原数组的方法(添加/删除元素)

    这类方法会直接改变原数组,返回值多为操作结果(如长度、被删除元素)。

    1. **`push(...items)`**
    - 功能:向数组**末尾添加一个或多个元素**。
    - 参数:要添加的元素(可多个)。
    - 返回值:添加后数组的**新长度**。
    - 示例:

    ```javascript
    const arr = [1, 2];
    const len = arr.push(3, 4);
    console.log(arr); // [1, 2, 3, 4](原数组改变)
    console.log(len); // 4
    ```

    2. **`pop()`**
    - 功能:删除数组的**最后一个元素**。
    - 参数:无。
    - 返回值:被删除的元素(若数组为空,返回 `undefined`)。
    - 示例:

    ```javascript
    const arr = [1, 2, 3];
    const last = arr.pop();
    console.log(arr); // [1, 2](原数组改变)
    console.log(last); // 3
    ```

    3. **`unshift(...items)`**
    - 功能:向数组**开头添加一个或多个元素**。
    - 参数:要添加的元素(可多个)。
    - 返回值:添加后数组的**新长度**。
    - 示例:

    ```javascript
    const arr = [3, 4];
    const len = arr.unshift(1, 2);
    console.log(arr); // [1, 2, 3, 4](原数组改变)
    console.log(len); // 4
    ```

    4. **`shift()`**
    - 功能:删除数组的**第一个元素**。
    - 参数:无。
    - 返回值:被删除的元素(若数组为空,返回 `undefined`)。
    - 示例:

    ```javascript
    const arr = [1, 2, 3];
    const first = arr.shift();
    console.log(arr); // [2, 3](原数组改变)
    console.log(first); // 1
    ```

    5. **`splice(start, deleteCount, ...items)`**
    - 功能:**删除、添加或替换**数组元素(最灵活的修改方法)。
    - 参数:
    - `start`:起始索引(从该位置开始操作);
    - `deleteCount`:要删除的元素数量(0 则不删除);
    - `...items`:要添加的元素(可选,在 `start` 位置插入)。
    - 返回值:被删除元素组成的**新数组**(若未删除则为空数组)。
    - 示例:

    ```javascript
    const arr = [1, 2, 3, 4];
    // 从索引1开始,删除2个元素,添加5、6
    const deleted = arr.splice(1, 2, 5, 6);
    console.log(arr); // [1, 5, 6, 4](原数组改变)
    console.log(deleted); // [2, 3](被删除的元素)
    ```

    ### 二、不修改原数组的方法(查询/截取)

    这类方法返回新数组或值,原数组保持不变。

    1. **`slice(start, end)`**
    - 功能:截取数组的**一部分**(从 `start` 到 `end`,不包含 `end`)。
    - 参数:
    - `start`:起始索引(默认 0);
    - `end`:结束索引(默认数组长度,省略则截取到末尾)。
    - 返回值:截取的元素组成的**新数组**。
    - 示例:

    ```javascript
    const arr = [1, 2, 3, 4];
    const sub = arr.slice(1, 3); // 从索引1到2(不包含3)
    console.log(arr); // [1, 2, 3, 4](原数组不变)
    console.log(sub); // [2, 3]
    ```

    ### 三、迭代方法(遍历并处理元素)

    这类方法通过回调函数遍历数组,返回新数组或值,**不修改原数组**。

    1. **`map(callback)`**
    - 功能:对数组中的**每个元素执行回调函数**,返回由回调结果组成的新数组。
    - 回调参数:`currentValue`(当前元素)、`index`(索引)、`array`(原数组)。
    - 返回值:新数组(长度与原数组相同,元素为回调返回值)。
    - 示例:

    ```javascript
    const arr = [1, 2, 3];
    // 每个元素乘以2
    const doubled = arr.map(num => num * 2);
    console.log(arr); // [1, 2, 3](原数组不变)
    console.log(doubled); // [2, 4, 6]
    ```

    2. **`filter(callback)`**
    - 功能:筛选出数组中**符合回调函数条件**的元素,组成新数组。
    - 回调参数:同 `map`,需返回布尔值(`true` 则保留元素)。
    - 返回值:新数组(包含所有符合条件的元素,长度可能小于原数组)。
    - 示例:

    ```javascript
    const arr = [1, 2, 3, 4, 5];
    // 筛选偶数
    const evens = arr.filter(num => num % 2 === 0);
    console.log(arr); // [1, 2, 3, 4, 5](原数组不变)
    console.log(evens); // [2, 4]
    ```

    ### 总结

    - **修改原数组**:`push`、`pop`、`unshift`、`shift`、`splice`(适合直接修改数组结构)。
    - **不修改原数组**:`slice`(截取)、`map`(转换)、`filter`(筛选)(适合查询或生成新数组)。

    掌握这些方法能高效处理数组操作,减少重复代码。

    ## 27.数组的 map、filter、reduce 方法的作用是什么?它们的返回值是什么?举例说明

    `map`、`filter`、`reduce` 是 JavaScript 数组中常用的**迭代方法**,均用于处理数组元素且**不修改原数组**,但各自的功能和返回值不同。以下分别说明:

    ### 1. `map` 方法

    - **作用**:对数组中的**每个元素执行相同的处理逻辑**(通过回调函数),将处理结果组成新数组返回。
    - **核心逻辑**:“一一映射”——原数组有多少元素,新数组就有多少元素,每个元素是回调函数对原元素的处理结果。
    - **返回值**:一个**新数组**(长度与原数组相同,元素为回调函数的返回值)。
    - **回调函数参数**:`currentValue`(当前元素)、`index`(当前索引,可选)、`array`(原数组,可选)。

    **示例**:将数组中的数字乘以 2,将字符串转为大写

    ```javascript
    // 数字处理
    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]

    // 字符串处理
    const words = ["apple", "banana"];
    const uppercased = words.map(word => word.toUpperCase());
    console.log(uppercased); // ["APPLE", "BANANA"]
    ```

    ### 2. `filter` 方法

    - **作用**:根据回调函数的条件**筛选数组元素**,保留符合条件(回调返回 `true`)的元素,组成新数组返回。
    - **核心逻辑**:“筛选过滤”——新数组仅包含原数组中满足条件的元素,长度可能小于原数组。
    - **返回值**:一个**新数组**(包含所有符合条件的元素,若没有符合条件的元素则返回空数组)。
    - **回调函数参数**:同 `map`,需返回布尔值(`true` 保留元素,`false` 排除元素)。

    **示例**:筛选偶数、筛选成年用户

    ```javascript
    // 筛选偶数
    const numbers = [1, 2, 3, 4, 5, 6];
    const evens = numbers.filter(num => num % 2 === 0);
    console.log(evens); // [2, 4, 6](仅保留偶数)

    // 筛选成年用户
    const users = [
    { name: "张三", age: 17 },
    { name: "李四", age: 20 },
    { name: "王五", age: 25 }
    ];
    const adults = users.filter(user => user.age >= 18);
    console.log(adults); // [{name: "李四", age: 20}, {name: "王五", age: 25}]
    ```

    ### 3. `reduce` 方法

    - **作用**:对数组元素进行**累积处理**(如求和、求积、合并数据等),将数组“缩减”为一个值(或对象、数组等)。
    - **核心逻辑**:“累积聚合”——通过回调函数维护一个“累加器”,依次处理每个元素并更新累加器,最终返回累加器的最终值。
    - **返回值**:累积处理的**最终结果**(类型由处理逻辑决定,如数字、对象、数组等)。
    - **参数**:
    - 第一个参数:回调函数,接收 `accumulator`(累加器,上一次回调的返回值)、`currentValue`(当前元素)、`index`(当前索引,可选)、`array`(原数组,可选)。
    - 第二个参数(可选):`initialValue`(累加器的初始值,若不提供,则以数组第一个元素作为初始累加器,从第二个元素开始遍历)。

    **示例 1:数组求和(基础累积)**

    ```javascript
    const numbers = [1, 2, 3, 4];
    // 有初始值(0):从0开始累加每个元素
    const sum = numbers.reduce((acc, num) => acc + num, 0);
    console.log(sum); // 10(0 + 1 + 2 + 3 + 4)
    ```

    **示例 2:统计元素出现次数(聚合为对象)**

    ```javascript
    const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];
    // 初始值为 {},累加器是统计对象
    const count = fruits.reduce((acc, fruit) => {
    // 若当前水果已在对象中,计数+1;否则初始化为1
    acc[fruit] = (acc[fruit] || 0) + 1;
    return acc; // 返回更新后的累加器
    }, {});

    console.log(count); // { apple: 3, banana: 2, orange: 1 }
    ```

    **示例 3:将数组转为对象(键值映射)**

    ```javascript
    const users = [
    { id: 1, name: "张三" },
    { id: 2, name: "李四" }
    ];
    // 初始值为 {},将数组转为 { id: name } 的结构
    const userMap = users.reduce((acc, user) => {
    acc[user.id] = user.name;
    return acc;
    }, {});

    console.log(userMap); // { 1: "张三", 2: "李四" }
    ```

    ### 总结

    | 方法 | 核心作用 | 返回值 | 典型场景 |
    |---------|-------------------------|-------------------------|------------------------------|
    | `map` | 元素转换(一一映射) | 新数组(长度与原数组相同) | 格式转换(如数字处理、字段提取) |
    | `filter`| 元素筛选(保留符合条件)| 新数组(含符合条件的元素) | 数据过滤(如筛选特定用户、状态) |
    | `reduce`| 元素累积(聚合为单一结果)| 累积处理的最终值(任意类型)| 求和、统计、数据结构转换 |

    三者均为函数式编程的重要工具,通过组合使用可高效处理复杂数据逻辑(如 `filter` 筛选后用 `map` 转换,再用 `reduce` 聚合)。

    ## 28.如何实现数组的去重?请列举至少 3 种方法

    数组去重是开发中常见的需求,以下是 5 种常用的实现方法,各有其适用场景:

    ### 方法 1:利用 ES6 的 `Set`(最简洁)

    `Set` 是 ES6 新增的数据结构,其特性是**成员唯一(不允许重复)**,结合数组的 `from` 方法或扩展运算符 `...` 可快速去重。

    **代码示例**:

    ```javascript
    function unique(arr) {
    // Set 接收数组作为参数,自动去重;再转为数组
    return [...new Set(arr)];
    // 或 return Array.from(new Set(arr));
    }

    // 测试
    const arr = [1, 2, 2, 3, 3, 3, null, null, undefined, undefined];
    console.log(unique(arr)); // [1, 2, 3, null, undefined]
    ```

    **优点**:简洁高效,一行代码实现。
    **缺点**:不兼容 ES6 之前的环境(如 IE11 及以下),且无法处理对象类型(对象引用不同时会被视为不同元素)。

    ### 方法 2:利用对象的 `key` 唯一特性

    对象的属性名(`key`)具有唯一性,可通过判断元素是否已作为对象的 `key` 存在来实现去重。

    **代码示例**:

    ```javascript
    function unique(arr) {
    const obj = {};
    const result = [];
    for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    // 用 JSON.stringify 处理复杂类型(避免 1 和 '1' 被视为相同)
    const key = typeof item + JSON.stringify(item);
    if (!obj[key]) {
    obj[key] = true; // 标记为已存在
    result.push(item); // 加入结果数组
    }
    }
    return result;
    }

    // 测试
    const arr = [1, '1', 1, { a: 1 }, { a: 1 }, null, null];
    console.log(unique(arr)); // [1, '1', {a:1}, null](对象因引用不同被保留)
    ```

    **优点**:可处理基本类型和复杂类型(通过 `JSON.stringify` 区分),兼容性好。
    **缺点**:实现相对繁琐,`JSON.stringify` 对函数、`Symbol` 等类型处理有限制。

    ### 方法 3:利用数组的 `indexOf` 或 `includes`

    遍历数组,通过 `indexOf` 或 `includes` 判断元素是否已在新数组中存在,仅保留首次出现的元素。

    **代码示例(`indexOf`)**:

    ```javascript
    function unique(arr) {
    const result = [];
    for (let i = 0; i < arr.length; i++) {
    // indexOf 返回元素在 result 中首次出现的索引,-1 表示不存在
    if (result.indexOf(arr[i]) === -1) {
    result.push(arr[i]);
    }
    }
    return result;
    }

    // 测试
    const arr = [2, 1, 2, 3, 1];
    console.log(unique(arr)); // [2, 1, 3]
    ```

    **优点**:逻辑直观,兼容性好(支持 IE9+)。
    **缺点**:`indexOf` 每次都会遍历数组,时间复杂度为 O(n²),大数据量下效率较低。

    ### 方法 4:利用 `filter` + `indexOf`

    `filter` 方法筛选出**首次出现**的元素(当前元素的索引等于它在原数组中首次出现的索引)。

    **代码示例**:

    ```javascript
    function unique(arr) {
    return arr.filter((item, index) => {
    // 仅保留 index 等于首次出现位置的元素
    return arr.indexOf(item) === index;
    });
    }

    // 测试
    const arr = ['a', 'b', 'a', 'c', 'b'];
    console.log(unique(arr)); // ['a', 'b', 'c']
    ```

    **优点**:代码简洁,利用数组方法链式调用。
    **缺点**:同方法 3,`indexOf` 导致效率较低,且无法保留最后一次出现的元素。

    ### 方法 5:排序后去重(相邻元素对比)

    先对数组排序,重复元素会相邻,再遍历数组,仅保留与前一个元素不同的元素。

    **代码示例**:

    ```javascript
    function unique(arr) {
    if (arr.length <= 1) return arr;
    // 先排序(数字/字符串均可排序)
    const sortedArr = [...arr].sort((a, b) => a - b || a.localeCompare(b));
    const result = [sortedArr[0]];
    for (let i = 1; i < sortedArr.length; i++) {
    // 仅保留与前一个元素不同的元素
    if (sortedArr[i] !== sortedArr[i - 1]) {
    result.push(sortedArr[i]);
    }
    }
    return result;
    }

    // 测试
    const arr = [3, 1, 3, 2, 1];
    console.log(unique(arr)); // [1, 2, 3](排序后去重)
    ```

    **优点**:排序后去重逻辑简单,适合需要同时排序的场景。
    **缺点**:会改变原数组的顺序,且对复杂类型(如对象)排序可能不符合预期。

    ### 总结

    - 优先推荐 **`Set` 方法**(简洁高效,适用于现代环境)。
    - 需兼容旧环境或处理复杂类型,可选 **对象 `key` 方法**。
    - 需保留原数组顺序且数据量小时,可选 **`indexOf` 或 `filter` 方法**。

    根据实际场景(兼容性、数据类型、是否需保留顺序)选择合适的方法即可。

    ## 29.如何实现数组的扁平化(将多维数组转为一维数组)?请列举至少 2 种方法

    数组去重是开发中常见的需求,以下是 5 种常用的实现方法,各有其适用场景:

    ### 方法 1:利用 ES6 的 `Set`(最简洁)

    `Set` 是 ES6 新增的数据结构,其特性是**成员唯一(不允许重复)**,结合数组的 `from` 方法或扩展运算符 `...` 可快速去重。

    **代码示例**:

    ```javascript
    function unique(arr) {
    // Set 接收数组作为参数,自动去重;再转为数组
    return [...new Set(arr)];
    // 或 return Array.from(new Set(arr));
    }

    // 测试
    const arr = [1, 2, 2, 3, 3, 3, null, null, undefined, undefined];
    console.log(unique(arr)); // [1, 2, 3, null, undefined]
    ```

    **优点**:简洁高效,一行代码实现。
    **缺点**:不兼容 ES6 之前的环境(如 IE11 及以下),且无法处理对象类型(对象引用不同时会被视为不同元素)。

    ### 方法 2:利用对象的 `key` 唯一特性

    对象的属性名(`key`)具有唯一性,可通过判断元素是否已作为对象的 `key` 存在来实现去重。

    **代码示例**:

    ```javascript
    function unique(arr) {
    const obj = {};
    const result = [];
    for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    // 用 JSON.stringify 处理复杂类型(避免 1 和 '1' 被视为相同)
    const key = typeof item + JSON.stringify(item);
    if (!obj[key]) {
    obj[key] = true; // 标记为已存在
    result.push(item); // 加入结果数组
    }
    }
    return result;
    }

    // 测试
    const arr = [1, '1', 1, { a: 1 }, { a: 1 }, null, null];
    console.log(unique(arr)); // [1, '1', {a:1}, null](对象因引用不同被保留)
    ```

    **优点**:可处理基本类型和复杂类型(通过 `JSON.stringify` 区分),兼容性好。
    **缺点**:实现相对繁琐,`JSON.stringify` 对函数、`Symbol` 等类型处理有限制。

    ### 方法 3:利用数组的 `indexOf` 或 `includes`

    遍历数组,通过 `indexOf` 或 `includes` 判断元素是否已在新数组中存在,仅保留首次出现的元素。

    **代码示例(`indexOf`)**:

    ```javascript
    function unique(arr) {
    const result = [];
    for (let i = 0; i < arr.length; i++) {
    // indexOf 返回元素在 result 中首次出现的索引,-1 表示不存在
    if (result.indexOf(arr[i]) === -1) {
    result.push(arr[i]);
    }
    }
    return result;
    }

    // 测试
    const arr = [2, 1, 2, 3, 1];
    console.log(unique(arr)); // [2, 1, 3]
    ```

    **优点**:逻辑直观,兼容性好(支持 IE9+)。
    **缺点**:`indexOf` 每次都会遍历数组,时间复杂度为 O(n²),大数据量下效率较低。

    ### 方法 4:利用 `filter` + `indexOf`

    `filter` 方法筛选出**首次出现**的元素(当前元素的索引等于它在原数组中首次出现的索引)。

    **代码示例**:

    ```javascript
    function unique(arr) {
    return arr.filter((item, index) => {
    // 仅保留 index 等于首次出现位置的元素
    return arr.indexOf(item) === index;
    });
    }

    // 测试
    const arr = ['a', 'b', 'a', 'c', 'b'];
    console.log(unique(arr)); // ['a', 'b', 'c']
    ```

    **优点**:代码简洁,利用数组方法链式调用。
    **缺点**:同方法 3,`indexOf` 导致效率较低,且无法保留最后一次出现的元素。

    ### 方法 5:排序后去重(相邻元素对比)

    先对数组排序,重复元素会相邻,再遍历数组,仅保留与前一个元素不同的元素。

    **代码示例**:

    ```javascript
    function unique(arr) {
    if (arr.length <= 1) return arr;
    // 先排序(数字/字符串均可排序)
    const sortedArr = [...arr].sort((a, b) => a - b || a.localeCompare(b));
    const result = [sortedArr[0]];
    for (let i = 1; i < sortedArr.length; i++) {
    // 仅保留与前一个元素不同的元素
    if (sortedArr[i] !== sortedArr[i - 1]) {
    result.push(sortedArr[i]);
    }
    }
    return result;
    }

    // 测试
    const arr = [3, 1, 3, 2, 1];
    console.log(unique(arr)); // [1, 2, 3](排序后去重)
    ```

    **优点**:排序后去重逻辑简单,适合需要同时排序的场景。
    **缺点**:会改变原数组的顺序,且对复杂类型(如对象)排序可能不符合预期。

    ### 总结

    - 优先推荐 **`Set` 方法**(简洁高效,适用于现代环境)。
    - 需兼容旧环境或处理复杂类型,可选 **对象 `key` 方法**。
    - 需保留原数组顺序且数据量小时,可选 **`indexOf` 或 `filter` 方法**。

    根据实际场景(兼容性、数据类型、是否需保留顺序)选择合适的方法即可。

    ## 30.简述 JavaScript 中的对象的常用方法(如 Object.keys、Object.values、Object.assign、Object.freeze)

    JavaScript 提供了一系列内置的对象方法,用于操作和处理对象的属性、状态等,以下是最常用的对象方法及其特点:

    ### 1. `Object.keys(obj)`

    - **作用**:获取对象**自身所有可枚举属性的键名**(不包括继承的属性和不可枚举属性),返回一个由键名组成的数组。
    - **参数**:`obj`(要操作的对象)。
    - **返回值**:键名组成的数组(顺序与 `for...in` 循环一致,但不包含继承属性)。
    - **示例**:

    ```javascript
    const user = { name: "张三", age: 20, gender: "男" };
    const keys = Object.keys(user);
    console.log(keys); // ["name", "age", "gender"](键名数组)
    ```

    ### 2. `Object.values(obj)`

    - **作用**:获取对象**自身所有可枚举属性的属性值**,返回一个由值组成的数组(与 `Object.keys` 对应,顺序一致)。
    - **参数**:`obj`(要操作的对象)。
    - **返回值**:属性值组成的数组。
    - **示例**:

    ```javascript
    const user = { name: "张三", age: 20, gender: "男" };
    const values = Object.values(user);
    console.log(values); // ["张三", 20, "男"](值数组)
    ```

    ### 3. `Object.entries(obj)`

    - **作用**:获取对象**自身所有可枚举属性的键值对**,返回一个二维数组(每个子数组为 `[键名, 值]`)。
    - **参数**:`obj`(要操作的对象)。
    - **返回值**:键值对组成的二维数组。
    - **示例**:

    ```javascript
    const user = { name: "张三", age: 20 };
    const entries = Object.entries(user);
    console.log(entries); // [["name", "张三"], ["age", 20]]
    // 常用于将对象转为 Map
    const userMap = new Map(entries);
    console.log(userMap.get("name")); // "张三"
    ```

    ### 4. `Object.assign(target, ...sources)`

    - **作用**:将一个或多个**源对象(sources)的可枚举属性复制到目标对象(target)**,返回合并后的目标对象(浅拷贝)。
    - **参数**:`target`(目标对象)、`...sources`(一个或多个源对象)。
    - **特点**:
    - 同名属性会被后面的源对象覆盖;
    - 只拷贝自身属性,不拷贝继承属性和不可枚举属性;
    - 是浅拷贝(若属性值为对象,仅复制引用)。
    - **示例**:

    ```javascript
    const target = { a: 1 };
    const source1 = { b: 2 };
    const source2 = { a: 3, c: 4 };

    // 合并 source1、source2 到 target
    const result = Object.assign(target, source1, source2);
    console.log(result); // { a: 3, b: 2, c: 4 }(target 被修改)
    console.log(target === result); // true(返回的是目标对象本身)
    ```

    ### 5. `Object.freeze(obj)`

    - **作用**:**冻结对象**,冻结后对象无法被修改:
    - 不能添加新属性;
    - 不能删除现有属性;
    - 不能修改属性值(基本类型)或属性的可枚举性、可配置性;
    - 是“浅冻结”(若属性值为对象,该对象内部仍可修改)。
    - **参数**:`obj`(要冻结的对象)。
    - **返回值**:被冻结的对象(与传入的 `obj` 是同一个引用)。
    - **示例**:

    ```javascript
    const user = { name: "张三", info: { age: 20 } };
    Object.freeze(user);

    // 尝试修改属性:无效
    user.name = "李四";
    console.log(user.name); // "张三"(未修改)

    // 尝试添加属性:无效
    user.gender = "男";
    console.log(user.gender); // undefined

    // 浅冻结:内部对象仍可修改
    user.info.age = 21;
    console.log(user.info.age); // 21(修改成功)
    ```

    ### 6. `Object.hasOwnProperty(prop)`

    - **作用**:检查对象**自身是否包含指定属性**(不包括继承的属性,如原型链上的属性),返回布尔值。
    - **参数**:`prop`(属性名,字符串或 Symbol)。
    - **返回值**:`true`(自身包含该属性)/ `false`(不包含或为继承属性)。
    - **示例**:

    ```javascript
    const user = { name: "张三" };
    // 检查自身属性
    console.log(user.hasOwnProperty("name")); // true
    // 检查继承属性(如 toString 是 Object 原型的方法)
    console.log(user.hasOwnProperty("toString")); // false
    ```

    ### 总结

    | 方法 | 核心作用 | 典型场景 |
    |---------------------|-----------------------------------|------------------------------|
    | `Object.keys` | 获取自身可枚举键名数组 | 遍历对象键名 |
    | `Object.values` | 获取自身可枚举值数组 | 遍历对象值 |
    | `Object.entries` | 获取自身可枚举键值对数组 | 对象转 Map、键值对遍历 |
    | `Object.assign` | 合并对象(浅拷贝) | 对象复制、属性合并 |
    | `Object.freeze` | 冻结对象(禁止修改) | 保护常量对象、防止意外修改 |
    | `Object.hasOwnProperty` | 检查自身是否有指定属性 | 区分自身属性与继承属性 |

    这些方法是操作对象的基础工具,掌握它们能高效处理对象的属性访问、复制、保护等需求。

    ## 31.如何实现对象的深拷贝和浅拷贝?它们的区别是什么?

    ### 浅拷贝与深拷贝的核心区别

    浅拷贝和深拷贝都是复制对象的方式,核心区别在于**是否复制嵌套的引用类型(如对象、数组)**:

    - **浅拷贝(Shallow Copy)**:
    只复制对象的表层属性。对于**基本类型属性**(如 number、string),会复制具体值;但对于**引用类型属性**(如对象、数组),仅复制引用地址(新对象和原对象的嵌套引用类型指向同一块内存)。因此,修改新对象的嵌套引用类型属性,会影响原对象。

    - **深拷贝(Deep Copy)**:
    递归复制对象的所有层级(包括嵌套的引用类型)。新对象和原对象的所有属性(无论基本类型还是引用类型)都是完全独立的,修改其中一个不会影响另一个。

    ### 一、浅拷贝的实现方法

    #### 1. `Object.assign(target, source)`

    `Object.assign` 会将源对象的可枚举属性复制到目标对象,适合拷贝单层对象。

    ```javascript
    const obj = { name: "张三", info: { age: 20 } };

    // 浅拷贝:只复制表层
    const shallowCopy = Object.assign({}, obj);

    // 修改基本类型属性:不影响原对象
    shallowCopy.name = "李四";
    console.log(obj.name); // "张三"(原对象不变)

    // 修改嵌套对象属性:影响原对象(引用相同)
    shallowCopy.info.age = 21;
    console.log(obj.info.age); // 21(原对象被修改)
    ```

    #### 2. 扩展运算符 `...`

    扩展运算符可用于对象和数组的浅拷贝,语法更简洁。

    ```javascript
    const obj = { a: 1, b: { c: 2 } };

    // 浅拷贝对象
    const shallowCopy = { ...obj };

    // 浅拷贝数组
    const arr = [1, [2, 3]];
    const shallowArr = [...arr];
    ```

    #### 3. 数组的 `slice()` 或 `concat()`

    数组的这两个方法会返回新数组,属于浅拷贝(仅复制数组元素的引用)。

    ```javascript
    const arr = [1, [2, 3]];
    const shallowArr1 = arr.slice(); // 浅拷贝
    const shallowArr2 = [].concat(arr); // 浅拷贝

    shallowArr1[1][0] = 20;
    console.log(arr[1][0]); // 20(原数组的嵌套数组被修改)
    ```

    ### 二、深拷贝的实现方法

    #### 1. `JSON.parse(JSON.stringify(obj))`(简单场景适用)

    利用 JSON 序列化与反序列化实现深拷贝,适合处理纯 JSON 数据(无特殊类型)。

    ```javascript
    const obj = { name: "张三", info: { age: 20 }, hobbies: ["篮球"] };

    // 深拷贝
    const deepCopy = JSON.parse(JSON.stringify(obj));

    // 修改嵌套属性:不影响原对象
    deepCopy.info.age = 21;
    deepCopy.hobbies[0] = "足球";
    console.log(obj.info.age); // 20(原对象不变)
    console.log(obj.hobbies[0]); // "篮球"(原对象不变)
    ```

    **局限性**:

    - 无法拷贝函数、`Symbol`、`Date`(会转为字符串后再解析,可能失真)、`RegExp` 等特殊类型;
    - 存在循环引用(如 `obj.self = obj`)时会报错;
    - 会忽略 `undefined` 和不可枚举属性。

    #### 2. 手动递归实现(支持复杂场景)

    通过递归遍历对象的所有属性,对引用类型递归拷贝,基本类型直接复制,可处理特殊类型和循环引用。

    ```javascript
    function deepClone(obj, hash = new WeakMap()) {
    // 处理 null 或非对象类型(基本类型直接返回)
    if (obj === null || typeof obj !== "object") return obj;

    // 处理循环引用(已拷贝过的对象直接返回)
    if (hash.has(obj)) return hash.get(obj);

    // 处理数组和对象
    let cloneObj;
    if (obj instanceof Array) {
    cloneObj = [];
    } else if (obj instanceof Object) {
    cloneObj = {};
    }

    // 缓存已拷贝的对象(解决循环引用)
    hash.set(obj, cloneObj);

    // 递归拷贝属性
    Reflect.ownKeys(obj).forEach(key => {
    cloneObj[key] = deepClone(obj[key], hash); // 递归拷贝子属性
    });

    return cloneObj;
    }

    // 测试
    const obj = {
    name: "张三",
    info: { age: 20 },
    fn: () => console.log("函数"), // 可拷贝函数(JSON方法不行)
    date: new Date()
    };
    obj.self = obj; // 循环引用

    const deepCopy = deepClone(obj);
    console.log(deepCopy.info.age); // 20(与原对象独立)
    console.log(deepCopy.self === deepCopy); // true(正确处理循环引用)
    ```

    #### 3. 第三方库(如 Lodash 的 `cloneDeep`)

    成熟库的深拷贝方法处理了各种边缘场景,适合生产环境。

    ```javascript
    // 需引入 Lodash
    import { cloneDeep } from "lodash";

    const obj = { a: 1, b: { c: 2 } };
    const deepCopy = cloneDeep(obj);
    ```

    ### 总结

    | 类型 | 核心区别 | 适用场景 | 常用方法 |
    |----------|-----------------------------------|------------------------------|-----------------------------------|
    | 浅拷贝 | 不复制嵌套引用类型(共享引用) | 单层对象、性能优先场景 | `Object.assign`、扩展运算符、`slice` |
    | 深拷贝 | 完全复制所有层级(独立引用) | 嵌套对象、需彻底隔离的场景 | `JSON.parse`(简单场景)、递归实现、Lodash `cloneDeep` |

    实际开发中,根据对象复杂度和是否包含特殊类型选择合适的方法:简单JSON数据用 `JSON.parse`,复杂场景用递归实现或第三方库。

    ## 32.JavaScript 中的字符串的常用方法有哪些(如 indexOf、slice、substring、replace、split)?

    JavaScript 字符串提供了丰富的内置方法,用于处理和操作字符串内容。以下是常用方法的分类及核心用法(包含你提到的 `indexOf`、`slice` 等,及其他高频方法):

    ### 一、查找与判断(子字符串相关)

    #### 1. `indexOf(searchValue, fromIndex)`

    - **作用**:从 `fromIndex`(默认 0)开始查找 `searchValue` 在字符串中**首次出现的索引**。
    - **返回值**:找到则返回索引(number),未找到返回 `-1`。
    - **示例**:

    ```javascript
    const str = "hello world";
    console.log(str.indexOf("o")); // 4(首次出现的索引)
    console.log(str.indexOf("o", 5)); // 7(从索引5开始查找)
    console.log(str.indexOf("xyz")); // -1(未找到)
    ```

    #### 2. `lastIndexOf(searchValue, fromIndex)`

    - **作用**:从 `fromIndex`(默认字符串末尾)开始**反向查找** `searchValue` 最后一次出现的索引。
    - **示例**:

    ```javascript
    const str = "hello world";
    console.log(str.lastIndexOf("o")); // 7(最后一次出现的索引)
    ```

    #### 3. `includes(searchValue, fromIndex)`

    - **作用**:判断字符串是否**包含** `searchValue`(从 `fromIndex` 开始查找)。
    - **返回值**:布尔值(`true`/`false`)。
    - **示例**:

    ```javascript
    const str = "hello";
    console.log(str.includes("ll")); // true
    console.log(str.includes("x")); // false
    ```

    #### 4. `startsWith(searchValue, fromIndex)` / `endsWith(searchValue, length)`

    - **作用**:
    - `startsWith`:判断字符串是否以 `searchValue` **开头**(从 `fromIndex` 开始,默认 0)。
    - `endsWith`:判断字符串是否以 `searchValue` **结尾**(`length` 可选,指定字符串长度,默认原长度)。
    - **返回值**:布尔值。
    - **示例**:

    ```javascript
    const str = "hello world";
    console.log(str.startsWith("hello")); // true
    console.log(str.endsWith("world")); // true
    console.log(str.endsWith("lo", 5)); // true(仅判断前5个字符 "hello" 是否以 "lo" 结尾)
    ```

    ### 二、截取与提取

    #### 1. `slice(startIndex, endIndex)`

    - **作用**:从 `startIndex` 截取到 `endIndex`(**不包含 `endIndex`**),支持负数索引(负数表示从末尾开始计算,如 `-1` 是最后一个字符)。
    - **返回值**:截取的子字符串(原字符串不变)。
    - **示例**:

    ```javascript
    const str = "abcdef";
    console.log(str.slice(1, 4)); // "bcd"(从索引1到3)
    console.log(str.slice(2)); // "cdef"(从索引2截取到末尾)
    console.log(str.slice(-3)); // "def"(从倒数第3个字符截取到末尾)
    ```

    #### 2. `substring(startIndex, endIndex)`

    - **作用**:类似 `slice`,但**不支持负数索引**,且若 `startIndex > endIndex` 会自动交换参数。
    - **示例**:

    ```javascript
    const str = "abcdef";
    console.log(str.substring(1, 4)); // "bcd"(同 slice)
    console.log(str.substring(4, 1)); // "bcd"(自动交换参数,等价于 substring(1,4))
    console.log(str.substring(-2)); // "abcdef"(负数被视为0)
    ```

    #### 3. `substr(startIndex, length)`(注意:部分浏览器已弃用,建议用 `slice`)

    - **作用**:从 `startIndex` 开始,截取 `length` 个字符(`length` 省略则截取到末尾)。
    - **示例**:

    ```javascript
    const str = "abcdef";
    console.log(str.substr(1, 3)); // "bcd"(从索引1开始,截取3个字符)
    ```

    #### 4. `charAt(index)`

    - **作用**:返回字符串中 `index` 位置的字符(索引越界返回空字符串 `""`)。
    - **示例**:

    ```javascript
    const str = "hello";
    console.log(str.charAt(1)); // "e"
    console.log(str.charAt(10)); // ""(索引越界)
    ```

    ### 三、替换与分割

    #### 1. `replace(searchValue, replacement)`

    - **作用**:用 `replacement` 替换 `searchValue`(`searchValue` 可以是字符串或正则表达式)。
    - **特点**:
    - 默认只替换**第一个匹配项**;
    - 若 `searchValue` 是带 `g` 修饰符的正则,会替换**所有匹配项**;
    - `replacement` 中可使用特殊字符(如 `$&` 表示匹配的子串)。
    - **示例**:

    ```javascript
    const str = "hello world, hello javascript";

    // 替换第一个 "hello"
    console.log(str.replace("hello", "hi")); // "hi world, hello javascript"

    // 正则全局替换所有 "hello"
    console.log(str.replace(/hello/g, "hi")); // "hi world, hi javascript"

    // 特殊替换符($& 表示匹配的内容)
    console.log(str.replace(/hello/, "[$&]")); // "[hello] world, hello javascript"
    ```

    #### 2. `split(separator, limit)`

    - **作用**:以 `separator` 为分隔符,将字符串**分割为数组**(`limit` 可选,限制返回数组的长度)。
    - **示例**:

    ```javascript
    const str = "apple,banana,orange";

    // 以逗号分割
    console.log(str.split(",")); // ["apple", "banana", "orange"]

    // 限制返回2个元素
    console.log(str.split(",", 2)); // ["apple", "banana"]

    // 以空字符串分割(拆分为单个字符)
    console.log("abc".split("")); // ["a", "b", "c"]
    ```

    ### 四、转换与修整

    #### 1. `toUpperCase()` / `toLowerCase()`

    - **作用**:将字符串转为**全大写** / **全小写**。
    - **示例**:

    ```javascript
    const str = "Hello World";
    console.log(str.toUpperCase()); // "HELLO WORLD"
    console.log(str.toLowerCase()); // "hello world"
    ```

    #### 2. `trim()` / `trimStart()` / `trimEnd()`

    - **作用**:
    - `trim()`:去除字符串**首尾的空白字符**(空格、换行、制表符等)。
    - `trimStart()` / `trimEnd()`:仅去除开头 / 结尾的空白字符。
    - **示例**:

    ```javascript
    const str = " hello ";
    console.log(str.trim()); // "hello"(去除首尾空格)
    console.log(str.trimStart()); // "hello "(仅去除开头空格)
    ```

    ### 总结

    | 类别 | 常用方法 | 核心功能 |
    |--------------|-----------------------------------|------------------------------|
    | 查找判断 | `indexOf`、`includes`、`startsWith` | 查找子串位置、判断包含关系 |
    | 截取提取 | `slice`、`substring`、`charAt` | 截取子串、获取指定位置字符 |
    | 替换分割 | `replace`、`split` | 替换子串、分割为数组 |
    | 转换修整 | `toUpperCase`、`trim` | 大小写转换、去除空白 |

    这些方法是处理字符串的基础,掌握后可高效完成字符串的查找、截取、转换等操作,且均**不修改原字符串**(返回新字符串或数组)。
    JavaScript 中的字符串方法**可以链式调用**,这是因为大多数字符串方法的返回值仍然是一个字符串(或可继续调用方法的对象),因此可以在返回值上直接调用下一个方法。

    ### 原理

    字符串是 JavaScript 中的**不可变类型**——所有字符串方法都不会修改原字符串,而是返回一个**新的字符串**(或其他数据类型,如 `indexOf` 返回数字)。
    当方法返回新字符串时,就可以在这个新字符串上继续调用其他字符串方法,形成链式调用。

    ### JavaScript中的字符串方法可以链式调用吗?

    ### 链式调用的优势

    - **代码简洁**:链式调用可以将多个操作合并为一行,提高代码可读性。
    - **避免中间变量**:不必为每个操作创建额外的变量,减少代码量。
    - **更灵活**:可以根据需要选择调用哪些方法,而无需关心方法的返回值。

    ### 示例

    以下是链式调用的典型场景:

    ```javascript
    const str = " Hello, World! ";

    // 链式调用:先去除首尾空格 → 转为小写 → 替换逗号为感叹号 → 截取前10个字符
    const result = str
    .trim() // "Hello, World!"(去除空格)
    .toLowerCase() // "hello, world!"(转为小写)
    .replace(",", "!") // "hello! world!"(替换字符)
    .slice(0, 10); // "hello! wor"(截取子串)

    console.log(result); // "hello! wor"
    ```

    ### 注意事项

    - 链式调用的前提是**前一个方法的返回值必须是字符串**(或包含后续方法的对象)。
    若某个方法返回非字符串类型(如 `indexOf` 返回数字、`includes` 返回布尔值),则后续无法继续调用字符串方法,否则会报错。

    ```javascript
    // 错误示例:indexOf 返回数字,无法调用 toUpperCase()
    "abc".indexOf("a").toUpperCase(); // TypeError: 数字没有 toUpperCase 方法
    ```

    - 链式调用不宜过长,否则会降低代码可读性,建议适当拆分。

    ### 总结

    由于字符串方法通常返回新字符串,因此支持链式调用,这一特性可以简化代码,提高编写效率。使用时只需确保链式调用中的每个步骤都返回字符串即可。

    ## 33.什么是正则表达式?简述正则表达式的常见元字符(如 ^、\$、.、\*、+)及作用

    ### 什么是正则表达式

    正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于**描述字符串模式的工具**,主要用于字符串的匹配、查找、替换和验证。它通过“普通字符”(如 `a`、`1`)和“元字符”(具有特殊含义的符号)的组合,定义一个“规则”,然后用这个规则检查字符串是否符合模式,或从字符串中提取/替换符合模式的部分。

    例如,用正则 `\d{11}` 可匹配 11 位数字(如手机号),用 `^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z]+$` 可验证邮箱格式。

    ### 正则表达式的常见元字符及作用

    元字符是正则表达式中**具有特殊含义的字符**(非字面意义),用于构建匹配规则。以下是常见元字符及其作用:

    #### 1. `^`(脱字符)

    - **作用**:匹配字符串的**开始位置**(表示“以…开头”)。
    - **示例**:
    - `^abc` 匹配以 "abc" 开头的字符串(如 "abc123" 会匹配,"xabc" 不匹配)。
    - `^Hello` 匹配以 "Hello" 开头的字符串(如 "Hello World" 会匹配)。

    #### 2. `$`(美元符)

    - **作用**:匹配字符串的**结束位置**(表示“以…结尾”)。
    - **示例**:
    - `abc$` 匹配以 "abc" 结尾的字符串(如 "123abc" 会匹配,"abcx" 不匹配)。
    - `World$` 匹配以 "World" 结尾的字符串(如 "Hello World" 会匹配)。

    #### 3. `.`(点号)

    - **作用**:匹配除**换行符(\n)** 之外的**任意单个字符**(字母、数字、符号等)。
    - **示例**:
    - `a.c` 匹配 "a" 和 "c" 之间有任意一个字符的字符串(如 "abc"、"a1c"、"a@c" 均匹配,但 "ac" 不匹配,因中间无字符)。
    - `h.t` 匹配 "hot"、"h2t"、"h#t" 等,但不匹配 "ht" 或 "h\nt"(含换行符)。

    #### 4. `*`(星号)

    - **作用**:表示**前面的字符或子模式可以出现 0 次或多次**(默认贪婪模式:尽可能多匹配)。
    - **示例**:
    - `a*` 匹配 0 个或多个 "a"(如 ""、"a"、"aa"、"aaa" 均匹配)。
    - `ab*c` 中 `b*` 表示 "b" 可出现 0 次或多次:匹配 "ac"(b 出现 0 次)、"abc"(b 出现 1 次)、"abbc"(b 出现 2 次)等。

    #### 5. `+`(加号)

    - **作用**:表示**前面的字符或子模式可以出现 1 次或多次**(默认贪婪模式)。
    - **示例**:
    - `a+` 匹配 1 个或多个 "a"(如 "a"、"aa" 匹配,但 "" 不匹配)。
    - `ab+c` 中 `b+` 表示 "b" 至少出现 1 次:匹配 "abc"(b 1 次)、"abbc"(b 2 次)等,但不匹配 "ac"(b 0 次)。

    #### 补充:其他常用元字符

    - `?`:表示前面的字符或子模式出现 **0 次或 1 次**(如 `a?` 匹配 "" 或 "a")。
    - `\d`:匹配任意**数字**(等价于 `[0-9]`)。
    - `\w`:匹配任意**字母、数字或下划线**(等价于 `[a-zA-Z0-9_]`)。
    - `[]`:字符集,匹配括号内的**任意一个字符**(如 `[abc]` 匹配 "a"、"b" 或 "c")。
    - `()`:分组,将子模式视为一个整体(如 `(ab)+` 匹配 "ab"、"abab" 等)。

    ### 总结

    元字符是正则表达式的核心,通过 `^`、`$` 定位字符串首尾,`.*+` 控制字符出现次数,`. \d \w` 匹配特定类型字符等,可组合出灵活的匹配规则,广泛用于表单验证、日志分析、文本处理等场景。

    ## 34.正则表达式的 test 和 exec 方法有什么区别?

    `test` 和 `exec` 是 JavaScript 正则表达式(`RegExp` 对象)的两个核心方法,均用于执行字符串匹配,但功能和返回值有显著区别,主要体现在**用途**和**返回结果**上:

    ### 1. `test(str)` 方法

    - **核心作用**:检测字符串 `str` 是否**符合正则表达式的匹配规则**(仅判断“是否匹配”,不返回具体匹配内容)。
    - **返回值**:布尔值(`true` 表示匹配成功,`false` 表示匹配失败)。
    - **适用场景**:简单的匹配检查(如验证输入格式是否正确)。

    **示例**:

    ```javascript
    const regex = /\d+/; // 匹配一个或多个数字

    // 检查字符串是否包含数字
    console.log(regex.test("abc123")); // true(包含数字)
    console.log(regex.test("abcdef")); // false(不包含数字)
    ```

    ### 2. `exec(str)` 方法

    - **核心作用**:在字符串 `str` 中**执行正则匹配**,返回匹配的**详细信息**(若匹配失败则返回 `null`)。
    - **返回值**:
    - 匹配成功时:返回一个**数组**,包含以下信息:
    - 索引 `0`:整个匹配的子字符串;
    - 索引 `1, 2, ...`:正则中分组(`()` 包裹的部分)匹配的子字符串;
    - `index` 属性:匹配结果在原字符串中的起始索引;
    - `input` 属性:原字符串(被匹配的字符串)。
    - 匹配失败时:返回 `null`。
    - **适用场景**:需要获取匹配的具体内容(如提取子串、获取分组信息)。

    **示例**:

    ```javascript
    const regex = /(\w+)@(\w+)\.(\w+)/; // 匹配邮箱(分组:用户名、域名、后缀)
    const str = "我的邮箱是 abc@example.com";

    const result = regex.exec(str);
    if (result) {
    console.log(result[0]); // "abc@example.com"(整个匹配的邮箱)
    console.log(result[1]); // "abc"(第一个分组:用户名)
    console.log(result[2]); // "example"(第二个分组:域名)
    console.log(result[3]); // "com"(第三个分组:后缀)
    console.log(result.index); // 5(邮箱在原字符串中的起始索引)
    console.log(result.input); // "我的邮箱是 abc@example.com"(原字符串)
    }
    ```

    ### 关键区别总结

    | 维度 | `test(str)` | `exec(str)` |
    |---------------------|--------------------------------------|--------------------------------------|
    | **返回值** | 布尔值(`true`/`false`) | 匹配信息数组(成功)或 `null`(失败) |
    | **核心用途** | 判断是否匹配(“有或无”) | 获取匹配的详细内容(“是什么、在哪里”) |
    | **全局匹配(`g`)** | 会更新正则的 `lastIndex`(下次匹配从该位置开始),可能影响连续调用结果 | 会更新 `lastIndex`,且可通过多次调用获取所有匹配结果(全局模式下) |
    | **性能** | 更轻量(仅返回布尔值) | 更复杂(需返回详细信息) |

    ### 全局模式(`g` 修饰符)下的特殊行为

    当正则表达式带有 `g`(全局匹配)修饰符时,两者都会更新正则对象的 `lastIndex` 属性(记录下次匹配的起始位置),但行为略有不同:

    - **`test`**:连续调用时,会从 `lastIndex` 开始匹配,直到匹配失败后 `lastIndex` 重置为 0。

    ```javascript
    const regex = /a/g;
    const str = "aab";

    console.log(regex.test(str)); // true(匹配第一个 'a',lastIndex 变为 1)
    console.log(regex.test(str)); // true(匹配第二个 'a',lastIndex 变为 2)
    console.log(regex.test(str)); // false(无匹配,lastIndex 重置为 0)
    ```

    - **`exec`**:全局模式下,多次调用可依次获取所有匹配结果(直到返回 `null`),适合提取所有符合规则的子串。

    ```javascript
    const regex = /a/g;
    const str = "aab";

    console.log(regex.exec(str)); // ["a", index: 0, ...](第一个 'a',lastIndex=1)
    console.log(regex.exec(str)); // ["a", index: 1, ...](第二个 'a',lastIndex=2)
    console.log(regex.exec(str)); // null(无匹配,lastIndex=0)
    ```

    ### 总结

    - 仅需判断“是否匹配”(如验证手机号格式):用 `test`,简洁高效。
    - 需要获取匹配的具体内容(如提取所有邮箱、获取分组信息):用 `exec`,功能更强大。
    - 全局模式下需注意 `lastIndex` 对连续调用的影响。

    ## 35.简述 JavaScript 中的类型转换规则(强制转换和隐式转换),如数字转字符串、字符串转数字

    在 JavaScript 中,类型转换是将一种数据类型(如 `string`、`number`、`boolean`)转换为另一种类型的过程,主要分为 **强制转换(显式转换)** 和 **隐式转换(自动转换)** 两类。JS 的原始类型(`string`、`number`、`boolean`、`null`、`undefined`、`symbol`、`bigint`)间的转换规则是核心,对象转原始类型则依赖内部逻辑。

    ## 一、强制转换(显式转换)

    强制转换是通过 **显式调用内置函数** 主动触发的转换,规则明确,常见场景包括“转字符串”“转数字”“转布尔值”。

    ### 1. 强制转换为字符串(`String()` 或 `toString()`)

    #### 核心方法

    - **`String(value)`**:可处理所有类型(包括 `null` 和 `undefined`)。
    - **`value.toString()`**:仅适用于非 `null`/`undefined` 的值(否则报错),部分类型(如数组、数字)有自定义实现。

    #### 转换规则(按类型划分)

    | 原始类型 | 转换结果示例 | 说明 |
    |----------------|-----------------------------|---------------------------------|
    | `number` | `123`→`"123"`、`NaN`→`"NaN"`、`Infinity`→`"Infinity"` | 数字直接转为对应字符串,特殊值保留字面量 |
    | `boolean` | `true`→`"true"`、`false`→`"false"` | 布尔值直接转为对应字符串 |
    | `null` | `String(null)`→`"null"` | `null.toString()` 会报错(Cannot read property 'toString' of null) |
    | `undefined` | `String(undefined)`→`"undefined"` | `undefined.toString()` 会报错 |
    | `symbol` | `Symbol('a')`→`"Symbol(a)"` | 符号转为带描述的字符串 |
    | **对象/数组** | `{}`→`"[object Object]"`、`[1,2]`→`"1,2"`、`[]`→`""` | 默认调用 `toString()`:数组会拼接元素,普通对象返回固定格式 |

    #### 示例

    ```javascript
    // String() 处理所有类型
    console.log(String(123)); // "123"
    console.log(String(null)); // "null"
    console.log(String(undefined)); // "undefined"

    // toString() 处理非 null/undefined
    console.log((123).toString()); // "123"(数字需加括号,避免解析为小数点)
    console.log([1,2].toString()); // "1,2"
    // console.log(null.toString()); // 报错:Cannot read property 'toString' of null

2. 强制转换为数字(Number()parseInt()parseFloat()

核心方法(分两类)

  • Number(value)严格转换,整个值必须符合数字格式(否则返回 NaN),适用于完整值转换。
  • parseInt(str, radix) / parseFloat(str)解析转换,从字符串开头提取数字,遇到非数字停止(适用于提取部分数字),parseInt 需指定基数(如 10 表示十进制,避免八进制歧义)。

转换规则(以 Number() 为例)

原始类型 转换结果示例 说明
string "123"123"123abc"NaN""0"0x10"16 纯数字字符串转对应数字,非纯数字/空字符串特殊处理
boolean true1false0 布尔值固定对应 1/0
null Number(null)0 null 转为 0(特殊规则)
undefined Number(undefined)NaN undefined 无法转为有效数字
symbol Number(Symbol('a'))NaN 符号无法转为数字
对象/数组 {}NaN[]0[123]123[1,2]NaN 先转原始类型(调用 valueOf()toString()),再转数字

Number()parseInt() 区别示例

1
2
3
4
5
6
7
8
// Number() 严格转换
console.log(Number("123abc")); // NaN(非纯数字字符串)
console.log(Number("")); // 0(空字符串)

// parseInt() 解析转换(从开头提取数字)
console.log(parseInt("123abc", 10)); // 123(忽略后面的 "abc",基数10)
console.log(parseInt("abc123", 10)); // NaN(开头非数字)
console.log(parseInt("0x10", 16)); // 16(基数16,解析十六进制)

3. 强制转换为布尔值(Boolean()

核心规则

JS 中只有 7 个“假值(falsy value)” 会转为 false,其余所有值(包括空对象、空数组)均转为 true

假值列表(必记)

  1. false(本身)
  2. 0-0(数字零)
  3. NaN(非数字)
  4. ""(空字符串)
  5. null
  6. undefined
  7. 0n(BigInt 零)

示例

1
2
3
4
5
console.log(Boolean(0));        // false(假值)
console.log(Boolean("0")); // true(非空字符串,不是假值)
console.log(Boolean([])); // true(空数组不是假值)
console.log(Boolean({})); // true(空对象不是假值)
console.log(Boolean(null)); // false(假值)

二、隐式转换(自动转换)

隐式转换是 JS 在 运算、比较、逻辑判断 等场景下自动触发的转换,无需显式调用函数,规则依赖具体场景(目标类型通常是“字符串”“数字”或“布尔值”)。

1. 隐式转换为字符串(场景:+ 运算符含字符串)

+ 运算符的 至少一个操作数是字符串 时,其他操作数会隐式转为字符串,最终执行“字符串拼接”(而非加法)。

示例

1
2
3
4
5
6
7
8
// 数字 + 字符串 → 字符串(数字隐转字符串)
console.log(123 + "45"); // "12345"

// 布尔值 + 字符串 → 字符串(布尔隐转字符串)
console.log(true + "abc"); // "trueabc"

// 数组 + 字符串 → 字符串(数组先转原始类型为字符串)
console.log([1,2] + "3"); // "1,23"

2. 隐式转换为数字(场景:算术运算、比较运算)

当触发 算术运算(-*/%++--比较运算(><<=>= 时,非数字操作数会隐式转为数字。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 算术运算:字符串隐转数字
console.log("5" - 3); // 2("5"→5,5-3=2)
console.log("10" * 2); // 20("10"→10,10*2=20)
console.log("abc" / 1); // NaN("abc"→NaN,NaN/1=NaN)

// 比较运算:字符串隐转数字
console.log("12" > 5); // true("12"→12,12>5)
console.log("20" < "3"); // false(注意:两个字符串比较时转 Unicode 编码,"2"→50,"3"→51,50<51,但此处"20"是"2"开头,仍按编码比)

// 自增运算符:字符串隐转数字
let a = "3";
a++;
console.log(a); // 4("3"→3,自增后为4)

3. 隐式转换为布尔值(场景:逻辑判断、条件语句)

当触发 逻辑运算符(&&||!条件语句(ifwhile 时,操作数会隐式转为布尔值,规则与 Boolean() 一致(假值转 false,其余转 true)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
// if 条件:隐转布尔值
if ("hello") {
console.log("执行"); // 执行("hello"→true)
}

// 逻辑非 !:隐转布尔值后取反
console.log(!0); // true(0→false,取反为true)
console.log(![]); // false([]→true,取反为false)

// 逻辑与 &&:短路特性(前一个为 false 则不执行后一个)
console.log("" && "world"); // ""(""→false,短路返回"")
console.log("a" && "b"); // "b"("a"→true,返回后一个值)

4. 特殊场景:== 比较的隐式转换

== 是“松散相等”,会触发隐式转换后再比较;而 === 是“严格相等”,不转换类型,直接比较值和类型(推荐优先使用 === 避免坑)。

== 的核心规则

  • 若类型相同,直接比较值(同 ===)。
  • 若类型不同,按以下优先级转换:
    1. null == undefinedtrue(特殊规则,不转数字);
    2. 若一个是 string、一个是 number,将 string 转为 number 后比较;
    3. 若一个是 boolean,将 boolean 转为 numbertrue→1,false→0)后比较;
    4. 若一个是对象、一个是原始类型,先将对象转原始类型,再按上述规则比较。

示例(常见坑)

1
2
3
4
5
6
7
8
9
10
console.log(null == undefined); // true(特殊规则)
console.log(1 == "1"); // true("1"→1,1=1)
console.log(true == 1); // true(true→1,1=1)
console.log("0" == false); // true("0"→0,false→0,0=0)
console.log([] == false); // true([]→""→0,false→0,0=0)
console.log({} == true); // false({}→"[object Object]"→NaN,true→1,NaN≠1)

// === 严格比较(无转换)
console.log(1 === "1"); // false(类型不同)
console.log(null === undefined); // false(类型不同)

三、核心区别与注意事项

维度 强制转换(显式) 隐式转换(自动)
触发方式 显式调用 String()/Number() JS 自动触发(运算、比较等场景)
规则透明度 规则明确,易控制 规则依赖场景,易踩坑(如 "" == 0
推荐场景 需要明确转换结果时(如数据格式化) 简单场景(如 1 + "个"),复杂场景避免

避坑建议

  1. 比较值时 **优先使用 ===**,避免 == 的隐式转换坑;
  2. 字符串转数字时,若需提取部分数字用 parseInt(str, 10)(指定基数),若需严格转换用 Number()
  3. 避免依赖对象/数组的隐式转换(如 [] + {} 结果为 "[object Object]",逻辑不直观)。

36.== 和 === 的区别是什么?在什么情况下 == 会返回 true?

在 JavaScript 中,==(松散相等)和 ===(严格相等)都是用于比较两个值是否“相等”的运算符,但核心区别在于是否允许类型转换

一、===== 的核心区别

  • ===(严格相等)
    比较时不进行类型转换,只有当两个值的类型完全相同值严格相等时,才返回 true;否则返回 false

  • ==(松散相等)
    比较时允许类型转换(隐式转换),先将两个值转换为“相同类型”,再比较转换后的值是否相等。若转换后的值相等,返回 true;否则返回 false

二、== 返回 true 的常见情况

== 的比较逻辑依赖于类型转换规则,以下是典型场景:

1. 类型相同,且值相等(与 === 行为一致)

当两个值的类型相同,== 直接比较值是否相等,此时和 === 结果一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 数字类型
123 == 123; // true(123 === 123 也为 true)

// 字符串类型
"abc" == "abc"; // true("abc" === "abc" 也为 true)

// 布尔类型
true == true; // true(true === true 也为 true)

// 对象类型(比较引用地址)
const obj = { a: 1 };
obj == obj; // true(obj === obj 也为 true)
```

#### 2. 类型不同,但转换后值相等

当两个值类型不同时,`==` 会触发隐式转换,转换为相同类型后再比较。常见转换规则及示例:

##### (1)`null == undefined`(特殊规则)

`null` 和 `undefined` 是唯一一对“类型不同但 `==` 为 `true`”的特殊值(不进行类型转换,直接返回 `true`)。

```javascript
null == undefined; // true
null === undefined; // false(类型不同:null 是 Null 类型,undefined 是 Undefined 类型)
```

##### (2)字符串与数字比较(字符串转数字)

若一个值是字符串、另一个是数字,`==` 会先将字符串转换为数字,再比较。

```javascript
"123" == 123; // true("123" 转为数字 123,123 == 123)
"0" == 0; // true("0" 转为 0,0 == 0)
"123abc" == 123; // false("123abc" 转为 NaN,NaN 与任何值不相等)
```

##### (3)布尔值与其他类型比较(布尔值转数字)

若一个值是布尔值(`true`/`false`),`==` 会先将布尔值转换为数字(`true1`,`false0`),再与另一个值比较。

```javascript
true == 1; // true(true 转为 1,1 == 1)
false == 0; // true(false 转为 0,0 == 0)
true == "1"; // true(true→1,"1"→1,1 == 1)
false == ""; // true(false→0,""→0,0 == 0)
```

##### (4)对象与原始类型比较(对象转原始类型)

若一个值是对象(包括数组、函数等)、另一个是原始类型(字符串、数字、布尔),`==` 会先将对象转换为原始类型(通过 `valueOf()` 或 `toString()`),再比较。

```javascript
// 数组转原始类型(默认调用 toString())
[] == ""; // true([] 转为 "","" == "")
[123] == 123; // true([123] 转为 "123",再转为 123,123 == 123)
[1, 2] == "1,2"; // true([1,2] 转为 "1,2","1,2" == "1,2")

// 普通对象转原始类型(默认返回 "[object Object]")
{} == "[object Object]"; // true({} 转为 "[object Object]",字符串相等)
```

#### 3. 特殊值 `NaN` 的比较(永远为 `false`)

`NaN`(非数字)与任何值(包括自身)用 `==` 比较,结果都是 `false`(`===` 同样如此)。

```javascript
NaN == NaN; // false
NaN == 123; // false
```

### 三、总结

- **核心区别**:`===` 不允许类型转换(类型和值必须都相同);`==` 允许类型转换(先转同类型再比较值)。
- **`==` 返回 `true` 的情况**:
1. 类型相同且值相等;
2. 类型不同,但转换为相同类型后值相等(如 `1 == "1"`、`true == 1`);
3. 特殊情况:`null == undefined`。

实际开发中,**推荐优先使用 `===`**,避免因 `==` 的隐式转换导致难以预测的结果(如 `0 == ""`、`[] == false` 等反直觉情况)。

## 37.什么是 NaN?如何判断一个值是否为 NaN?

在 JavaScript 中,`NaN` 是一个特殊的数值类型值,核心作用是表示“**无效的数学运算结果**”或“**无法转换为数字的值**”,但它本身属于 `Number` 类型(而非“非数字类型”),这是其最容易混淆的特性之一。

### 一、什么是 NaN?

#### 1. 全称与本质

- **全称**:`NaN` 是 “Not a Number” 的缩写,但字面意思容易误导——它不是“非数字类型”,而是 **“数字类型中代表无效数值的值”**。
- 用 `typeof NaN` 检测会返回 `number`,证明其类型是 `Number`:

```javascript
typeof NaN; // "number" (关键特性,需重点注意)

2. NaN 的产生场景

NaN 通常在以下两种情况下出现:

  • 无效的数学运算:当运算结果无法用有效数字表示时,会返回 NaN

    1
    2
    3
    0 / 0; // NaN (0除以0无意义)
    Math.sqrt(-1); // NaN (负数开平方无实数解)
    Math.log(-10); // NaN (负数取对数无意义)
  • 类型转换失败:当试图将一个无法转为有效数字的非数字值(如字符串 "abc"、对象 {})转换为数字时,会返回 NaN

    1
    2
    3
    Number("abc"); // NaN (字符串"abc"无法转为数字)
    Number(undefined); // NaN (undefined转数字为NaN)
    parseInt("123abc"); // 123(注意:parseInt会“部分转换”,直到非数字字符;但Number()会整体转换,失败则NaN)

3. NaN 的关键特性

  • NaN 不等于任何值,包括它自身:这是 NaN 最核心的特性,也是判断它的重要依据。

    1
    2
    NaN == NaN; // false
    NaN === NaN; // false (严格相等也不成立)

二、如何判断一个值是否为 NaN?

由于 NaN 不与任何值相等(包括自身),直接用 ===== 判断会失效,必须使用专门的方法。以下是 3 种常见判断方式,需区分其适用场景:

1. 全局函数 isNaN()(不推荐,存在局限性)

  • 原理:先将传入的值强制转换为数字类型,再判断转换后的值是否为 NaN

  • 局限性:若传入的是非数字类型(如字符串 "abc"、对象 {}),会先被转为 NaN,导致 isNaN() 误判为 true(但这些值本身不是 NaN)。

    1
    2
    3
    4
    5
    6
    7
    8
    // 正确判断(值本身是NaN)
    isNaN(NaN); // true
    isNaN(0 / 0); // true

    // 误判(值本身不是NaN,但转换后是NaN)
    isNaN("abc"); // true ("abc"转数字为NaN,但"abc"不是NaN)
    isNaN({}); // true ({}转数字为NaN,但{}不是NaN)
    isNaN(true); // false (true转数字为1,不是NaN)
  • 适用场景:仅用于判断“某个值是否会被转换为NaN”,而非“值本身是否为NaN”。

2. ES6 新增的 Number.isNaN()(推荐,精准判断)

  • 原理:不进行类型转换,仅判断两个条件:

    1. 传入的值类型是 Number
    2. 值本身是 NaN
  • 优势:避免了全局 isNaN() 的误判问题,是判断“值本身是否为NaN”的最优方案

    1
    2
    3
    4
    5
    6
    7
    8
    // 正确判断(值是Number类型且为NaN)
    Number.isNaN(NaN); // true
    Number.isNaN(0 / 0); // true

    // 避免误判(非Number类型直接返回false)
    Number.isNaN("abc"); // false (类型是string,不是number)
    Number.isNaN({}); // false (类型是object,不是number)
    Number.isNaN(123); // false (是number,但值不是NaN)

3. 利用 NaN 不等于自身 的特性(巧妙判断)

由于 NaN 是唯一“不等于自身”的值,可通过 x !== x 来判断 x 是否为 NaN

1
2
3
4
5
6
7
function isNaNValue(x) {
return x !== x; // 仅当x是NaN时,返回true
}

isNaNValue(NaN); // true
isNaNValue("abc"); // false
isNaNValue(123); // false
  • 优势:无需依赖 ES6 特性,兼容性好(支持所有 JavaScript 环境),且逻辑简洁。

三、总结

判断方式 原理 优点 缺点 推荐度
全局 isNaN() 先转数字再判断 兼容性好 误判非数字类型(如”abc”) ⭐⭐
Number.isNaN() 不转类型,精准判断Number+NaN 无误差,逻辑清晰 依赖 ES6(需兼容旧环境时需 polyfill) ⭐⭐⭐⭐⭐
x !== x 利用NaN不相等自身的特性 兼容性极好,逻辑巧妙 可读性稍差(需理解原理) ⭐⭐⭐⭐

实际开发推荐

  • 若环境支持 ES6(如现代浏览器、Node.js),优先使用 Number.isNaN()
  • 若需兼容 IE 等旧环境,可使用 x !== x 或为 Number.isNaN() 添加 polyfill。

38.undefined 和 null 的区别是什么?它们的使用场景分别是什么?

在 JavaScript 中,undefinednull 均为原始值,但二者的含义、默认行为及使用场景有明确区别,核心差异在于:undefined 代表“被动未定义”(自然状态下的缺失),null 代表“主动空值”(开发者明确设置的空对象标识)。

一、undefined 与 null 的核心区别

我们从 定义含义、类型检测、默认值、类型转换 四个维度对比二者的差异:

对比维度 undefined null
核心含义 表示“未定义”:变量声明后未赋值、属性/参数缺失等“自然缺失”状态 表示“空值”:开发者主动赋值,明确表示“此处应为对象,但目前为空/不存在”
类型检测(typeof) typeof undefined === "undefined"(正确识别) typeof null === "object"(历史遗留 bug,实际是原始值)
默认值场景 会作为默认值(如未赋值的变量、未传的函数参数) 不会作为默认值,必须主动赋值才会出现
类型转换规则 - 转布尔:Boolean(undefined) → false
- 转数字:Number(undefined) → NaN
- 转布尔:Boolean(null) → false
- 转数字:Number(null) → 0
严格相等判断 undefined === null → false(类型不同) undefined == null → true(宽松相等下被视为“空值”等价)

关键补充:typeof null 的“历史 bug”

typeof null 返回 object 是 JavaScript 早期设计的遗留问题:
JS 最初用 32 位二进制表示值的类型,null 的二进制前三位为 000(与对象的类型标识一致),导致 typeof 误判。但从逻辑和标准上,null原始值,而非对象(可通过 Object.prototype.toString.call(null) 验证,返回 "[object Null]")。

二、undefined 的使用场景(被动未定义)

undefined 通常由 JavaScript 引擎“自动生成”,而非开发者主动赋值,常见场景包括:

  1. 变量声明后未赋值
    变量仅声明但未初始化时,默认值为 undefined

    1
    2
    let name;
    console.log(name); // undefined(引擎自动赋值)
  2. 对象的属性不存在
    访问对象中未定义的属性时,返回 undefined(而非报错):

    1
    2
    const user = { age: 20 };
    console.log(user.name); // undefined(属性 name 不存在)
  3. 函数参数未传递
    调用函数时,若未给某个参数传值,该参数的默认值为 undefined

    1
    2
    3
    4
    function greet(name) {
    console.log(name); // 未传参时,name 为 undefined
    }
    greet(); // undefined
  4. 函数无返回值
    函数未写 return 语句,或 return 后无内容时,默认返回 undefined

    1
    2
    3
    4
    5
    function add(a, b) {
    // 无 return
    }
    const result = add(1, 2);
    console.log(result); // undefined

三、null 的使用场景(主动空值)

null 是开发者主动赋值的结果,用于明确表示“此处应为对象,但当前为空/不存在”,常见场景包括:

  1. 初始化“待创建的对象”
    当变量计划存储对象(如从接口获取的数据、创建的实例),但初始时未就绪,可先赋值为 null(明确“空对象”语义,而非“未定义”):

    1
    2
    3
    let user = null; // 计划后续赋值为用户对象,当前为空
    // 后续从接口获取数据后赋值
    user = { name: "Alice", age: 20 };
  2. 函数返回“无结果的对象”
    当函数逻辑上应返回对象(如查找数据),但未找到目标时,主动返回 null(区分“无结果”和“错误”):

    1
    2
    3
    4
    5
    6
    7
    8
    // 查找 ID 为 100 的用户,未找到则返回 null
    function findUserById(id) {
    const users = [{ id: 1, name: "Bob" }];
    const target = users.find(u => u.id === id);
    return target || null; // 未找到时返回 null
    }
    const user = findUserById(100);
    console.log(user); // null(明确表示“无此用户”)
  3. 清空对象引用(帮助垃圾回收)
    当一个对象不再使用时,将其引用赋值为 null,可让 JS 垃圾回收机制(GC)识别并释放该对象占用的内存(避免内存泄漏):

    1
    2
    3
    let bigData = { /* 大量数据 */ };
    // 使用完 bigData 后,清空引用
    bigData = null; // GC 会回收原对象的内存
  4. 明确“空值”的属性语义
    对象属性若需明确表示“空”(而非“未定义”),可赋值为 null(例如表单中“用户主动清空的字段”):

    1
    2
    3
    4
    const formData = {
    username: "Alice",
    avatar: null // 明确表示“用户未上传头像”(而非“头像属性未定义”)
    };

四、常见误区与最佳实践

  1. 不要主动赋值 undefined
    若变量需表示“空”,优先用 null;主动写 let a = undefined 毫无意义(变量默认就是 undefined),还会混淆“被动未定义”的语义。

  2. 宽松相等(==)的注意事项
    undefined == null 会返回 true(JS 视二者为“空值”的两种形式),但严格不推荐依赖此特性;实际开发中应使用 ===(严格相等),明确区分类型和值。

  3. 判断空值的建议

    • 若需判断“变量是否未定义”:用 typeof x === "undefined"(避免直接 x === undefined,因 undefined 可被重定义,虽现代环境已禁止,但兼容性仍需注意)。
    • 若需判断“对象是否为空”:用 x === null(明确主动空值)。

总结

类型 核心语义 产生方式 典型场景
undefined 被动未定义(自然缺失) 引擎自动生成 未赋值变量、不存在的属性、未传参数
null 主动空值(明确空对象) 开发者主动赋值 待创建对象、无结果返回、清空引用

记住一句话:“undefined 是 JS 说‘我不知道这是什么’,null 是你说‘我知道这是空的’”

39.简述 JavaScript 中的函数参数的特性(如 arguments 对象、默认参数、剩余参数)

在 JavaScript 中,函数参数的设计非常灵活,支持多种特性来适配不同的传参需求,核心特性包括 arguments 对象默认参数剩余参数。这些特性分别解决了“获取所有实参”“处理参数缺省”“收集不定长实参”等问题,以下分点详解:

一、arguments 对象:类数组的实参集合

arguments 是一个类数组对象(Array-like),仅存在于非箭头函数中,用于存储函数调用时传递的所有实际参数(实参),无论形参是否声明。

1. 核心特性

  • 类数组本质:有 length 属性(表示实参个数),可通过索引(如 arguments[0])访问单个实参,但不具备数组的原型方法(如 forEachmap),需手动转换为数组(如 Array.from(arguments))。
  • 参数同步性(非严格模式):非严格模式下,修改 arguments 中元素的值,会同步修改对应形参的值(反之亦然);严格模式(’use strict’)下,这种同步关系被切断arguments 与形参独立。
  • 箭头函数无 arguments:箭头函数不绑定 arguments 对象,若需获取实参,需用“剩余参数”替代。

2. 示例与注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 示例1:非严格模式下的 arguments(参数同步)
function add(a, b) {
console.log(arguments.length); // 调用时传递的实参个数,如 add(1,2,3) 则为 3
arguments[0] = 10; // 修改 arguments[0]
console.log(a); // 非严格模式:10(同步修改);严格模式:1(无同步)
return a + b;
}
add(1, 2); // 非严格模式返回 12,严格模式返回 3

// 示例2:arguments 转数组(使用 Array.from 或 ... 扩展运算符)
function sum() {
const args = Array.from(arguments); // 转为真数组
return args.reduce((total, curr) => total + curr, 0);
}
sum(1, 2, 3, 4); // 10

// 注意:箭头函数中使用 arguments 会报错
const func = () => console.log(arguments);
func(); // ReferenceError: arguments is not defined

二、默认参数:参数缺省时的默认值

默认参数允许为函数形参设置默认值,当调用函数时该参数未传递(或传递为 undefined),则自动使用默认值,避免手动判断 undefined 的繁琐。

1. 核心特性

  • 生效条件:仅当实参为 undefined 时触发默认值(若传递 null0'' 等 falsy 值,默认值不生效)。
  • 默认值支持表达式:默认值可以是常量、变量、函数调用等表达式(表达式在函数每次调用时动态计算,而非定义时)。
  • 独立作用域:默认参数会形成一个单独的“参数作用域”,与函数体作用域隔离,避免变量污染。
  • 参数顺序:若有多个参数,默认参数应放在普通参数(无默认值)之后(否则未传递的普通参数会被解析为 undefined,可能不符合预期)。

2. 示例与注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 示例1:基础默认参数
function greet(name = 'Guest', age = 18) {
console.log(`Hello, ${name} (${age}岁)`);
}
greet(); // Hello, Guest (18岁)(两个参数均未传递,用默认值)
greet('Alice'); // Hello, Alice (18岁)(仅 age 用默认值)
greet('Bob', null); // Hello, Bob (null岁)(传递 null,默认值不生效)

// 示例2:默认值为表达式(函数调用)
function getDefaultTime() {
return new Date().getHours();
}
function logTime(hour = getDefaultTime()) {
console.log(`Current hour: ${hour}`);
}
logTime(); // 动态获取当前小时(每次调用都会重新执行 getDefaultTime)

// 示例3:默认参数的作用域(避免污染函数体)
const x = 10;
function func(a = x + 5) { // 参数作用域的 x 指向外部的 x
const x = 20; // 函数体作用域的 x,不影响参数默认值
console.log(a); // 15(而非 25)
}
func();

三、剩余参数:收集不定长实参为真数组

剩余参数(Rest Parameters)用 ...变量名 语法表示,用于收集函数调用时“剩余的实参”,并将其转换为一个真数组(可直接使用数组方法),是 arguments 对象的现代替代方案。

1. 核心特性

  • 真数组本质:剩余参数返回的是标准数组(而非类数组),可直接使用 forEachmapreduce 等数组方法,无需手动转换。
  • 位置限制:剩余参数必须是函数的最后一个形参(否则会报错),且一个函数只能有一个剩余参数。
  • 箭头函数支持:剩余参数可在箭头函数中使用,弥补了箭头函数无 arguments 的缺陷。
  • 与 arguments 的区别:剩余参数仅收集“未被前面形参匹配的实参”,而 arguments 收集所有实参;剩余参数是真数组,arguments 是类数组。

2. 示例与注意事项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 示例1:基础剩余参数(收集所有实参)
function sum(...nums) { // nums 是真数组
return nums.reduce((total, curr) => total + curr, 0);
}
sum(1, 2, 3); // 6
sum(5, 10, 15, 20); // 50

// 示例2:剩余参数 + 普通参数(剩余参数必须在最后)
function printUser(name, age, ...hobbies) {
console.log(`Name: ${name}, Age: ${age}`);
console.log('Hobbies:', hobbies); // hobbies 收集 name/age 之后的所有实参
}
printUser('Alice', 20, 'reading', 'coding', 'hiking');
// 输出:Hobbies: ["reading", "coding", "hiking"]

// 示例3:箭头函数中使用剩余参数
const multiply = (...args) => args.reduce((prod, curr) => prod * curr, 1);
multiply(2, 3, 4); // 24

// 注意:剩余参数不能在非最后位置,否则报错
function wrongFunc(...a, b) {} // SyntaxError: Rest parameter must be last formal parameter

四、特性对比与使用场景总结

特性 本质 核心优势 适用场景
arguments 对象 类数组 兼容老代码,获取所有实参 非严格模式的老项目,需兼容 ES5 及以下
默认参数 形参缺省值 简化参数缺省判断,代码更简洁 明确参数缺省时的默认行为(如分页默认页号)
剩余参数 真数组 处理不定长实参,支持数组方法 动态传参场景(如求和、批量处理数据)

最佳实践:现代 JavaScript 开发中,优先使用 默认参数剩余参数,减少对 arguments 的依赖(尤其是箭头函数场景),代码更易读、易维护。

40.箭头函数与普通函数有什么区别?(如 this 指向、arguments、构造函数)

箭头函数(Arrow Function)是 ES6 新增的函数语法,与传统的普通函数(Function)在 this 指向、语法结构、功能限制 等方面有显著区别,核心差异如下:

一、this 指向:箭头函数无独立 this,普通函数 this 动态绑定

这是两者最核心的区别:

  • 普通函数this 的指向是动态的,取决于函数的调用方式(谁调用它,this 就指向谁),具体规则:

    • 全局调用(非严格模式):this 指向全局对象(浏览器中是 window,Node.js 中是 global);
    • 作为对象方法调用:this 指向该对象;
    • new 调用(作为构造函数):this 指向新创建的实例;
    • call/apply/bind 调用:this 指向传入的第一个参数。
  • 箭头函数:没有自己的 this,它的 this继承自外层作用域的 this(定义时的上下文),且一旦确定就无法改变call/apply/bind 也无法修改)。

示例对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
// 普通函数:this 动态绑定
const obj1 = {
name: "普通函数",
fn: function() {
console.log(this.name); // 谁调用指向谁
}
};
obj1.fn(); // "普通函数"(obj1 调用,this 指向 obj1)
const fn1 = obj1.fn;
fn1(); // 全局调用,this 指向 window(非严格模式),输出 undefined


// 箭头函数:this 继承外层作用域
const obj2 = {
name: "箭头函数",
fn: () => {
console.log(this.name); // this 继承自 obj2 定义时的外层作用域(此处是 window)
}
};
obj2.fn(); // undefined(this 指向 window,window 无 name 属性)


// 箭头函数 this 不可修改(call/apply 无效)
const arrowFn = () => console.log(this);
arrowFn.call({ a: 1 }); // 仍指向 window(非严格模式),而非 {a:1}
```

### 二、`arguments` 对象:箭头函数无 `arguments`,普通函数有

- **普通函数**:内部有 `arguments` 对象(类数组),包含函数调用时传递的所有实参。
- **箭头函数**:内部**没有 `arguments` 对象**,若需获取实参,需使用 **剩余参数(`...args`)**。

**示例对比**:

```javascript
// 普通函数:有 arguments
function normalFunc() {
console.log(arguments); // 类数组:[1, 2, 3]
}
normalFunc(1, 2, 3);


// 箭头函数:无 arguments,用剩余参数替代
const arrowFunc = (...args) => {
console.log(args); // 真数组:[1, 2, 3](可直接用数组方法)
};
arrowFunc(1, 2, 3);

// 箭头函数中使用 arguments 会报错
const errorFunc = () => console.log(arguments);
errorFunc(); // ReferenceError: arguments is not defined
```

### 三、构造函数:箭头函数不能作为构造函数,普通函数可以

- **普通函数**:可以通过 `new` 关键字作为构造函数使用,会创建一个实例对象,`this` 指向该实例。
- **箭头函数**:**不能作为构造函数**,使用 `new` 调用会报错(因箭头函数没有 `prototype` 属性,也没有 `[[Construct]]` 内部方法)。

**示例对比**:

```javascript
// 普通函数:可作为构造函数
function Person(name) {
this.name = name;
}
const person = new Person("张三");
console.log(person.name); // "张三"(成功创建实例)


// 箭头函数:不能作为构造函数
const ArrowPerson = (name) => {
this.name = name;
};
const arrowPerson = new ArrowPerson("李四");
// TypeError: ArrowPerson is not a constructor(报错)
```

### 四、其他区别

1. **语法简洁性**:
箭头函数语法更简洁,可省略 `function` 关键字;若只有一个参数,可省略括号;若函数体只有一句 `return`,可省略大括号和 `return`。

```javascript
// 普通函数
function add(a, b) {
return a + b;
}

// 箭头函数(等价简写)
const add = (a, b) => a + b;
```

2. **原型(`prototype`)**:
普通函数有 `prototype` 属性(用于构造函数的原型链);箭头函数**没有 `prototype` 属性**。

```javascript
function normalFunc() {}
console.log(normalFunc.prototype); // { constructor: f }(有原型)

const arrowFunc = () => {};
console.log(arrowFunc.prototype); // undefined(无原型)
```

3. **`yield` 关键字**:
箭头函数不能作为 Generator 函数(无法使用 `yield` 关键字);普通函数可以。

### 总结:核心区别对照表

| 特性 | 普通函数 | 箭头函数 |
|---------------------|-------------------------------------------|-------------------------------------------|
| `this` 指向 | 动态绑定(取决于调用方式) | 继承外层作用域的 `this`(定义时确定,不可改) |
| `arguments` 对象 | 有(类数组) | 无(需用剩余参数 `...args`) |
| 构造函数 | 可以(`new` 调用有效) | 不可以(`new` 调用报错) |
| 原型(`prototype`) | 有 | 无 |
| `yield` 支持 | 可以(作为 Generator 函数) | 不可以 |
| 语法 | 需 `function` 关键字,结构较繁琐 | 省略 `function`,支持简写 |

### 适用场景

- **箭头函数**:适合作为**回调函数**(如 `setTimeout`、数组方法 `map`/`filter`),避免 `this` 指向混乱;不适合作为对象方法或构造函数。
- **普通函数**:适合需要**动态 `this`**(如对象方法)、作为**构造函数**、或需要 `arguments` 对象的场景。

## 41.什么是立即执行函数(IIFE)?它的作用是什么?

立即执行函数(Immediately Invoked Function Expression,简称 IIFE)是一种**在定义后立即被调用执行的函数表达式**。它的核心特点是:函数在声明的同时就会执行,且通常只执行一次。

### 一、IIFE 的基本语法

IIFE 的关键是将函数声明转换为**函数表达式**(避免语法错误),然后立即调用。常见写法有两种:

```javascript
// 写法1:用括号包裹函数表达式,再添加调用括号
(function() {
console.log("我是 IIFE,会立即执行");
})();

// 写法2:在函数前加运算符(!、+、- 等)将其转为表达式,再调用(效果相同)
!function() {
console.log("我也是 IIFE");
}();
```

- 括号 `()` 的作用:将函数声明(`function() {}`)转为函数表达式(避免 JavaScript 引擎将其解析为函数声明,因函数声明不能直接加 `()` 调用)。
- 最后的 `()`:表示立即调用该函数表达式。

### 二、IIFE 的核心作用

在 ES6 块级作用域(`let`/`const`)出现之前,IIFE 是 JavaScript 中**创建独立局部作用域**的主要方式,核心作用如下:

#### 1. 隔离变量,避免污染全局作用域

IIFE 会创建一个独立的函数作用域,内部声明的变量和函数不会泄露到全局作用域,有效避免全局变量冲突。

**示例**:

```javascript
// 不使用 IIFE:变量污染全局
let count = 0;
function increment() {
count++;
}
increment();
console.log(window.count); // 1(全局变量被污染)


// 使用 IIFE:变量隔离在局部作用域
(function() {
let count = 0; // 局部变量,全局无法访问
function increment() {
count++;
}
increment();
console.log(count); // 1(仅在 IIFE 内部有效)
})();

console.log(window.count); // undefined(全局未被污染)
```

#### 2. 保护私有变量,实现“模块化”雏形

IIFE 内部的变量和函数默认对外不可见(私有),但可通过返回对象/函数的方式暴露“接口”,实现类似模块化的封装效果(ES6 模块普及前的常用技巧)。

**示例**:

```javascript
const module = (function() {
// 私有变量(外部无法直接访问)
let username = "张三";

// 私有函数
function formatName(name) {
return `Mr. ${name}`;
}

// 暴露公共接口(外部仅能通过接口访问)
return {
getFormattedName: function() {
return formatName(username);
},
setUsername: function(newName) {
username = newName;
}
};
})();

// 外部通过接口访问,无法直接修改私有变量
console.log(module.getFormattedName()); // "Mr. 张三"
module.setUsername("李四");
console.log(module.getFormattedName()); // "Mr. 李四"
console.log(module.username); // undefined(私有变量无法直接访问)
```

#### 3. 立即执行初始化代码

对于页面加载时需要立即执行的逻辑(如数据初始化、DOM 操作),IIFE 可确保代码在定义后马上执行,无需手动调用。

**示例**:

```javascript
// 页面加载后立即初始化数据
(function() {
const userData = localStorage.getItem("user");
if (userData) {
console.log("初始化用户数据:", JSON.parse(userData));
} else {
console.log("无用户数据,初始化默认值");
}
})();
```

#### 4. 捕获即时值(解决闭包中的变量延迟绑定问题)

在循环中使用闭包时(如事件绑定),变量可能因延迟绑定导致值不符合预期,IIFE 可捕获循环时的即时值。

**示例**(解决循环闭包问题):

```javascript
// 问题:循环中的闭包延迟绑定,所有按钮点击后都输出 3
for (var i = 0; i < 3; i++) {
document.getElementById(`btn${i}`).onclick = function() {
console.log(i); // 点击任何按钮都输出 3(i 最终为 3)
};
}

// 解决:用 IIFE 捕获即时的 i 值
for (var i = 0; i < 3; i++) {
(function(currentI) { // currentI 是 IIFE 的参数,捕获当前 i 值
document.getElementById(`btn${i}`).onclick = function() {
console.log(currentI); // 点击 btn0 输出 0,btn1 输出 1,以此类推
};
})(i); // 传入当前 i 值
}
```

### 三、IIFE 的现代替代方案

ES6 引入 `let`/`const`(块级作用域)和模块系统后,IIFE 的部分作用被替代:

- 块级作用域:`{ let x = 1; }` 可替代 IIFE 的变量隔离功能;
- 模块系统:`import`/`export` 更规范地实现模块化,替代 IIFE 的封装逻辑。

但 IIFE 仍在一些场景中有用(如兼容旧环境、需要立即执行的简单逻辑),且理解 IIFE 有助于掌握 JavaScript 的作用域和闭包特性。

### 总结

IIFE 是“定义后立即执行的函数表达式”,核心价值在于**创建独立作用域(避免全局污染)** 和**封装私有变量**,是 ES6 之前 JavaScript 模块化和作用域管理的重要工具。其语法简洁,通过 `(function(){})()` 形式实现,至今仍有其适用场景。

## 42.简述 JavaScript 中的模块化发展(如 CommonJS、AMD、ES6 Module)

JavaScript 中的模块化发展是为了解决**代码复用、依赖管理、全局变量污染**等问题,随着应用规模扩大,从早期的“无模块”状态逐步演进到标准化方案。主要经历了 **CommonJS、AMD、UMD** 到 **ES6 Module** 的过程,各阶段的方案针对不同场景(服务器端/浏览器端)设计,核心差异体现在**加载机制、语法规范**和**适用环境**上。

### 一、早期无模块时代(2009年前)

在模块化规范出现前,JavaScript 没有官方的模块系统,开发者通过以下方式模拟“模块”:

- **文件拆分**:将代码按功能拆分成多个 `.js` 文件,通过 `<script>` 标签依次引入。
- **命名空间**:用对象包裹函数/变量(如 `const moduleA = { fn: () => {} }`),减少全局变量。

**问题**:

- 依赖顺序完全由 `<script>` 标签顺序决定,一旦顺序错误就会报错;
- 所有变量共享全局作用域,容易出现命名冲突;
- 无法声明模块间的显式依赖关系,代码维护性差。

### 二、CommonJS(2009年,服务器端优先)

CommonJS 是 Node.js 推动的模块化规范,主要为**服务器端场景**设计(Node.js 需处理大量本地文件模块)。

#### 核心特点

1. **语法**:
- 用 `require(模块路径)` 加载模块;
- 用 `module.exports` 或 `exports` 导出模块内容。

```javascript
// 导出模块(math.js)
function add(a, b) { return a + b; }
module.exports = { add };

// 导入模块(app.js)
const math = require('./math.js');
console.log(math.add(1, 2)); // 3
```

2. **加载机制**:
- **同步加载**:模块加载时会阻塞后续代码执行(适合服务器端,因模块存于本地磁盘,加载速度快);
- **运行时加载**:加载的是模块的“值的拷贝”(修改导入的变量不会影响原模块);
- **动态路径**:`require` 可接收动态生成的路径(如 `require('./' + filename)`)。

3. **适用场景**:
主要用于 Node.js 服务器端,浏览器端无法直接使用(同步加载会导致页面卡顿)。

### 三、AMD(2010年,浏览器端异步加载)

由于 CommonJS 的同步加载不适合浏览器(模块需从网络请求,同步加载会阻塞渲染),AMD(Asynchronous Module Definition,异步模块定义)应运而生,专为**浏览器端**设计,代表实现是 RequireJS。

#### 核心特点

1. **语法**:
- 用 `define(id?, dependencies?, factory)` 定义模块(`dependencies` 是依赖的模块数组);
- 用 `require(dependencies, callback)` 加载模块。

```javascript
// 定义模块(math.js)
define([], function() {
return {
add: function(a, b) { return a + b; }
};
});

// 加载模块(主文件)
require(['./math.js'], function(math) {
console.log(math.add(1, 2)); // 3
});
```

2. **加载机制**:
- **异步加载**:模块加载不会阻塞后续代码,依赖加载完成后执行回调函数;
- **依赖前置**:定义模块时需显式声明所有依赖(提前加载)。

3. **优缺点**:
- 解决了浏览器端模块异步加载问题;
- 语法较繁琐,且随着前端工程化发展,逐步被更优方案替代。

### 四、UMD(通用模块定义,跨环境兼容)

UMD(Universal Module Definition)不是独立规范,而是**兼容 CommonJS 和 AMD 的通用方案**,让模块可在浏览器和 Node.js 中同时运行,主要用于第三方库(如 jQuery、Lodash)的发布。

#### 核心逻辑

通过判断环境(是否存在 `exports` 或 `define`),自动适配不同模块规范:

```javascript
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 环境
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS 环境(Node.js)
module.exports = factory();
} else {
// 全局环境(无模块系统)
root.math = factory();
}
})(this, function() {
// 模块内容
return { add: (a, b) => a + b };
});
```

### 五、ES6 Module(ESM,2015年,官方标准化)

ES6(2015)引入了官方模块化规范 **ES6 Module**,旨在统一浏览器和服务器端的模块系统,解决之前规范的碎片化问题,是目前前端模块化的主流方案。

#### 核心特点

1. **语法**:
- 用 `export` 导出模块(`export const a = 1` 或 `export default`);
- 用 `import` 导入模块(`import { a } from './module.js'` 或 `import * as mod from './module.js'`)。

```javascript
// 导出模块(math.js)
export const add = (a, b) => a + b;
export default 10; // 默认导出

// 导入模块(app.js)
import { add } from './math.js';
import num from './math.js';
console.log(add(1, 2)); // 3
console.log(num); // 10
```

2. **加载机制**:
- **静态加载**:编译时(代码执行前)解析模块依赖,支持“树摇(Tree-shaking)”(删除未使用的代码,减小打包体积);
- **异步加载**:浏览器端通过 `<script type="module">` 加载,默认异步执行,不阻塞页面;
- **值的引用**:导入的是模块的“值的引用”(原模块修改后,导入方会同步更新);
- **路径固定**:`import` 的路径必须是静态字符串(不能动态生成,如 `import('./' + path)` 需用 `import()` 动态导入)。

3. **适用场景**:
现代前端工程化(Webpack、Vite 等工具均支持)和 Node.js(需显式指定 `.mjs` 后缀或 `package.json` 中 `type: "module"`)。

### 发展总结与对比

| 规范 | 出现时间 | 核心场景 | 加载方式 | 语法关键词 | 核心优势 |
|------------|----------|------------|----------|---------------------|---------------------------|
| CommonJS | 2009 | Node.js 服务器 | 同步 | `require`/`module.exports` | 适合本地模块,语法简单 |
| AMD | 2010 | 浏览器端 | 异步 | `define`/`require` | 解决浏览器异步加载问题 |
| UMD | 2011 | 跨环境库发布 | 适配环境 | 无固定关键词 | 兼容 CommonJS/AMD/全局环境 |
| ES6 Module | 2015 | 浏览器+Node.js | 静态异步 | `import`/`export` | 官方标准,支持树摇,统一生态 |

**演进逻辑**:从解决单一环境(服务器/浏览器)的模块化问题,到通过官方标准(ES6 Module)实现全环境统一,体现了 JavaScript 模块化从碎片化到标准化的过程。目前,ES6 Module 已成为主流,CommonJS 仍在 Node.js 生态中广泛使用,AMD 和 UMD 逐渐退出历史舞台。

## 43.ES6 Module 的 import 和 export 有哪些用法?(如默认导出、命名导出、动态 import)

ES6 Module(ESM)通过 `export` 和 `import` 关键字实现模块的导出与导入,支持多种灵活的用法,核心可分为 **命名导出/导入**、**默认导出/导入** 和 **动态导入** 三大类。以下是具体用法及示例:

### 一、`export`:模块导出

`export` 用于将模块内的变量、函数、类等成员暴露给外部,分为 **命名导出(Named Export)** 和 **默认导出(Default Export)** 两种方式。一个模块可以同时包含命名导出和默认导出。

#### 1. 命名导出(Named Export)

用于导出多个成员,每个成员都有明确的名称,外部导入时需使用对应的名称(可重命名)。

##### 用法1:直接导出声明

在声明变量、函数、类时直接添加 `export`:

```javascript
// math.js(命名导出)
export const pi = 3.14; // 导出变量

export function add(a, b) { // 导出函数
return a + b;
}

export class Calculator { // 导出类
multiply(a, b) {
return a * b;
}
}
```

##### 用法2:集中导出(推荐,更清晰)

先声明成员,再通过 `export` 集中导出(适合导出多个成员时保持代码整洁):

```javascript
// math.js(集中命名导出)
const pi = 3.14;
function add(a, b) { return a + b; }
class Calculator { /* ... */ }

// 集中导出(对象形式,键值对,键名可省略,默认与变量名一致)
export { pi, add, Calculator };
```

##### 用法3:导出时重命名(`as`)

若需修改导出的名称(避免冲突),可用 `as` 重命名:

```javascript
// math.js
const max = 100;
export { max as maximum }; // 导出时将 max 重命名为 maximum
```

#### 2. 默认导出(Default Export)

每个模块**只能有一个默认导出**,通常用于导出模块的“主要内容”(如一个核心函数、类)。外部导入时可自定义名称,无需与导出名称一致。

##### 用法1:直接导出默认成员

```javascript
// utils.js(默认导出函数)
export default function formatDate(date) {
return date.toLocaleString();
}
```

##### 用法2:先声明后导出默认成员

```javascript
// user.js(默认导出对象)
const user = { name: "Alice", age: 20 };
export default user; // 默认导出对象
```

##### 用法3:默认导出类

```javascript
// Person.js(默认导出类)
class Person {
constructor(name) {
this.name = name;
}
}
export default Person; // 默认导出类
```

#### 3. 混合导出(命名导出 + 默认导出)

一个模块可同时包含命名导出和默认导出:

```javascript
// api.js
// 命名导出(辅助函数)
export function log(message) {
console.log(message);
}

// 默认导出(核心功能)
export default function fetchData(url) {
return fetch(url);
}
```

### 二、`import`:模块导入

`import` 用于导入其他模块导出的成员,需与对应模块的导出方式匹配(命名导出对应命名导入,默认导出对应默认导入)。

#### 1. 导入命名导出的成员(`{}` 语法)

导入命名导出的成员时,需用 `{ 成员名 }` 包裹,名称必须与导出时一致(可通过 `as` 重命名)。

##### 基本用法

```javascript
// 导入 math.js 中的命名导出成员
import { pi, add, Calculator } from './math.js';

console.log(pi); // 3.14
console.log(add(1, 2)); // 3
const calc = new Calculator();
console.log(calc.multiply(2, 3)); // 6
```

##### 重命名导入(`as`)

若导入的成员名称与当前模块冲突,可用 `as` 重命名:

```javascript
import { pi as circlePi, add } from './math.js';
console.log(circlePi); // 3.14(使用重命名后的名称)
```

#### 2. 导入默认导出的成员(无 `{}`)

导入默认导出的成员时,无需用 `{}`,可自定义名称(与导出时的名称无关):

```javascript
// 导入 utils.js 的默认导出(函数)
import format from './utils.js'; // 自定义名称为 format
console.log(format(new Date())); // 调用默认导出的函数

// 导入 user.js 的默认导出(对象)
import userInfo from './user.js'; // 自定义名称为 userInfo
console.log(userInfo.name); // "Alice"

// 导入 Person.js 的默认导出(类)
import PersonClass from './Person.js';
const person = new PersonClass("Bob");
```

#### 3. 同时导入命名导出和默认导出

对于混合导出的模块,可同时导入命名成员和默认成员:

```javascript
// 导入 api.js 的默认导出和命名导出
import fetchData, { log } from './api.js';
// 注意:默认导出在前,命名导出在后(顺序固定)

log("开始请求数据"); // 调用命名导出的 log
fetchData("https://api.example.com"); // 调用默认导出的 fetchData
```

#### 4. 整体导入(`* as`)

将模块的所有导出(包括命名导出和默认导出)整体导入为一个对象,通过对象属性访问成员:

```javascript
// 整体导入 math.js 所有成员
import * as MathModule from './math.js';

console.log(MathModule.pi); // 3.14(访问命名导出)
console.log(MathModule.add(1, 2)); // 3(访问命名导出的函数)
const calc = new MathModule.Calculator(); // 访问命名导出的类
```

默认导出的成员会被放在整体对象的 `default` 属性中:

```javascript
// 整体导入 utils.js(默认导出函数)
import * as UtilsModule from './utils.js';
UtilsModule.default(new Date()); // 调用默认导出的函数(通过 .default 访问)
```

### 三、动态导入(`import()`)

ES2020 引入的动态导入(`import()`)允许在**运行时动态加载模块**,返回一个 `Promise`,适合“按需加载”场景(如路由懒加载、条件加载)。

#### 特点

- 可在非模块文件中使用(无需 `type="module"`);
- 路径可动态生成(支持变量);
- 加载完成后通过 `then` 或 `await` 获取模块内容。

#### 用法示例

```javascript
// 1. 基本用法(then 语法)
import('./math.js').then((module) => {
console.log(module.add(1, 2)); // 3(访问命名导出)
});

// 2. 结合 async/await(更简洁)
async function loadModule() {
const math = await import('./math.js');
console.log(math.pi); // 3.14
}
loadModule();

// 3. 动态路径(根据条件加载不同模块)
const moduleName = 'math';
import(`./${moduleName}.js`).then((module) => {
// 加载 ./math.js
});

// 4. 导入默认导出(通过 .default 访问)
import('./utils.js').then((module) => {
module.default(new Date()); // 调用默认导出的函数
});
```

### 四、注意事项

1. **路径规范**:`import` 的路径必须是**相对路径**(`./`、`../`)或**绝对路径**,不能省略文件后缀(浏览器环境中);
2. **静态解析**:非动态的 `import` 必须放在模块顶部(不能在条件语句、函数内),因 ESM 是“静态加载”(编译时解析依赖);
3. **跨域限制**:浏览器中通过 `<script type="module">` 加载的模块受同源策略限制,跨域模块需服务器配置 CORS;
4. **活绑定**:ESM 的导入是“活的绑定”——若原模块修改了导出的变量,导入方会同步更新(与 CommonJS 的“值拷贝”不同):

```javascript
// module.js
export let count = 0;
export function increment() { count++; }

// app.js
import { count, increment } from './module.js';
console.log(count); // 0
increment();
console.log(count); // 1(同步更新)
```

### 总结

| 导出方式 | 语法示例 | 导入方式 | 适用场景 |
|----------------|-----------------------------------|-----------------------------------|------------------------------|
| 命名导出 | `export const a = 1;` 或 `export { a, b };` | `import { a, b } from './m.js';` | 导出多个独立成员 |
| 默认导出 | `export default function() {}` | `import func from './m.js';` | 导出模块的核心/唯一成员 |
| 动态导入 | 无(导入语法) | `import('./m.js').then(...)` | 按需加载、动态路径加载 |

ES6 Module 的导入导出机制通过明确的语法区分命名和默认成员,兼顾了灵活性和规范性,是现代前端工程化(如 Webpack、Vite)的基础。

## 44.什么是防抖(Debounce)和节流(Throttle)?它们的区别是什么?如何实现?

防抖(Debounce)和节流(Throttle)是 JavaScript 中用于**控制函数执行频率**的两种优化技术,主要解决高频触发事件(如滚动、输入框输入、窗口 resize 等)导致的性能问题(如频繁 DOM 操作、网络请求)。二者的核心目标是减少不必要的函数调用,但实现逻辑和适用场景不同。

### 一、概念与核心区别

#### 1. 防抖(Debounce)

**核心逻辑**:当事件被高频触发时,**只在最后一次触发后等待指定时间再执行函数**。如果在等待期间事件再次触发,则重新计时。

- 效果:多次连续触发 → 合并为**一次执行**(最后一次触发后延迟执行)。

**示例**:搜索框输入时,用户连续输入字符,不会每次输入都发送请求,而是等待用户停止输入(比如停顿 500ms)后,才执行搜索请求。

#### 2. 节流(Throttle)

**核心逻辑**:规定一个**时间间隔**,在这个间隔内,无论事件触发多少次,函数**最多只执行一次**。

- 效果:多次连续触发 → 按固定间隔**周期性执行**(如每隔 100ms 执行一次)。

**示例**:滚动页面时,需要实时计算滚动位置,但无需每次滚动都计算,而是每隔 200ms 计算一次,既保证实时性,又避免性能浪费。

#### 核心区别

| 特性 | 防抖(Debounce) | 节流(Throttle) |
|--------------|---------------------------------|---------------------------------|
| 执行时机 | 最后一次触发后延迟执行 | 固定时间间隔内执行一次 |
| 连续触发效果 | 合并为一次执行 | 按间隔周期性执行 |
| 核心目标 | 等待“稳定期”后执行 | 控制单位时间内的执行次数 |

### 二、实现方法

#### 1. 防抖(Debounce)实现

利用 `setTimeout` 延迟执行函数,每次触发事件时清除之前的定时器,重新计时。

```javascript
/**
* 防抖函数
* @param {Function} func - 需要防抖的函数
* @param {number} wait - 等待时间(ms)
* @param {boolean} immediate - 是否立即执行(true:触发时立即执行,后续等待;false:等待后执行)
* @returns {Function} 防抖处理后的函数
*/
function debounce(func, wait, immediate = false) {
let timeout = null; // 定时器标识

return function(...args) {
const context = this; // 保留原函数的this指向

// 每次触发时清除之前的定时器
if (timeout) clearTimeout(timeout);

// 立即执行版本:第一次触发时立即执行,之后等待
if (immediate) {
// 如果定时器不存在,说明是第一次触发或已超过等待时间
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null; // 等待结束后重置定时器
}, wait);
if (callNow) func.apply(context, args);
} else {
// 延迟执行版本:最后一次触发后等待wait时间执行
timeout = setTimeout(() => {
func.apply(context, args); // 执行函数,绑定this和参数
}, wait);
}
};
}
```

**使用示例**:

```javascript
// 模拟搜索请求函数
function search(keyword) {
console.log(`搜索:${keyword}`);
}

// 对搜索函数进行防抖处理(等待500ms,停止输入后执行)
const debouncedSearch = debounce(search, 500);

// 输入框输入事件(高频触发)
input.addEventListener('input', (e) => {
debouncedSearch(e.target.value); // 只有停止输入500ms后才执行搜索
});
```

#### 2. 节流(Throttle)实现

常见有两种方式:**时间戳法**和**定时器法**,核心是控制两次执行的时间间隔不小于指定值。

##### 方法1:时间戳法(立即执行)

记录上次执行时间,每次触发时判断当前时间与上次时间的差是否大于间隔,若大于则执行。

```javascript
/**
* 节流函数(时间戳法)
* @param {Function} func - 需要节流的函数
* @param {number} interval - 时间间隔(ms)
* @returns {Function} 节流处理后的函数
*/
function throttleTimestamp(func, interval) {
let lastTime = 0; // 上次执行时间

return function(...args) {
const context = this;
const now = Date.now(); // 当前时间

// 若当前时间 - 上次执行时间 >= 间隔,则执行
if (now - lastTime >= interval) {
func.apply(context, args);
lastTime = now; // 更新上次执行时间
}
};
}
```

##### 方法2:定时器法(延迟执行)

设置定时器,函数执行后清除定时器,确保间隔内只执行一次。

```javascript
/**
* 节流函数(定时器法)
* @param {Function} func - 需要节流的函数
* @param {number} interval - 时间间隔(ms)
* @returns {Function} 节流处理后的函数
*/
function throttleTimer(func, interval) {
let timeout = null; // 定时器标识

return function(...args) {
const context = this;

// 若定时器不存在,说明可以执行
if (!timeout) {
timeout = setTimeout(() => {
func.apply(context, args);
timeout = null; // 执行后清除定时器,允许下次执行
}, interval);
}
};
}
```

##### 方法3:优化版(结合时间戳和定时器)

解决前两种方法的缺陷(时间戳法可能丢失最后一次触发,定时器法可能延迟执行),确保:

- 首次触发立即执行;
- 最后一次触发后,若未达间隔,仍会执行一次。

```javascript
function throttle(func, interval) {
let lastTime = 0;
let timeout = null;

return function(...args) {
const context = this;
const now = Date.now();

// 清除未执行的定时器(避免重复)
if (timeout) clearTimeout(timeout);

// 若达到间隔,立即执行
if (now - lastTime >= interval) {
func.apply(context, args);
lastTime = now;
} else {
// 未达间隔,设置定时器确保最后一次触发能执行
timeout = setTimeout(() => {
func.apply(context, args);
lastTime = Date.now();
timeout = null;
}, interval - (now - lastTime));
}
};
}
```

**使用示例**:

```javascript
// 模拟滚动时计算位置的函数
function handleScroll() {
console.log('滚动位置:', window.scrollY);
}

// 对滚动处理函数节流(每隔200ms执行一次)
const throttledScroll = throttle(handleScroll, 200);

// 滚动事件(高频触发)
window.addEventListener('scroll', throttledScroll); // 每隔200ms输出一次位置
```

### 三、适用场景

- **防抖**:适合需要“等待用户操作结束后再执行”的场景,如:
- 搜索框输入联想(停止输入后请求);
- 窗口 resize 事件(调整结束后重新布局);
- 按钮点击防重复提交(短时间内多次点击只执行最后一次)。

- **节流**:适合需要“周期性执行”的场景,如:
- 滚动加载(每隔一段时间判断是否触底);
- 高频点击按钮(限制每秒最多点击一次);
- 鼠标移动跟踪(每隔100ms更新位置)。

### 总结

- 防抖:**合并多次触发为最后一次执行**,核心是“等待稳定期”。
- 节流:**控制单位时间内的执行次数**,核心是“固定间隔执行”。
- 实现均依赖定时器或时间戳,根据场景选择合适的方案(如防抖的立即执行版、节流的优化版)。

## 45.简述 JavaScript 中的 JSON 对象的常用方法(JSON.parse、JSON.stringify)及注意事项

在 JavaScript 中,`JSON` 对象是一个内置对象,提供了两个核心方法用于处理 JSON 格式数据:`JSON.parse()`(解析 JSON 字符串)和 `JSON.stringify()`(序列化 JavaScript 对象为 JSON 字符串)。这两个方法是前后端数据交互、本地存储(如 `localStorage`)的基础工具。

### 一、`JSON.parse()`:解析 JSON 字符串为 JavaScript 对象

`JSON.parse()` 用于将符合 JSON 格式的字符串转换为对应的 JavaScript 对象(或基本类型值)。

#### 语法

```javascript
JSON.parse(text[, reviver])
```

#### 参数

- `text`:必选,需要解析的 **JSON 格式字符串**(必须符合严格的 JSON 语法)。
- `reviver`:可选,一个函数,用于在解析过程中“过滤”或“转换”结果。解析后的每个键值对会先经过该函数处理,再返回最终结果。

#### 基本用法

```javascript
// 解析 JSON 字符串为对象
const jsonStr = '{"name":"Alice","age":20,"isStudent":true}';
const obj = JSON.parse(jsonStr);
console.log(obj); // { name: "Alice", age: 20, isStudent: true }
console.log(obj.name); // "Alice"(已转换为 JavaScript 对象)

// 解析 JSON 字符串为数组
const arrStr = '[1, 2, "three", null]';
const arr = JSON.parse(arrStr);
console.log(arr); // [1, 2, "three", null]
```

#### 进阶用法:`reviver` 函数

`reviver` 函数接收两个参数 `(key, value)`,可对解析后的键值对进行加工(如类型转换、过滤属性):

```javascript
const jsonStr = '{"name":"Bob","birth":"2000-01-01","age":23}';

// 使用 reviver 将日期字符串转换为 Date 对象
const obj = JSON.parse(jsonStr, (key, value) => {
if (key === 'birth') {
return new Date(value); // 将 "2000-01-01" 转为 Date 对象
}
return value; // 其他属性保持不变
});

console.log(obj.birth); // 2000-01-01T00:00:00.000Z(Date 对象)
```

若 `reviver` 返回 `undefined`,则对应的属性会被**过滤掉**:

```javascript
const obj = JSON.parse(jsonStr, (key, value) => {
if (key === 'age') return undefined; // 过滤 age 属性
return value;
});
console.log(obj); // { name: "Bob", birth: "2000-01-01" }(无 age 属性)
```

### 二、`JSON.stringify()`:序列化 JavaScript 对象为 JSON 字符串

`JSON.stringify()` 用于将 JavaScript 对象(或基本类型值)转换为符合 JSON 格式的字符串。

#### 语法

```javascript
JSON.stringify(value[, replacer[, space]])
```

#### 参数

- `value`:必选,需要序列化的 JavaScript 值(对象、数组、基本类型等)。
- `replacer`:可选,用于控制序列化过程中哪些属性被包含,以及如何转换属性值。可以是**函数**(类似 `reviver`)或**数组**(仅保留数组中指定的属性名)。
- `space`:可选,用于格式化输出的缩进空格数(或字符串,如 `'\t'`),使 JSON 字符串更易读(默认无缩进)。

#### 基本用法

```javascript
// 序列化对象
const obj = { name: "Alice", age: 20, isStudent: true };
const jsonStr = JSON.stringify(obj);
console.log(jsonStr); // '{"name":"Alice","age":20,"isStudent":true}'(JSON 字符串)

// 序列化数组
const arr = [1, 2, "three", null];
console.log(JSON.stringify(arr)); // '[1,2,"three",null]'
```

#### 进阶用法

1. **`replacer` 函数**:控制属性的序列化逻辑

```javascript
const user = { name: "Bob", password: "123456", age: 23 };

// 使用 replacer 过滤敏感属性(如 password)
const jsonStr = JSON.stringify(user, (key, value) => {
if (key === 'password') return undefined; // 不序列化 password
return value;
});
console.log(jsonStr); // '{"name":"Bob","age":23}'
```

2. **`replacer` 数组**:仅保留指定属性

```javascript
const user = { name: "Bob", age: 23, gender: "male" };
// 只序列化 name 和 age 属性
const jsonStr = JSON.stringify(user, ['name', 'age']);
console.log(jsonStr); // '{"name":"Bob","age":23}'
```

3. **`space` 参数**:格式化输出(便于阅读)

```javascript
const obj = { a: 1, b: { c: 2 } };
// 缩进 2 个空格
const prettyStr = JSON.stringify(obj, null, 2);
console.log(prettyStr);
// 输出:
// {
// "a": 1,
// "b": {
// "c": 2
// }
// }
```

### 三、注意事项(核心坑点)

#### 1. `JSON.parse()` 的注意事项

- **JSON 格式严格性**:传入的字符串必须符合严格的 JSON 语法,否则会抛出 `SyntaxError`:
- 属性名必须用**双引号**(单引号无效);
- 字符串值必须用双引号(`'hello'` 无效,必须是 `"hello"`);
- 不能有**末尾逗号**(`{ "a": 1, }` 无效);
- 不能包含 `undefined`、函数、`Symbol` 等 JSON 不支持的类型。

```javascript
// 错误示例:属性名用单引号
JSON.parse("{'name': 'Alice'}"); // SyntaxError: 意外的标记 '

// 错误示例:末尾逗号
JSON.parse('{"a": 1,}'); // SyntaxError: 位置 6 处的 JSON 中的意外令牌 }
```

- **日期解析**:JSON 中没有“日期类型”,日期对象会被序列化为 ISO 字符串(如 `"2023-01-01T00:00:00.000Z"`),解析后仍为字符串,需手动转换为 `Date` 对象(可通过 `reviver` 函数自动处理)。

#### 2. `JSON.stringify()` 的注意事项

- **不支持的类型**:以下类型在序列化时会被特殊处理:
- `undefined`、函数、`Symbol`:作为对象属性时会被**忽略**;作为数组元素时会被转为 `null`;单独序列化时返回 `undefined`。

```javascript
console.log(JSON.stringify(undefined)); // undefined
console.log(JSON.stringify([1, undefined, 3])); // '[1,null,3]'
console.log(JSON.stringify({ a: 1, b: undefined })); // '{"a":1}'
```

- `BigInt`:会直接抛出 `TypeError`(JSON 不支持 BigInt)。

```javascript
JSON.stringify(123n); // TypeError: Do not know how to serialize a BigInt
```

- 循环引用的对象:会抛出 `TypeError`(JSON 不支持循环引用)。

```javascript
const obj = {};
obj.self = obj; // 循环引用
JSON.stringify(obj); // TypeError: Converting circular structure to JSON
```

- **特殊值处理**:
- `NaN`、`Infinity`、`-Infinity` 会被转为 `null`;
- `null`、布尔值、数字、字符串会正常序列化;
- 对象的**不可枚举属性**、`Symbol` 作为键名的属性会被忽略。

```javascript
console.log(JSON.stringify(NaN)); // 'null'
console.log(JSON.stringify(Infinity)); // 'null'
```

- **`Date` 对象**:会被序列化为 ISO 字符串(`date.toISOString()` 的结果),但解析后需手动转回 `Date` 对象。

```javascript
const date = new Date();
console.log(JSON.stringify(date)); // '"2023-10-01T08:00:00.000Z"'(字符串)
```

### 总结

| 方法 | 作用 | 核心参数 | 注意事项 |
|-----------------|-------------------------------|-----------------------------------|-------------------------------------------|
| `JSON.parse()` | 解析 JSON 字符串为 JS 对象 | `text`(JSON 字符串)、`reviver`(处理函数) | 严格校验 JSON 格式,日期需手动转类型 |
| `JSON.stringify()` | 序列化 JS 对象为 JSON 字符串 | `value`(JS 值)、`replacer`(过滤/转换)、`space`(格式化) | 不支持 undefined/函数/循环引用,特殊值转 null |

这两个方法是 JavaScript 处理 JSON 数据的核心工具,掌握其用法和限制(尤其是格式严格性和特殊类型处理)能有效避免数据交互中的常见错误。

## 46.如何判断一个对象是否为空对象(不包含可枚举属性)?

判断一个对象是否为空对象(即不包含任何**自身可枚举属性**),需要结合对象的属性特性和类型检查,常见方法如下:

### 一、核心判断逻辑

“空对象”的定义:

- 是**普通对象**(排除数组、函数、`null` 等特殊类型);
- 没有**自身可枚举属性**(不考虑原型链上的属性,也不考虑不可枚举属性)。

### 二、常用判断方法

#### 1. `Object.keys()` 方法(推荐)

`Object.keys(obj)` 返回对象**自身可枚举属性**组成的数组(不包含原型链属性和不可枚举属性)。若数组长度为 `0`,则为“空对象”。

**步骤**:

- 先判断 `obj` 是普通对象(排除 `null`、数组、函数等);
- 再通过 `Object.keys(obj).length === 0` 判断是否无自身可枚举属性。

```javascript
function isEmptyObject(obj) {
// 排除非对象类型(如 null、undefined、基本类型)
if (typeof obj !== 'object' || obj === null) {
return false;
}
// 排除数组、日期等特殊对象(仅判断普通对象)
if (Object.prototype.toString.call(obj) !== '[object Object]') {
return false;
}
// 检查自身可枚举属性数量
return Object.keys(obj).length === 0;
}

// 测试
console.log(isEmptyObject({})); // true(空对象)
console.log(isEmptyObject({ a: 1 })); // false(有属性)
console.log(isEmptyObject([])); // false(是数组,非普通对象)
console.log(isEmptyObject(null)); // false(非对象)
console.log(isEmptyObject(Object.create({ a: 1 }))); // true(原型链有属性,但自身无)
```

#### 2. `JSON.stringify()` 方法(有局限性)

`JSON.stringify(obj)` 会将对象序列化为 JSON 字符串,若对象是“无自身可枚举属性的普通对象”,结果为 `"{}"`。

**注意**:

- 不适合包含不可枚举属性、`Symbol` 键、`Date` 对象等场景(这些会被忽略或特殊处理);
- 无法区分普通对象和其他空对象(如空数组 `[]` 序列化后为 `"[]"`,但空对象为 `"{}"`)。

```javascript
function isEmptyObjectByStringify(obj) {
return typeof obj === 'object' && obj !== null && JSON.stringify(obj) === '{}';
}

// 测试
console.log(isEmptyObjectByStringify({})); // true
console.log(isEmptyObjectByStringify({ a: 1 })); // false
console.log(isEmptyObjectByStringify(Object.create(null))); // true(空对象,无原型)
console.log(isEmptyObjectByStringify({ [Symbol('key')]: 1 })); // true(Symbol 键会被忽略)
```

#### 3. `for...in` 循环(传统方法)

`for...in` 会遍历对象的**自身及原型链上的可枚举属性**,配合 `hasOwnProperty` 可仅检查自身属性。若遍历中无自身可枚举属性,则为“空对象”。

```javascript
function isEmptyObjectByLoop(obj) {
// 先判断是否为普通对象
if (typeof obj !== 'object' || obj === null || Object.prototype.toString.call(obj) !== '[object Object]') {
return false;
}
// 遍历自身可枚举属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) { // 只关注自身属性
return false; // 有自身可枚举属性,非空
}
}
return true; // 无自身可枚举属性,为空
}

// 测试
console.log(isEmptyObjectByLoop({})); // true
console.log(isEmptyObjectByLoop({ a: 1 })); // false
console.log(isEmptyObjectByLoop(Object.create({ b: 2 }))); // true(原型链属性不影响)
```

### 三、注意事项

1. **排除非普通对象**:
- 数组(`[]`)、日期(`new Date()`)、正则(`/reg/`)等虽然是对象,但通常不视为“空对象”,需通过 `Object.prototype.toString.call(obj) === '[object Object]'` 严格判断是否为普通对象。
- `null` 的 `typeof` 为 `'object'`,需单独排除(`obj === null`)。

2. **不考虑不可枚举属性**:
上述方法均忽略不可枚举属性(如 `Object.defineProperty(obj, 'a', { enumerable: false })` 定义的属性),若需判断“是否无任何自身属性(包括不可枚举)”,需使用 `Object.getOwnPropertyNames(obj).length === 0 && Object.getOwnPropertySymbols(obj).length === 0`。

3. **Symbol 键的处理**:
`Object.keys()` 和 `for...in` 均不包含 `Symbol` 类型的键,若对象有可枚举的 `Symbol` 键(如 `obj[Symbol('key')] = 1`),上述方法会误判为“空对象”。如需包含 `Symbol` 键,需额外检查:

```javascript
function isEmptyObjectWithSymbol(obj) {
// 检查字符串键 + Symbol 键
const hasStringKeys = Object.keys(obj).length > 0;
const hasSymbolKeys = Object.getOwnPropertySymbols(obj).some(sym => obj.propertyIsEnumerable(sym));
return !hasStringKeys && !hasSymbolKeys;
}
```

### 总结

- **推荐方法**:`Object.keys()` 结合类型判断(简洁且可靠,覆盖大多数场景)。
- **核心逻辑**:确保是普通对象,且无自身可枚举属性(字符串键)。
- **特殊需求**:若需考虑 `Symbol` 键或不可枚举属性,需扩展判断逻辑。

## 47.简述 JavaScript 中的 for 循环的几种形式(for、for...in、for...of、forEach)及区别

在 JavaScript 中,`for` 循环是遍历数据(如数组、对象)的核心工具,常见形式包括 **普通 `for` 循环**、**`for...in`**、**`for...of`** 和 **`forEach`**。它们的语法、适用场景和特性存在显著差异,以下将逐一解析并对比。

## 一、四种循环形式的语法与核心特性

### 1. 普通 `for` 循环(基础可控型)

普通 `for` 循环是最传统的遍历方式,通过**索引/条件控制**循环过程,灵活性最高,适用于需要精确控制遍历顺序、步长或中断的场景。

#### 语法

```javascript
for (初始化表达式; 条件判断表达式; 更新表达式) {
// 循环体(满足条件时执行)
}
  • 初始化表达式:循环前执行一次(如声明索引变量 let i = 0)。
  • 条件判断表达式:每次循环前判断,true 则执行循环体,false 则退出。
  • 更新表达式:每次循环体执行后更新变量(如 i++,控制步长)。

示例(遍历数组)

1
2
3
4
5
6
7
8
9
10
const arr = [10, 20, 30];
// 正序遍历
for (let i = 0; i < arr.length; i++) {
console.log(`索引 ${i}${arr[i]}`); // 输出:索引0:10,索引1:20,索引2:30
}

// 倒序遍历(灵活控制顺序)
for (let i = arr.length - 1; i >= 0; i--) {
console.log(`索引 ${i}${arr[i]}`); // 输出:索引2:30,索引1:20,索引0:10
}

核心特性

  • 适用对象:数组(通过索引访问)、类数组对象(如 arguments)。
  • 支持中断:可通过 break(退出整个循环)、continue(跳过当前迭代)控制。
  • 灵活性:可自定义步长(如 i += 2 隔一个遍历)、遍历范围。

2. for...in 循环(对象属性遍历型)

for...in 专为遍历对象属性设计,会遍历对象的自身可枚举属性原型链上的可枚举属性(需注意过滤原型属性),不建议用于遍历数组。

语法

1
2
3
for (let 键名 in 对象) {
// 循环体(键名为字符串类型,如对象属性名、数组索引)
}

示例(遍历对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = { name: "Alice", age: 20 };
// 遍历对象自身属性(需过滤原型链属性)
for (let key in obj) {
// hasOwnProperty() 确保只处理对象自身属性,排除原型链属性
if (obj.hasOwnProperty(key)) {
console.log(`键:${key},值:${obj[key]}`); // 输出:键:name,值:Alice;键:age,值:20
}
}

// 不建议遍历数组:索引是字符串,且可能遍历原型属性
const arr = [10, 20];
Array.prototype.customProp = "原型属性"; // 给数组原型添加属性
for (let i in arr) {
console.log(i); // 输出:0、1、customProp(意外遍历到原型属性)
}

核心特性

  • 适用对象:普通对象(优先)、数组(不推荐)。
  • 获取内容:遍历的是键名(对象属性名、数组索引,均为字符串类型),需通过 对象[键名] 获取值。
  • 原型链问题:会遍历原型链上的可枚举属性,必须用 obj.hasOwnProperty(key) 过滤。
  • 支持中断:可通过 break/continue 中断循环。

3. for...of 循环(可迭代对象遍历型)

for...of 是 ES6 新增的循环,专为遍历可迭代对象(Iterable)设计,直接获取元素值,不遍历原型链,是遍历数组、集合等的优选方案。

可迭代对象范围

包括:数组、字符串、SetMapGenerator、类数组对象(如 NodeList)等(普通对象默认不可迭代,需手动部署 Iterator 接口才能用)。

语法

1
2
3
for (let 元素值 of 可迭代对象) {
// 循环体
}

示例(遍历数组/Set/字符串)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 遍历数组(直接获取值,无索引问题)
const arr = [10, 20, 30];
for (let val of arr) {
console.log(val); // 输出:10、20、30
}

// 2. 遍历 Set(自动去重,获取值)
const set = new Set([1, 2, 2, 3]);
for (let val of set) {
console.log(val); // 输出:1、2、3
}

// 3. 遍历字符串(获取每个字符)
const str = "hello";
for (let char of str) {
console.log(char); // 输出:h、e、l、l、o
}

// 4. 遍历 Map(需解构获取键值对)
const map = new Map([["name", "Bob"], ["age", 22]]);
for (let [key, val] of map) {
console.log(`${key}: ${val}`); // 输出:name: Bob;age: 22
}

核心特性

  • 适用对象:可迭代对象(数组、SetMap 等,普通对象不直接支持)。
  • 获取内容:直接遍历元素值(无需通过键名间接获取),Map 需解构获取键值对。
  • 原型链安全:仅遍历对象自身的迭代内容,不涉及原型链属性。
  • 支持中断:可通过 break/continue 中断循环。

4. forEach 方法(数组专属遍历型)

forEach 是数组的实例方法,专为数组全量遍历设计,语法简洁,但无法中断循环,无返回值。

语法

1
2
3
数组.forEach((当前元素值, 当前索引, 原数组) => {
// 循环体(回调函数,每次遍历执行)
}, thisArg); // thisArg(可选):指定回调函数中的 this 指向

示例(遍历数组)

1
2
3
4
5
6
7
8
9
10
11
12
const arr = [10, 20, 30];
// 基础用法
arr.forEach((val, index, arr) => {
console.log(`索引 ${index}${val}(原数组:${arr})`);
// 输出:索引0:10(原数组:10,20,30);索引1:20(原数组:10,20,30);索引2:30(原数组:10,20,30)
});

// 指定 this 指向(需用普通函数,箭头函数不绑定 this)
const obj = { prefix: "值:" };
arr.forEach(function(val) {
console.log(this.prefix + val); // 输出:值:10;值:20;值:30
}, obj); // this 指向 obj

核心特性

  • 适用对象:仅数组(非数组需先转为数组,如 [...NodeList].forEach(...))。
  • 无返回值:回调函数的 return 仅退出当前迭代,不影响整体循环,且 forEach 最终返回 undefined
  • 不可中断:无法用 break/continue 中断循环(即使抛出异常也不推荐)。
  • 遍历全量:必须遍历数组所有元素,无法跳过范围(除非在回调中用条件判断跳过当前执行)。

二、四种循环的核心区别对比

为了更清晰区分,以下从 6 个关键维度整理对比表:

对比维度 普通 for 循环 for...in for...of forEach
适用对象 数组、类数组 普通对象(优先) 可迭代对象(数组/Set等) 仅数组
获取内容 索引 → 元素(arr[i] 键名(字符串)→ 值 直接获取元素值 元素值、索引(回调参数)
原型链遍历 不涉及 会遍历(需 hasOwnProperty 过滤) 不遍历 不遍历
中断支持 支持(break/continue 支持 支持 不支持
返回值 无(需手动维护结果) 固定返回 undefined
特殊注意事项 需手动控制索引/步长 数组索引为字符串,不推荐遍历数组 普通对象默认不可用 无法跳过循环,回调 this 需注意绑定

三、适用场景总结

  1. 普通 for 循环
    需精确控制遍历(如倒序、自定义步长)、或需中断循环的数组/类数组场景(如“找到目标元素后退出”)。

  2. for...in 循环
    仅用于遍历普通对象的自身可枚举属性(如获取对象的所有键名),必须搭配 hasOwnProperty 过滤原型属性,绝对不用于数组

  3. for...of 循环
    遍历数组、SetMap 等可迭代对象,且可能需要中断循环的场景(如“遍历数组时遇到负数则退出”),语法简洁且安全。

  4. forEach 方法
    数组的全量无中断遍历(如批量修改数组元素、批量打印),语法简洁,无需关心索引控制。

通过以上对比,可根据数据类型(对象/数组/集合)和需求(是否中断、是否控制索引)选择最合适的循环方式。

48.什么是柯里化(Currying)?它的作用是什么?如何实现函数柯里化?

柯里化(Currying)是函数式编程中的一种技术,核心是将接受多个参数的函数转换为一系列只接受单个参数(或部分参数)的函数,使得函数可以分阶段接收参数,最终在参数齐全时执行。

一、柯里化的基本概念

普通函数调用:fn(a, b, c)(一次性传入所有参数)
柯里化后调用:fn(a)(b)(c)(分三次传入参数,每次传入部分参数)

例如,一个计算三数之和的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
// 普通函数
function sum(a, b, c) {
return a + b + c;
}
sum(1, 2, 3); // 6

// 柯里化后
function curriedSum(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
curriedSum(1)(2)(3); // 6(分三次传入参数)
```

### 二、柯里化的核心作用

1. **参数复用**:固定部分频繁使用的参数,生成新的函数,减少重复传参。
例如,计算不同商品在固定税率下的税费:

```javascript
// 普通函数:每次都要传税率(假设税率固定为0.1)
function calculateTax(price, taxRate) {
return price * taxRate;
}
calculateTax(100, 0.1); // 10
calculateTax(200, 0.1); // 20(重复传0.1)

// 柯里化:固定税率,生成新函数
function curriedTax(taxRate) {
return function(price) {
return price * taxRate;
};
}
const calculateWithTax01 = curriedTax(0.1); // 固定税率0.1
calculateWithTax01(100); // 10
calculateWithTax01(200); // 20(无需重复传税率)
```

2. **延迟执行**:将函数的执行推迟到所有参数都传入后,支持分阶段处理参数。
例如,事件监听中需要分步骤收集参数:

```javascript
// 柯里化函数:分阶段传入事件类型、选择器、处理函数
function on(eventType) {
return function(selector) {
return function(handler) {
document.querySelector(selector).addEventListener(eventType, handler);
};
};
}

// 分阶段调用(先指定事件类型,再指定元素,最后指定处理函数)
const onClick = on('click'); // 第一步:固定事件类型为click
const onClickButton = onClick('#btn'); // 第二步:固定元素为#btn
onClickButton(() => console.log('按钮被点击')); // 第三步:传入处理函数(执行绑定)
```

3. **函数组合的基础**:柯里化后的函数更易与其他函数组合,构建复杂逻辑(函数式编程核心思想)。
例如,通过柯里化实现函数的“管道(pipe)”组合:

```javascript
// 柯里化的加法和乘法函数
const add = a => b => a + b;
const multiply = a => b => a * b;

// 组合函数:先加2,再乘3
const add2Multiply3 = x => multiply(3)(add(2)(x));
add2Multiply3(4); // (4+2)*3 = 18
```

### 三、如何实现通用柯里化函数

手动为每个函数写柯里化版本效率低,可实现一个**通用柯里化工具函数**,自动将任意多参数函数转换为柯里化函数。

核心逻辑:

- 收集每次传入的参数;
- 若参数总数达到原函数的参数长度(`fn.length`),则执行原函数;
- 否则返回一个新函数,继续收集剩余参数。

#### 通用柯里化实现代码

```javascript
/**
* 通用柯里化函数
* @param {Function} fn - 需要柯里化的原函数
* @returns {Function} 柯里化后的函数
*/
function curry(fn) {
// 保存原函数的参数长度(fn.length 即形参数量)
const fnArgLength = fn.length;

// 递归收集参数的函数
function collectArgs(...args) {
// 情况1:已收集的参数数量 >= 原函数所需参数数量 → 执行原函数
if (args.length >= fnArgLength) {
return fn.apply(this, args); // 绑定this,确保原函数的this正确
}

// 情况2:参数不足 → 返回新函数,继续收集参数
return function(...nextArgs) {
// 合并已有参数和新参数,递归调用collectArgs
return collectArgs.apply(this, [...args, ...nextArgs]);
};
}

return collectArgs; // 返回柯里化入口函数
}
```

#### 使用示例

```javascript
// 1. 柯里化多参数函数
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);

// 测试:分多次传参
console.log(curriedSum(1)(2)(3)); // 6(三次传参)
console.log(curriedSum(1, 2)(3)); // 6(前两次传2个参数,第三次传1个)
console.log(curriedSum(1)(2, 3)); // 6(第一次传1个,第二次传2个)


// 2. 柯里化带this的函数
const obj = {
name: "Alice",
greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
};

// 柯里化greet方法(注意绑定this)
const curriedGreet = curry(obj.greet.bind(obj));
console.log(curriedGreet("Hello")("!")); // "Hello, Alice!"
```

### 四、注意事项

1. **参数长度问题**:`fn.length` 仅统计“首个默认参数前的形参数量”,若原函数有默认参数,可能导致柯里化判断不准确。
例如:

```javascript
function func(a, b = 1, c) { return a + b + c; }
console.log(func.length); // 1(默认参数b之后的c不计入length)
const curriedFunc = curry(func);
curriedFunc(2)(3); // 2 + 1 + 3 = 6(实际只需2个参数,因b有默认值)
```

2. **剩余参数**:若原函数使用剩余参数(`...args`),`fn.length` 为 0,柯里化会立即执行(需特殊处理)。

3. **this绑定**:柯里化过程中需注意原函数的`this`指向,必要时用`bind`固定。

### 总结

- **柯里化**:将多参数函数转为分阶段接收参数的函数,形式为 `fn(a)(b)(c)...`。
- **核心作用**:参数复用、延迟执行、支持函数组合。
- **实现方式**:通过递归收集参数,达到原函数参数长度时执行,否则继续返回新函数。

柯里化是函数式编程的重要技巧,能让代码更灵活、复用性更高,尤其适合处理多参数场景下的逻辑拆分。

## 49.简述 JavaScript 中的生成器函数(Generator)的概念及使用方式(yield、next 方法)

生成器函数(Generator Function)是 ES6 引入的一种特殊函数,它的核心特性是**可暂停执行、可恢复执行**,并能通过迭代器逐步返回多个值(而非普通函数的一次性返回)。这种“分步执行”的能力让它在处理惰性序列、异步操作流程控制等场景中非常灵活。

### 一、生成器函数的基本概念

#### 1. 声明方式

生成器函数通过 `function*`(函数名前加 `*`)声明,与普通函数的区别在于:

- 执行时**不直接运行函数体**,而是返回一个**迭代器对象**(Generator 迭代器);
- 函数体内通过 `yield` 关键字控制暂停,每次调用迭代器的 `next()` 方法会恢复执行,直到下一个 `yield` 或函数结束。

#### 2. 核心特性

- **暂停与恢复**:`yield` 是“暂停点”,执行到 `yield` 时函数暂停,保存当前上下文(变量状态、执行位置);
- **分步返回值**:每次暂停时,通过 `yield` 返回一个值,最终通过迭代器的 `next()` 方法获取;
- **迭代器协议**:生成器返回的迭代器对象遵循迭代器协议(有 `next()` 方法,返回 `{ value: any, done: boolean }`)。

### 二、核心语法与使用方式

#### 1. `yield` 关键字

`yield` 只能在生成器函数内部使用,作用是:

- 暂停函数执行;
- 返回一个包含 `value`(当前 yield 后的值)和 `done`(是否完成,此时为 `false`)的对象;
- 等待 `next()` 方法调用以恢复执行。

**示例**:

```javascript
// 声明生成器函数
function* generatorDemo() {
console.log('开始执行');
yield '第一个值'; // 第一次暂停,返回 '第一个值'

console.log('继续执行');
yield '第二个值'; // 第二次暂停,返回 '第二个值'

console.log('执行结束');
return '最终值'; // 函数结束,返回最终值(done 变为 true)
}
```

#### 2. 迭代器的 `next()` 方法

生成器函数执行后返回的迭代器对象,通过 `next()` 方法控制生成器的执行:

- 第一次调用 `next()`:启动生成器,执行到第一个 `yield` 处暂停,返回 `{ value: '第一个值', done: false }`;
- 第二次调用 `next()`:从暂停处恢复执行,到下一个 `yield` 处暂停,返回 `{ value: '第二个值', done: false }`;
- 第三次调用 `next()`:从暂停处恢复执行,直到函数结束,返回 `{ value: '最终值', done: true }`;
- 后续调用 `next()`:始终返回 `{ value: undefined, done: true }`。

**示例**(执行生成器):

```javascript
// 获取迭代器
const iterator = generatorDemo();

// 第一次调用 next()
console.log(iterator.next());
// 输出:
// 开始执行
// { value: '第一个值', done: false }

// 第二次调用 next()
console.log(iterator.next());
// 输出:
// 继续执行
// { value: '第二个值', done: false }

// 第三次调用 next()
console.log(iterator.next());
// 输出:
// 执行结束
// { value: '最终值', done: true }

// 第四次调用 next()
console.log(iterator.next());
// 输出:{ value: undefined, done: true }
```

#### 3. `next()` 方法传递参数

`next()` 可以接收一个参数,该参数会作为**上一个 `yield` 表达式的返回值**。这允许外部向生成器内部传递数据,实现“双向通信”。

**示例**:

```javascript
function* dataReceiver() {
console.log('准备接收数据');
const data1 = yield; // 暂停,等待 next() 传递参数(data1 会接收该参数)
console.log('收到数据1:', data1);

const data2 = yield; // 再次暂停
console.log('收到数据2:', data2);
}

const iterator = dataReceiver();

// 第一次 next() 无意义参数(因第一个 yield 前无暂停点)
iterator.next('无效参数'); // 输出:准备接收数据 → 返回 { value: undefined, done: false }

// 第二次 next() 传递参数,作为第一个 yield 的返回值
iterator.next('Hello'); // 输出:收到数据1:Hello → 返回 { value: undefined, done: false }

// 第三次 next() 传递参数,作为第二个 yield 的返回值
iterator.next('World'); // 输出:收到数据2:World → 返回 { value: undefined, done: true }
```

#### 4. 其他常用方法

- **`return(value)`**:提前结束生成器,返回 `{ value: 传入值, done: true }`,后续 `next()` 均返回 `{ value: undefined, done: true }`。

```javascript
const iterator = generatorDemo();
iterator.next(); // { value: '第一个值', done: false }
console.log(iterator.return('提前结束')); // { value: '提前结束', done: true }
iterator.next(); // { value: undefined, done: true }
```

- **`throw(error)`**:向生成器内部抛出错误,若未捕获则终止生成器。

```javascript
function* errorHandler() {
try {
yield '正常执行';
} catch (e) {
console.log('捕获错误:', e);
}
}
const iterator = errorHandler();
iterator.next(); // { value: '正常执行', done: false }
iterator.throw(new Error('出错了')); // 输出:捕获错误:Error: 出错了 → { value: undefined, done: true }
```

### 三、典型应用场景

1. **惰性生成序列**:按需生成数据(如无限序列、大数组分片),避免一次性占用过多内存。

```javascript
// 生成无限自然数序列
function* naturalNumbers() {
let n = 1;
while (true) {
yield n++;
}
}
const numbers = naturalNumbers();
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3(按需生成)
```

2. **异步操作流程控制**:在 `async/await` 出现前,生成器常被用于简化异步代码(通过 `yield` 等待异步结果)。

```javascript
// 模拟异步请求
function fetchData(url) {
return new Promise(resolve => {
setTimeout(() => resolve(`数据:${url}`), 1000);
});
}

// 生成器控制异步流程
function* asyncFlow() {
const data1 = yield fetchData('url1');
console.log(data1); // 1秒后输出:数据:url1
const data2 = yield fetchData('url2');
console.log(data2); // 再1秒后输出:数据:url2
}

// 执行生成器(需手动驱动)
const iterator = asyncFlow();
iterator.next().value.then(data1 => {
iterator.next(data1).value.then(data2 => {
iterator.next(data2);
});
});
```

### 总结

- **生成器函数**:通过 `function*` 声明,执行返回迭代器,支持暂停/恢复执行。
- **`yield`**:暂停函数,返回当前值,等待 `next()` 恢复。
- **`next()`**:恢复执行,可传递参数作为上一个 `yield` 的返回值,返回 `{ value, done }`。

生成器的核心价值在于“可控的分步执行”,适用于惰性序列、异步流程等需要精细控制执行过程的场景。

## 50.什么是 Proxy 和 Reflect?它们的作用是什么?举例说明 Proxy 的使用场景

在 JavaScript 中,`Proxy` 和 `Reflect` 是 ES6 引入的两个密切相关的特性,它们共同为对象操作提供了更强大的拦截、定制和默认行为控制能力。

### 一、Proxy(代理)

`Proxy` 用于**创建一个对象的代理**,通过定义“拦截器”(陷阱)来拦截并自定义对象的基本操作(如属性访问、赋值、删除、函数调用等)。其核心作用是**在对象的默认行为之上添加自定义逻辑**,而无需修改对象本身。

#### 1. 基本语法

```javascript
const proxy = new Proxy(target, handler);
```

- `target`:被代理的目标对象(可以是对象、数组、函数等)。
- `handler`:拦截器对象,包含一系列“陷阱方法”(如 `get` 拦截属性访问、`set` 拦截属性赋值等),当对代理对象执行对应操作时,会触发这些方法。

#### 2. 常用陷阱方法

`handler` 支持多种陷阱方法,覆盖对象的大部分操作,常用的包括:

- `get(target, prop, receiver)`:拦截对象属性的读取(`proxy.prop` 或 `proxy[prop]`)。
- `set(target, prop, value, receiver)`:拦截对象属性的赋值(`proxy.prop = value`)。
- `has(target, prop)`:拦截 `in` 操作符(`prop in proxy`)。
- `deleteProperty(target, prop)`:拦截 `delete` 操作(`delete proxy.prop`)。
- `apply(target, thisArg, args)`:拦截函数调用(当目标是函数时,`proxy(...args)`)。

### 二、Reflect(反射)

`Reflect` 是一个内置对象,提供了一系列与对象操作相关的**静态方法**,这些方法与 `Proxy` 的陷阱方法一一对应(如 `Reflect.get` 对应 `handler.get`)。其核心作用是:

1. **以函数形式执行对象的默认操作**:替代传统的对象操作语法(如 `obj[prop]` 可写成 `Reflect.get(obj, prop)`)。
2. **配合 Proxy 使用**:在 Proxy 的陷阱方法中,通过 `Reflect` 调用目标对象的默认行为,确保拦截操作不破坏原有的逻辑。
3. **返回操作结果**:`Reflect` 方法会返回操作是否成功的布尔值(如 `Reflect.set` 成功赋值返回 `true`),便于错误处理。

#### 常用 Reflect 方法(与 Proxy 陷阱对应)

- `Reflect.get(target, prop, receiver)`:获取对象属性的默认值(对应 `get` 陷阱)。
- `Reflect.set(target, prop, value, receiver)`:设置对象属性的默认值(对应 `set` 陷阱)。
- `Reflect.has(target, prop)`:判断属性是否存在(对应 `has` 陷阱)。
- `Reflect.deleteProperty(target, prop)`:删除属性(对应 `deleteProperty` 陷阱)。

### 三、Proxy 与 Reflect 的配合使用

`Proxy` 用于拦截操作并添加自定义逻辑,`Reflect` 用于在拦截中执行默认行为,两者结合可以在不破坏原有功能的前提下扩展对象的能力。

**示例**:拦截对象属性赋值,添加数据校验,并通过 `Reflect` 执行默认赋值:

```javascript
const target = { age: 18 };

// 创建代理
const proxy = new Proxy(target, {
// 拦截属性赋值
set(target, prop, value, receiver) {
// 自定义逻辑:校验 age 必须是正数
if (prop === 'age' && (typeof value !== 'number' || value <= 0)) {
throw new Error('年龄必须是正数');
}
// 通过 Reflect.set 执行默认赋值行为
return Reflect.set(target, prop, value, receiver);
},

// 拦截属性读取
get(target, prop, receiver) {
console.log(`读取属性 ${prop}`); // 自定义日志逻辑
return Reflect.get(target, prop, receiver); // 执行默认读取
}
});

// 使用代理
proxy.age = 20; // 正常赋值(触发 set 陷阱,通过校验)
console.log(proxy.age); // 读取属性 age → 20(触发 get 陷阱)

proxy.age = -5; // 抛出错误:年龄必须是正数(校验失败)
```

### 四、Proxy 的典型使用场景

#### 1. 数据响应式(框架核心)

实现数据变化时自动触发 UI 更新(如 Vue 3 的响应式系统核心就是 `Proxy`)。

```javascript
// 简单的响应式实现
function reactive(target) {
return new Proxy(target, {
set(target, prop, value) {
const result = Reflect.set(target, prop, value);
// 数据变化时触发更新
console.log(`属性 ${prop} 变化,触发 UI 更新`);
return result;
}
});
}

const data = reactive({ name: 'Alice' });
data.name = 'Bob'; // 输出:属性 name 变化,触发 UI 更新(自动更新UI)
```

#### 2. 数据校验与过滤

对对象的属性赋值进行校验,确保数据符合预期格式。

```javascript
const user = { username: '', password: '' };

const userProxy = new Proxy(user, {
set(target, prop, value) {
if (prop === 'username') {
if (typeof value !== 'string' || value.length < 3) {
throw new Error('用户名必须至少3个字符');
}
}
if (prop === 'password') {
if (!/^(?=.*\d)/.test(value)) {
throw new Error('密码必须包含数字');
}
}
return Reflect.set(target, prop, value);
}
});

userProxy.username = 'Al'; // 抛出错误(长度不足)
userProxy.password = 'abc'; // 抛出错误(无数字)
userProxy.username = 'Alice'; // 成功
userProxy.password = 'abc123'; // 成功
```

#### 3. 日志记录与监控

拦截对象的所有操作,记录访问日志(如调试、审计场景)。

```javascript
function withLogging(target, name) {
return new Proxy(target, {
get(target, prop) {
console.log(`[日志] 访问 ${name}.${prop}`);
return Reflect.get(target, prop);
},
set(target, prop, value) {
console.log(`[日志] 修改 ${name}.${prop}${value}`);
return Reflect.set(target, prop, value);
}
});
}

const obj = withLogging({ a: 1 }, '测试对象');
obj.a; // [日志] 访问 测试对象.a
obj.a = 2; // [日志] 修改 测试对象.a 为 2
```

#### 4. 只读对象

禁止对对象进行任何修改(比 `Object.freeze` 更灵活,可自定义拦截提示)。

```javascript
function readonly(target) {
return new Proxy(target, {
set() {
throw new Error('禁止修改只读对象');
},
deleteProperty() {
throw new Error('禁止删除只读对象的属性');
}
});
}

const config = readonly({ url: 'https://api.example.com' });
config.url = 'new url'; // 抛出错误:禁止修改只读对象
delete config.url; // 抛出错误:禁止删除只读对象的属性

5. 函数参数劫持

拦截函数调用,对参数进行预处理(如类型转换、默认值填充)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function withParamCheck(fn) {
return new Proxy(fn, {
apply(target, thisArg, args) {
// 确保参数是数字
const validArgs = args.map(arg => {
if (typeof arg !== 'number') return Number(arg) || 0;
return arg;
});
return Reflect.apply(target, thisArg, validArgs);
}
});
}

// 原函数:计算两数之和
function add(a, b) {
return a + b;
}

const safeAdd = withParamCheck(add);
console.log(safeAdd('10', 20)); // 30(字符串'10'被转为数字10)
console.log(safeAdd(null, 5)); // 5(null转为0)

总结

  • Proxy:通过拦截对象操作(属性访问、赋值等),实现自定义逻辑(校验、监控、响应式等),是对象行为扩展的核心工具。
  • Reflect:提供与 Proxy 陷阱对应的默认操作方法,用于在拦截中保持原对象的默认行为,简化代码并增强兼容性。
  • 两者配合使用,既能灵活扩展对象功能,又不破坏原生逻辑,广泛应用于框架开发、数据处理、权限控制等场景。