三_JavaScript_基础
本文将详细讲解JavaScript的基础语法,包括变量、数据类型、运算符、表达式等内容,适合初学者阅读。
1.JavaScript 的数据类型有哪些?基本数据类型和引用数据类型的区别是什么?
一、数据类型
- 基本数据类型:Number、String、Boolean、Null、Undefined、Symbol(ES6)、BigInt(ES2020)。
- 引用数据类型:Object(含Array、Function、Date、RegExp等)。
二、核心区别
| 维度 | 基本数据类型 | 引用数据类型 |
|---|---|---|
| 存储 | 栈内存直接存值 | 栈存地址,堆内存存值 |
| 赋值 | 值拷贝,变量独立 | 地址拷贝,指向同一对象 |
| 比较 | 比较值是否相等 | 比较引用地址是否相同 |
| 可变性 | 不可变(修改生成新值) | 可变(直接修改内部属性) |
2.如何判断一个变量的数据类型?typeof、instanceof、Object.prototype.toString () 的区别是什么?
一、判断变量数据类型的常用方法
常用方法有三种:typeof、instanceof、Object.prototype.toString()。
二、各方法特点及区别
1. typeof
- 作用:检测基本数据类型(返回字符串)。
- 返回值:如
"number"、"string"、"boolean"、"undefined"、"symbol"、"bigint"、"function"(函数)、"object"(对象/数组/null等)。 - 局限:
null会被误判为"object"(历史遗留问题)。- 数组、日期等引用类型均返回
"object",无法区分。
2. instanceof
- 作用:检测对象是否为某个构造函数的实例(基于原型链),返回布尔值。
- 示例:
[1,2] instanceof Array → true、new 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]"。
- 优势:可准确区分所有类型(包括
null、undefined及各种引用类型)。
三、核心区别对比
| 方法 | 检测范围 | 返回形式 | 典型局限 |
|---|---|---|---|
typeof |
基本类型为主 | 字符串(如”number”) | 无法区分对象/数组/null |
instanceof |
引用类型(基于原型链) | 布尔值 | 不支持基本类型,跨环境可能失效 |
Object.prototype.toString() |
所有类型 | 统一格式字符串(如”[object Array]”) | 无明显局限,需调用 call() 绑定目标 |
3.简述 JavaScript 中的变量提升(Hoisting)现象,函数声明和变量声明的提升优先级有何不同?
一、变量提升(Hoisting)现象
变量提升是 JavaScript 引擎的一种预编译机制:在代码执行前,引擎会将变量声明和函数声明提升到其所在作用域的顶部(但初始化/赋值不会被提升)。
这意味着可以在声明前使用这些变量或函数(但可能导致不符合预期的结果)。
二、函数声明与变量声明的提升优先级
函数声明的提升:整个函数体(包括定义)会被完整提升到作用域顶部。
例: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
71foo(); // 输出 "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 | function outer() { |
闭包的形成条件
闭包的形成必须同时满足以下 3 个条件,缺一不可:
- 函数嵌套:存在“外部函数”和“内部函数”的嵌套结构(内部函数定义在外部函数内部);
- 作用域引用:内部函数主动引用了外部函数作用域中的变量/参数(若内部函数不引用外部资源,则无闭包);
- 外部引用保留:外部函数执行后,内部函数没有被销毁,而是被外部函数之外的变量引用(如返回给外部变量、赋值给全局变量等)。
举例说明闭包的形成条件
闭包形成需同时满足三个条件:函数嵌套、内部函数引用外部变量、外部保留内部函数引用。以下示例清晰体现这三个条件:
1 | function outer() { |
2. 防抖(Debounce)与节流(Throttle)
用于控制函数执行频率(如输入框搜索、滚动事件),通过闭包保存“定时器ID”“最后执行时间”等状态,避免状态丢失。
示例(简单防抖):
1 | function debounce(fn, delay) { |
3. 函数柯里化(Currying)
将多参数函数拆分为一系列单参数函数,通过闭包逐步“固定”参数,返回新函数。
示例:
1 | function add(a) { |
4. 保存循环中的状态
解决“循环中异步函数访问循环变量”的经典问题(如 for(var i=0) 时,异步函数执行时 i 已变成循环结束值)。
示例:
1 | // 问题代码:所有setTimeout执行时,i已变成3 |
闭包的潜在问题及解决思路
闭包并非“万能”,不当使用会引发问题,核心问题是内存泄漏。
1. 核心问题:内存泄漏
原因:闭包会持续引用外部函数的作用域(包括其中的变量、DOM元素等),导致外部函数的作用域无法被 JavaScript 垃圾回收机制(GC)回收,长期积累会占用过多内存,甚至导致页面卡顿。
示例(危险场景):
1
2
3
4
5
6
7
8function 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
60console.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, ...)
作用:立即执行函数,thisArg为this指向的对象,后续参数为函数的参数列表(逐个传入)。示例:
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
145function 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.prototype → Object.prototype → null`。
简言之,原型是对象共享属性的载体,原型链是属性查找的路径,通过这种机制,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 |
|
- 同步代码开始
- 同步代码结束
- 微任务 Promise.then 执行
- 第二个微任务执行
- 宏任务 setTimeout 执行
- 宏任务内的微任务执行
- 微任务内的宏任务执行
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"));
```
**执行结果(浏览器环境)**: - 同步代码开始
- 同步代码结束
- 微任务 Promise.then 执行
- 微任务中新增的微任务
- 微任务 MutationObserver 执行
// (UI渲染步骤:此时浏览器可能更新DOM,但无输出) - 宏任务 setTimeout 执行
- 宏任务内的微任务执行
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 |
true→1、false→0 |
布尔值固定对应 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 | // Number() 严格转换 |
3. 强制转换为布尔值(Boolean())
核心规则
JS 中只有 7 个“假值(falsy value)” 会转为 false,其余所有值(包括空对象、空数组)均转为 true。
假值列表(必记)
false(本身)0和-0(数字零)NaN(非数字)""(空字符串)nullundefined0n(BigInt 零)
示例
1 | console.log(Boolean(0)); // false(假值) |
二、隐式转换(自动转换)
隐式转换是 JS 在 运算、比较、逻辑判断 等场景下自动触发的转换,无需显式调用函数,规则依赖具体场景(目标类型通常是“字符串”“数字”或“布尔值”)。
1. 隐式转换为字符串(场景:+ 运算符含字符串)
当 + 运算符的 至少一个操作数是字符串 时,其他操作数会隐式转为字符串,最终执行“字符串拼接”(而非加法)。
示例
1 | // 数字 + 字符串 → 字符串(数字隐转字符串) |
2. 隐式转换为数字(场景:算术运算、比较运算)
当触发 算术运算(-、*、/、%、++、--) 或 比较运算(>、<、<=、>=) 时,非数字操作数会隐式转为数字。
示例
1 | // 算术运算:字符串隐转数字 |
3. 隐式转换为布尔值(场景:逻辑判断、条件语句)
当触发 逻辑运算符(&&、||、!) 或 条件语句(if、while) 时,操作数会隐式转为布尔值,规则与 Boolean() 一致(假值转 false,其余转 true)。
示例
1 | // if 条件:隐转布尔值 |
4. 特殊场景:== 比较的隐式转换
== 是“松散相等”,会触发隐式转换后再比较;而 === 是“严格相等”,不转换类型,直接比较值和类型(推荐优先使用 === 避免坑)。
== 的核心规则
- 若类型相同,直接比较值(同
===)。 - 若类型不同,按以下优先级转换:
null == undefined→true(特殊规则,不转数字);- 若一个是
string、一个是number,将string转为number后比较; - 若一个是
boolean,将boolean转为number(true→1,false→0)后比较; - 若一个是对象、一个是原始类型,先将对象转原始类型,再按上述规则比较。
示例(常见坑)
1 | console.log(null == undefined); // true(特殊规则) |
三、核心区别与注意事项
| 维度 | 强制转换(显式) | 隐式转换(自动) |
|---|---|---|
| 触发方式 | 显式调用 String()/Number() 等 |
JS 自动触发(运算、比较等场景) |
| 规则透明度 | 规则明确,易控制 | 规则依赖场景,易踩坑(如 "" == 0) |
| 推荐场景 | 需要明确转换结果时(如数据格式化) | 简单场景(如 1 + "个"),复杂场景避免 |
避坑建议
- 比较值时 **优先使用
===**,避免==的隐式转换坑; - 字符串转数字时,若需提取部分数字用
parseInt(str, 10)(指定基数),若需严格转换用Number(); - 避免依赖对象/数组的隐式转换(如
[] + {}结果为"[object Object]",逻辑不直观)。
36.== 和 === 的区别是什么?在什么情况下 == 会返回 true?
在 JavaScript 中,==(松散相等)和 ===(严格相等)都是用于比较两个值是否“相等”的运算符,但核心区别在于是否允许类型转换。
一、== 和 === 的核心区别
===(严格相等):
比较时不进行类型转换,只有当两个值的类型完全相同且值严格相等时,才返回true;否则返回false。==(松散相等):
比较时允许类型转换(隐式转换),先将两个值转换为“相同类型”,再比较转换后的值是否相等。若转换后的值相等,返回true;否则返回false。
二、== 返回 true 的常见情况
== 的比较逻辑依赖于类型转换规则,以下是典型场景:
1. 类型相同,且值相等(与 === 行为一致)
当两个值的类型相同,== 直接比较值是否相等,此时和 === 结果一致。
1 | // 数字类型 |
2. NaN 的产生场景
NaN 通常在以下两种情况下出现:
无效的数学运算:当运算结果无法用有效数字表示时,会返回
NaN。1
2
30 / 0; // NaN (0除以0无意义)
Math.sqrt(-1); // NaN (负数开平方无实数解)
Math.log(-10); // NaN (负数取对数无意义)类型转换失败:当试图将一个无法转为有效数字的非数字值(如字符串
"abc"、对象{})转换为数字时,会返回NaN。1
2
3Number("abc"); // NaN (字符串"abc"无法转为数字)
Number(undefined); // NaN (undefined转数字为NaN)
parseInt("123abc"); // 123(注意:parseInt会“部分转换”,直到非数字字符;但Number()会整体转换,失败则NaN)
3. NaN 的关键特性
NaN 不等于任何值,包括它自身:这是
NaN最核心的特性,也是判断它的重要依据。1
2NaN == 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()(推荐,精准判断)
原理:不进行类型转换,仅判断两个条件:
- 传入的值类型是
Number; - 值本身是
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 | function isNaNValue(x) { |
- 优势:无需依赖 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 中,undefined 和 null 均为原始值,但二者的含义、默认行为及使用场景有明确区别,核心差异在于: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 引擎“自动生成”,而非开发者主动赋值,常见场景包括:
变量声明后未赋值
变量仅声明但未初始化时,默认值为undefined:1
2let name;
console.log(name); // undefined(引擎自动赋值)对象的属性不存在
访问对象中未定义的属性时,返回undefined(而非报错):1
2const user = { age: 20 };
console.log(user.name); // undefined(属性 name 不存在)函数参数未传递
调用函数时,若未给某个参数传值,该参数的默认值为undefined:1
2
3
4function greet(name) {
console.log(name); // 未传参时,name 为 undefined
}
greet(); // undefined函数无返回值
函数未写return语句,或return后无内容时,默认返回undefined:1
2
3
4
5function add(a, b) {
// 无 return
}
const result = add(1, 2);
console.log(result); // undefined
三、null 的使用场景(主动空值)
null 是开发者主动赋值的结果,用于明确表示“此处应为对象,但当前为空/不存在”,常见场景包括:
初始化“待创建的对象”
当变量计划存储对象(如从接口获取的数据、创建的实例),但初始时未就绪,可先赋值为null(明确“空对象”语义,而非“未定义”):1
2
3let user = null; // 计划后续赋值为用户对象,当前为空
// 后续从接口获取数据后赋值
user = { name: "Alice", age: 20 };函数返回“无结果的对象”
当函数逻辑上应返回对象(如查找数据),但未找到目标时,主动返回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(明确表示“无此用户”)清空对象引用(帮助垃圾回收)
当一个对象不再使用时,将其引用赋值为null,可让 JS 垃圾回收机制(GC)识别并释放该对象占用的内存(避免内存泄漏):1
2
3let bigData = { /* 大量数据 */ };
// 使用完 bigData 后,清空引用
bigData = null; // GC 会回收原对象的内存明确“空值”的属性语义
对象属性若需明确表示“空”(而非“未定义”),可赋值为null(例如表单中“用户主动清空的字段”):1
2
3
4const formData = {
username: "Alice",
avatar: null // 明确表示“用户未上传头像”(而非“头像属性未定义”)
};
四、常见误区与最佳实践
不要主动赋值 undefined
若变量需表示“空”,优先用null;主动写let a = undefined毫无意义(变量默认就是undefined),还会混淆“被动未定义”的语义。宽松相等(==)的注意事项
undefined == null会返回true(JS 视二者为“空值”的两种形式),但严格不推荐依赖此特性;实际开发中应使用===(严格相等),明确区分类型和值。判断空值的建议
- 若需判断“变量是否未定义”:用
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])访问单个实参,但不具备数组的原型方法(如forEach、map),需手动转换为数组(如Array.from(arguments))。 - 参数同步性(非严格模式):非严格模式下,修改
arguments中元素的值,会同步修改对应形参的值(反之亦然);严格模式(’use strict’)下,这种同步关系被切断,arguments与形参独立。 - 箭头函数无 arguments:箭头函数不绑定
arguments对象,若需获取实参,需用“剩余参数”替代。
2. 示例与注意事项
1 | // 示例1:非严格模式下的 arguments(参数同步) |
二、默认参数:参数缺省时的默认值
默认参数允许为函数形参设置默认值,当调用函数时该参数未传递(或传递为 undefined),则自动使用默认值,避免手动判断 undefined 的繁琐。
1. 核心特性
- 生效条件:仅当实参为
undefined时触发默认值(若传递null、0、''等 falsy 值,默认值不生效)。 - 默认值支持表达式:默认值可以是常量、变量、函数调用等表达式(表达式在函数每次调用时动态计算,而非定义时)。
- 独立作用域:默认参数会形成一个单独的“参数作用域”,与函数体作用域隔离,避免变量污染。
- 参数顺序:若有多个参数,默认参数应放在普通参数(无默认值)之后(否则未传递的普通参数会被解析为
undefined,可能不符合预期)。
2. 示例与注意事项
1 | // 示例1:基础默认参数 |
三、剩余参数:收集不定长实参为真数组
剩余参数(Rest Parameters)用 ...变量名 语法表示,用于收集函数调用时“剩余的实参”,并将其转换为一个真数组(可直接使用数组方法),是 arguments 对象的现代替代方案。
1. 核心特性
- 真数组本质:剩余参数返回的是标准数组(而非类数组),可直接使用
forEach、map、reduce等数组方法,无需手动转换。 - 位置限制:剩余参数必须是函数的最后一个形参(否则会报错),且一个函数只能有一个剩余参数。
- 箭头函数支持:剩余参数可在箭头函数中使用,弥补了箭头函数无
arguments的缺陷。 - 与 arguments 的区别:剩余参数仅收集“未被前面形参匹配的实参”,而
arguments收集所有实参;剩余参数是真数组,arguments是类数组。
2. 示例与注意事项
1 | // 示例1:基础剩余参数(收集所有实参) |
四、特性对比与使用场景总结
| 特性 | 本质 | 核心优势 | 适用场景 |
|---|---|---|---|
| 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 | // 普通函数:this 动态绑定 |
- 初始化表达式:循环前执行一次(如声明索引变量
let i = 0)。 - 条件判断表达式:每次循环前判断,
true则执行循环体,false则退出。 - 更新表达式:每次循环体执行后更新变量(如
i++,控制步长)。
示例(遍历数组)
1 | const arr = [10, 20, 30]; |
核心特性
- 适用对象:数组(通过索引访问)、类数组对象(如
arguments)。 - 支持中断:可通过
break(退出整个循环)、continue(跳过当前迭代)控制。 - 灵活性:可自定义步长(如
i += 2隔一个遍历)、遍历范围。
2. for...in 循环(对象属性遍历型)
for...in 专为遍历对象属性设计,会遍历对象的自身可枚举属性及原型链上的可枚举属性(需注意过滤原型属性),不建议用于遍历数组。
语法
1 | for (let 键名 in 对象) { |
示例(遍历对象)
1 | const obj = { name: "Alice", age: 20 }; |
核心特性
- 适用对象:普通对象(优先)、数组(不推荐)。
- 获取内容:遍历的是键名(对象属性名、数组索引,均为字符串类型),需通过
对象[键名]获取值。 - 原型链问题:会遍历原型链上的可枚举属性,必须用
obj.hasOwnProperty(key)过滤。 - 支持中断:可通过
break/continue中断循环。
3. for...of 循环(可迭代对象遍历型)
for...of 是 ES6 新增的循环,专为遍历可迭代对象(Iterable)设计,直接获取元素值,不遍历原型链,是遍历数组、集合等的优选方案。
可迭代对象范围
包括:数组、字符串、Set、Map、Generator、类数组对象(如 NodeList)等(普通对象默认不可迭代,需手动部署 Iterator 接口才能用)。
语法
1 | for (let 元素值 of 可迭代对象) { |
示例(遍历数组/Set/字符串)
1 | // 1. 遍历数组(直接获取值,无索引问题) |
核心特性
- 适用对象:可迭代对象(数组、
Set、Map等,普通对象不直接支持)。 - 获取内容:直接遍历元素值(无需通过键名间接获取),Map 需解构获取键值对。
- 原型链安全:仅遍历对象自身的迭代内容,不涉及原型链属性。
- 支持中断:可通过
break/continue中断循环。
4. forEach 方法(数组专属遍历型)
forEach 是数组的实例方法,专为数组全量遍历设计,语法简洁,但无法中断循环,无返回值。
语法
1 | 数组.forEach((当前元素值, 当前索引, 原数组) => { |
示例(遍历数组)
1 | const arr = [10, 20, 30]; |
核心特性
- 适用对象:仅数组(非数组需先转为数组,如
[...NodeList].forEach(...))。 - 无返回值:回调函数的
return仅退出当前迭代,不影响整体循环,且forEach最终返回undefined。 - 不可中断:无法用
break/continue中断循环(即使抛出异常也不推荐)。 - 遍历全量:必须遍历数组所有元素,无法跳过范围(除非在回调中用条件判断跳过当前执行)。
二、四种循环的核心区别对比
为了更清晰区分,以下从 6 个关键维度整理对比表:
| 对比维度 | 普通 for 循环 |
for...in |
for...of |
forEach |
|---|---|---|---|---|
| 适用对象 | 数组、类数组 | 普通对象(优先) | 可迭代对象(数组/Set等) | 仅数组 |
| 获取内容 | 索引 → 元素(arr[i]) |
键名(字符串)→ 值 | 直接获取元素值 | 元素值、索引(回调参数) |
| 原型链遍历 | 不涉及 | 会遍历(需 hasOwnProperty 过滤) |
不遍历 | 不遍历 |
| 中断支持 | 支持(break/continue) |
支持 | 支持 | 不支持 |
| 返回值 | 无(需手动维护结果) | 无 | 无 | 固定返回 undefined |
| 特殊注意事项 | 需手动控制索引/步长 | 数组索引为字符串,不推荐遍历数组 | 普通对象默认不可用 | 无法跳过循环,回调 this 需注意绑定 |
三、适用场景总结
普通
for循环:
需精确控制遍历(如倒序、自定义步长)、或需中断循环的数组/类数组场景(如“找到目标元素后退出”)。for...in循环:
仅用于遍历普通对象的自身可枚举属性(如获取对象的所有键名),必须搭配hasOwnProperty过滤原型属性,绝对不用于数组。for...of循环:
遍历数组、Set、Map等可迭代对象,且可能需要中断循环的场景(如“遍历数组时遇到负数则退出”),语法简洁且安全。forEach方法:
数组的全量无中断遍历(如批量修改数组元素、批量打印),语法简洁,无需关心索引控制。
通过以上对比,可根据数据类型(对象/数组/集合)和需求(是否中断、是否控制索引)选择最合适的循环方式。
48.什么是柯里化(Currying)?它的作用是什么?如何实现函数柯里化?
柯里化(Currying)是函数式编程中的一种技术,核心是将接受多个参数的函数转换为一系列只接受单个参数(或部分参数)的函数,使得函数可以分阶段接收参数,最终在参数齐全时执行。
一、柯里化的基本概念
普通函数调用:fn(a, b, c)(一次性传入所有参数)
柯里化后调用:fn(a)(b)(c)(分三次传入参数,每次传入部分参数)
例如,一个计算三数之和的函数:
1 | // 普通函数 |
5. 函数参数劫持
拦截函数调用,对参数进行预处理(如类型转换、默认值填充)。
1 | function withParamCheck(fn) { |
总结
- Proxy:通过拦截对象操作(属性访问、赋值等),实现自定义逻辑(校验、监控、响应式等),是对象行为扩展的核心工具。
- Reflect:提供与 Proxy 陷阱对应的默认操作方法,用于在拦截中保持原对象的默认行为,简化代码并增强兼容性。
- 两者配合使用,既能灵活扩展对象功能,又不破坏原生逻辑,广泛应用于框架开发、数据处理、权限控制等场景。
