这一梯队属于中高频问题,虽然不像前两梯队那样每面必问,但在中等规模公司面试、大厂二面/三面、或者作为追问延伸时经常出现。掌握这些知识点能让你的回答更有深度,展现出扎实的基础。
第三梯队:中高频(进阶基础,深度追问)
- JavaScript 数组有哪些常用方法?
深入理解:数组方法的分类体系
JavaScript 数组方法超过 30 个,按功能可分为 8 大类:
数组方法分类图谱
├── 添加/删除元素(改变原数组)
│ ├── 尾部:push / pop
│ ├── 头部:unshift / shift
│ └── 任意位置:splice
├── 查找元素(不改变原数组)
│ ├── 按值查找:indexOf / lastIndexOf / includes
│ └── 按条件查找:find / findIndex / findLast / findLastIndex
├── 遍历处理(不改变原数组,返回新值)
│ ├── 映射:map
│ ├── 过滤:filter
│ ├── 归约:reduce / reduceRight
│ └── 遍历:forEach(无返回值)
├── 切片与连接(不改变原数组)
│ ├── 切片:slice
│ └── 连接:concat
├── 排序与反转(改变原数组)
│ ├── 排序:sort
│ └── 反转:reverse
├── 测试方法(返回布尔值)
│ ├── 全部满足:every
│ ├── 部分满足:some
│ └── 包含满足:at(ES2022)
├── 扁平化与填充(ES2019+)
│ ├── 扁平化:flat / flatMap
│ └── 填充:fill / copyWithin
└── 静态方法(Array 构造函数上)
├── 创建:Array.from / Array.of
└── 判断:Array.isArray
重点方法深度解析
splice 的万能性(改变原数组的"瑞士军刀"):
const arr = ['a', 'b', 'c', 'd'];
// 删除:从索引1开始,删除2个
arr.splice(1, 2); // 返回 ['b', 'c'],arr 变为 ['a', 'd']
// 插入:从索引1开始,删除0个,插入'x','y'
arr.splice(1, 0, 'x', 'y'); // arr 变为 ['a', 'x', 'y', 'd']
// 替换:从索引1开始,删除1个,插入'z'
arr.splice(1, 1, 'z'); // arr 变为 ['a', 'z', 'y', 'd']
reduce 的强大之处(数组"万能方法"):
// 不仅仅是求和
const arr = [1, 2, 3, 4];
// 1. 求和
const sum = arr.reduce((acc, cur) => acc + cur, 0);
// 2. 转对象(分组统计)
const count = arr.reduce((acc, cur) => {
acc[cur] = (acc[cur] || 0) + 1;
return acc;
}, {});
// 3. 嵌套数组扁平化
const nested = [[1, 2], [3, 4], [5]];
const flat = nested.reduce((acc, cur) => acc.concat(cur), []);
// 4. 管道函数(函数组合)
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const add5 = x => x + 5;
const multiply2 = x => x * 2;
const addThenMultiply = pipe(add5, multiply2);
console.log(addThenMultiply(10)); // (10+5)*2 = 30
find 与 indexOf 的本质区别:
| 特性 | indexOf | find |
|---|---|---|
| 查找依据 | 严格相等(===) | 自定义条件函数 |
| 查找对象 | 值 | 引用(对象数组) |
| 返回 | 索引(-1表示未找到) | 元素值(undefined表示未找到) |
| NaN 处理 | 找不到(NaN !== NaN) | 可以找到 |
const arr = [NaN, {id: 1}, {id: 2}];
arr.indexOf(NaN); // -1(找不到)
arr.find(x => Number.isNaN(x)); // NaN(找到)
arr.indexOf({id: 1}); // -1(不同引用)
arr.find(x => x.id === 1); // {id: 1}(按条件找到)
ES2023 新增方法(面试加分项)
const arr = [1, 2, 3, 4, 5];
// toSorted - 不改变原数组的排序
const sorted = arr.toSorted((a, b) => b - a); // [5,4,3,2,1]
console.log(arr); // [1,2,3,4,5](原数组不变)
// toReversed - 不改变原数组的反转
const reversed = arr.toReversed(); // [5,4,3,2,1]
// toSpliced - 不改变原数组的 splice
const spliced = arr.toSpliced(1, 2, 'a', 'b'); // [1,'a','b',4,5]
// with - 不改变原数组的修改(替换指定索引)
const replaced = arr.with(2, 'x'); // [1,2,'x',4,5]
面试标准回答模板
"JavaScript 数组方法丰富,按功能可分为添加删除(push/pop/unshift/shift/splice)、查找(indexOf/find/includes)、遍历处理(map/filter/reduce/forEach)、切片连接(slice/concat)、排序反转(sort/reverse)、测试(every/some)、扁平化(flat/flatMap)等类别。
重点区分:改变原数组的方法有 push/pop/shift/unshift/splice/sort/reverse/fill/copyWithin;不改变原数组的方法有 map/filter/slice/concat/find/findIndex 等。ES2023 新增了 toSorted/toReversed/toSpliced/with 等不改变原数组的版本,类似于 React 的不可变数据理念。
reduce 是最强大的数组方法,可以实现 map、filter、flat 等功能,常用于数据转换、分组统计、函数管道等场景。"
- 哪些数组方法会改变原数组?
深入理解:可变 vs 不可变的设计哲学
为什么要有改变原数组的方法?
早期 JavaScript 设计追求性能和简洁。在内存有限的年代,原地修改避免创建新数组,节省内存和 GC 压力。
现代趋势:不可变性(Immutability)
React、Redux 等框架推广了不可变数据理念,因为:
- 可预测性:数据不意外变化,减少 bug
- 性能优化:配合虚拟 DOM,通过引用比较快速判断变化
- 时间旅行调试:保存状态历史,方便调试
ES2023 的解决方案:提供不可变版本
可变方法 不可变版本(ES2023)
─────────────────────────────────────
sort() → toSorted()
reverse() → toReversed()
splice() → toSpliced()
直接索引赋值 → with(index, value)
完整对照表
| 改变原数组(9个) | 不改变原数组(常用) |
|---|---|
push() - 尾部添加 | concat() - 连接数组 |
pop() - 尾部删除 | slice() - 切片 |
unshift() - 头部添加 | map() - 映射 |
shift() - 头部删除 | filter() - 过滤 |
splice() - 任意位置增删改 | find() - 查找元素 |
sort() - 排序 | findIndex() - 查找索引 |
reverse() - 反转 | indexOf() - 查找索引 |
fill() - 填充 | includes() - 包含判断 |
copyWithin() - 内部复制 | join() - 转字符 |
注意陷阱:
const arr = [3, 1, 2];
// 看似没改变,实际改变了!
const sorted = arr.sort();
console.log(arr); // [1, 2, 3] - 原数组被排序了!
// 正确做法:先复制
const sorted = [...arr].sort();
// 或 ES2023
const sorted = arr.toSorted();
面试标准回答模板
"会改变原数组的方法有 9 个:push、pop、unshift、shift、splice、sort、reverse、fill、copyWithin。其他常用方法如 map、filter、slice、concat、find 等都不会改变原数组。
在现代开发中,特别是在 React、Redux 等框架中,推荐保持数据不可变性。ES2023 新增了 toSorted、toReversed、toSpliced、with 等方法,提供不改变原数组的操作版本,可以在需要不可变性时替代传统方法。"
- for...in 和 for...of 的区别
深入理解:迭代协议的设计差异
for...in:遍历对象的「可枚举属性」
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
console.log(key); // 'a', 'b', 'c'(字符串键)
}
// 遍历数组的问题
const arr = ['a', 'b', 'c'];
for (const index in arr) {
console.log(index, arr[index]);
// '0' 'a', '1' 'b', '2' 'c' - index 是字符串!
// 还会遍历原型链上的可枚举属性(如果扩展了 Array.prototype)
}
for...of:遍历「可迭代对象的值」
const arr = ['a', 'b', 'c'];
for (const value of arr) {
console.log(value); // 'a', 'b', 'c' - 直接获取值
}
// 不能用于普通对象(不可迭代)
const obj = { a: 1, b: 2 };
for (const value of obj) {
// TypeError: obj is not iterable
}
底层机制:迭代器协议
可迭代对象(Iterable)必须实现:
const iterable = {
// Symbol.iterator 方法返回迭代器
[Symbol.iterator]() {
let i = 0;
const data = this.data;
// 迭代器对象必须有 next 方法
return {
next() {
if (i < data.length) {
return { value: data[i++], done: false };
}
return { done: true };
}
};
},
data: [1, 2, 3]
};
for (const value of iterable) {
console.log(value); // 1, 2, 3
}
内置可迭代对象:
- Array、String、Map、Set、TypedArray、arguments、NodeList
对比总结表
| 特性 | for...in | for...of |
|---|---|---|
| 遍历目标 | 对象的可枚举属性(包括继承的) | 可迭代对象的值 |
| 返回内容 | 键(字符串) | 值(任意类型) |
| 适用对象 | 普通对象、数组(不推荐) | 数组、Map、Set、String 等可迭代对象 |
| 数组索引 | 字符串 '0', '1', '2' | 不需要索引,直接取值 |
| 原型链 | 会遍历原型链(需 hasOwnProperty 过滤) | 不会 |
| 性能 | 较慢(需检查原型链) | 较快(直接迭代) |
面试标准回答模板
"for...in 遍历对象的可枚举属性,返回字符串键,会遍历原型链,适合普通对象但不推荐用于数组,因为获取的是字符串索引且可能包含原型属性。for...of 遍历可迭代对象的值,直接获取元素值,适用于数组、Map、Set、String 等实现了迭代器协议的对象。
for...of 底层使用迭代器协议(Symbol.iterator),通过 next 方法逐个获取值,直到 done 为 true。普通对象没有默认迭代器,所以不能用 for...of,但可以通过 Object.keys/values/entries 配合 for...of 使用。"
- map 和 forEach 的区别
深入理解:函数式 vs 过程式
核心区别:返回值与链式调用
const arr = [1, 2, 3];
// map:返回新数组,支持链式调用
const doubled = arr.map(x => x * 2)
.filter(x => x > 2)
.reduce((a, b) => a + b, 0);
// forEach:无返回值,不支持链式调用
let sum = 0;
arr.forEach(x => {
sum += x; // 副作用:修改外部变量
});
// 无法继续 .filter().map()
关键差异对比:
| 特性 | map | forEach |
|---|---|---|
| 返回值 | 新数组(映射结果) | undefined |
| 链式调用 | 支持 | 不支持 |
| 原数组 | 不改变(除非回调中修改) | 不改变(除非回调中修改) |
| 跳过空位 | 不跳过,会执行回调返回 undefined | 跳过空位,不执行回调 |
| 中断遍历 | 不能(除非 throw) | 不能(除非 throw) |
| 使用场景 | 数据转换、函数式编程 | 副作用操作(打印、修改外部变量) |
空位处理差异(稀疏数组):
const sparse = [1, , 3]; // 索引1是空位,不是 undefined
// map 不跳过空位,但保留空位
sparse.map(x => x * 2); // [2, , 6] - 空位还是空位,但执行了回调(返回 undefined)
// forEach 跳过空位
sparse.forEach(x => console.log(x)); // 只输出 1 和 3
性能考量:
现代引擎优化下,两者性能差异极小。选择依据应是语义清晰度:
- 需要转换数据 → 用 map
- 需要副作用(修改外部状态、DOM 操作、打印日志)→ 用 forEach
面试标准回答模板
"map 和 forEach 都用于遍历数组,但 map 返回新数组,支持链式调用,适合数据转换场景;forEach 无返回值,适合执行副作用操作如修改外部变量、DOM 操作、打印日志等。
两者都不能中断遍历(break/return 无效),如需中断应使用 for 循环或 some/every。另外,处理稀疏数组时,map 会保留空位但返回 undefined,forEach 会跳过空位不执行回调。"
- reduce 的使用场景
深入理解:reduce 是"万能归约"
reduce 的本质:将数组"折叠"为单个值
数组 [1, 2, 3, 4]
↓
归约过程
↓
单个值 10(求和)
或对象 {count: 4, sum: 10, max: 4}
或新数组 [2, 4, 6, 8](相当于 map)
或树结构、Promise 链等
六大经典使用场景
- 基础聚合(求和、求积、最值)
const nums = [3, 1, 4, 1, 5];
// 求和
const sum = nums.reduce((acc, cur) => acc + cur, 0);
// 求最大值
const max = nums.reduce((acc, cur) => cur > acc ? cur : acc, -Infinity);
// 同时计算多个统计值(一次遍历)
const stats = nums.reduce((acc, cur) => ({
count: acc.count + 1,
sum: acc.sum + cur,
min: Math.min(acc.min, cur),
max: Math.max(acc.max, cur)
}), { count: 0, sum: 0, min: Infinity, max: -Infinity });
- 数组转对象(分组、映射)
const users = [
{ id: 1, name: 'Tom', group: 'A' },
{ id: 2, name: 'Jerry', group: 'B' },
{ id: 3, name: 'Spike', group: 'A' }
];
// 按 group 分组
const grouped = users.reduce((acc, user) => {
acc[user.group] = acc[user.group] || [];
acc[user.group].push(user);
return acc;
}, {});
// { A: [Tom, Spike], B: [Jerry] }
// 转 id 映射对象(快速查找)
const userMap = users.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {});
// { 1: {id:1, name:'Tom'}, 2: {...}, 3: {...} }
- 数组扁平化
const nested = [[1, 2], [3, 4], [5, [6, 7]]];
// 一层扁平化
const flat = nested.reduce((acc, cur) => acc.concat(cur), []);
// 深度扁平化(递归)
const deepFlat = arr => arr.reduce((acc, cur) =>
acc.concat(Array.isArray(cur) ? deepFlat(cur) : cur), []
);
- 实现 map 和 filter(展示 reduce 的万能性)
// 用 reduce 实现 map
const map = (arr, fn) => arr.reduce((acc, cur, i) => {
acc.push(fn(cur, i));
return acc;
}, []);
// 用 reduce 实现 filter
const filter = (arr, fn) => arr.reduce((acc, cur) => {
if (fn(cur)) acc.push(cur);
return acc;
}, []);
- 函数管道(函数组合)
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const add5 = x => x + 5;
const multiply2 = x => x * 2;
const toString = x => String(x);
const transform = pipe(add5, multiply2, toString);
transform(10); // '30'
- Promise 顺序执行(串行异步)
const tasks = [fn1, fn2, fn3]; // 返回 Promise 的函数数组
// 串行执行,前一个完成再执行下一个
tasks.reduce((promise, task) => {
return promise.then(result => task(result));
}, Promise.resolve(initialValue));
面试标准回答模板
"reduce 是数组的归约方法,将数组元素通过回调函数累积为单个值。常见使用场景包括:数据聚合(求和、最值、统计)、数组转对象(分组、快速查找映射)、数组扁平化、实现 map/filter 展示函数式编程能力、构建函数管道(函数组合)、以及串行执行 Promise 等。
reduce 接受两个参数:回调函数(接收累积值、当前值、索引、原数组)和初始值。提供初始值是最佳实践,否则数组为空时会抛出 TypeError,且类型推断更困难。"
- 数组去重有哪些方法?
深入理解:去重的复杂度与适用场景
方法对比表:
| 方法 | 代码 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Set(推荐) | [...new Set(arr)] | 简洁、O(n)、保持插入顺序 | 只能处理原始值 | 原始值数组 |
| filter + indexOf | arr.filter((v,i) => arr.indexOf(v) === i) | 兼容性好 | O(n²)、indexOf 每次遍历 | 小数组 |
| reduce | arr.reduce((a,b) => a.includes(b) ? a : [...a,b], []) | 函数式 | O(n²)、创建新数组开销 | 函数式场景 |
| Map 对象 | 用 Map 存储已见值 | O(n)、可处理对象(引用) | 代码较多 | 对象数组 |
| 排序后去重 | arr.sort().filter((v,i,a) => v !== a[i-1]) | O(n log n) | 改变原数组顺序 | 需要排序时 |
处理对象数组(按属性去重):
const users = [
{ id: 1, name: 'Tom' },
{ id: 2, name: 'Jerry' },
{ id: 1, name: 'Tom Clone' } // id 重复
];
// 按 id 去重,保留最后一个
const unique = [...new Map(users.map(u => [u.id, u])).values()];
// [{id: 2, name: 'Jerry'}, {id: 1, name: 'Tom Clone'}]
// 原理:Map 的键唯一,后值覆盖前值,最后转回数组
处理 NaN(特殊值):
const arr = [1, NaN, 2, NaN, 3];
// Set 能正确处理 NaN(NaN !== NaN,但 Set 认为相等)
[...new Set(arr)]; // [1, NaN, 2, 3]
// indexOf 无法找到 NaN
arr.filter((v, i) => arr.indexOf(v) === i); // [1, 2, 3](NaN 丢失!)
面试标准回答模板
"数组去重最常用的方法是 Set:
[...new Set(arr)],时间复杂度 O(n),代码简洁,能正确处理 NaN,保持插入顺序,适合原始值数组。对于对象数组,可以使用 Map 按唯一属性去重:[...new Map(arr.map(v => [v.id, v])).values()]。
兼容性要求低的环境可以用 filter 配合 indexOf,但时间复杂度 O(n²) 不适合大数组。如果数组已排序,可以先排序再用一次遍历去重,复杂度 O(n log n)。"
- 数组扁平化如何实现?
深入理解:递归 vs 迭代的权衡
三种实现方式对比:
const arr = [1, [2, 3], [4, [5, 6]]];
// 1. 递归实现(深度优先)
function flatDeep(arr, depth = 1) {
return depth > 0
? arr.reduce((acc, val) =>
acc.concat(Array.isArray(val) ? flatDeep(val, depth - 1) : val),
[])
: arr.slice();
}
// 2. 迭代实现(广度优先,使用栈)
function flatDeepIter(arr, depth = 1) {
const stack = arr.map(item => [item, depth]);
const result = [];
while (stack.length > 0) {
const [item, d] = stack.pop();
if (Array.isArray(item) && d > 0) {
stack.push(...item.map(sub => [sub, d - 1]));
} else {
result.push(item);
}
}
return result.reverse(); // 栈是后进先出,需要反转
}
// 3. 使用原生 flat(ES2019)
arr.flat(2); // [1, 2, 3, 4, 5, 6]
arr.flat(Infinity); // 完全扁平化
性能考量:
- 递归:代码简洁,但深度大时有栈溢出风险
- 迭代:安全,但代码复杂,需要反转结果
- 原生 flat:最优选择,引擎优化,无栈溢出风险
面试标准回答模板
"数组扁平化可以用递归或迭代实现。递归方案使用 reduce 配合 concat,遇到数组则递归处理,代码简洁但深度大时有栈溢出风险;迭代方案使用栈模拟递归,安全但代码较复杂。
生产环境推荐使用 ES2019 的原生 flat 方法,支持指定深度或 Infinity 完全扁平化,引擎优化性能好。如果需要在扁平化时处理元素,可以使用 flatMap,它等价于 map 后接 flat(1)。"
- 如何判断一个对象是空对象?
深入理解:不同场景的判断标准
三种方法对比:
const obj = {};
// 1. for...in(最准确,考虑可枚举属性)
function isEmpty(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) return false;
}
return true;
}
// 2. Object.keys(常用,只考虑自有可枚举属性)
Object.keys(obj).length === 0;
// 3. JSON.stringify(简单,但性能差,有边界情况)
JSON.stringify(obj) === '{}';
边界情况处理:
// 特殊情况 1:对象有不可枚举属性
const obj = {};
Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false });
Object.keys(obj).length === 0; // true(但对象不是"空"的)
for (const key in obj) {} // 也遍历不到
// 特殊情况 2:继承属性
const child = Object.create({ inherited: 'value' });
Object.keys(child).length === 0; // true(没有自有属性)
for (const key in child) { console.log(key); } // 'inherited'
// 特殊情况 3:Symbol 属性
const sym = Symbol('secret');
const obj = { [sym]: 'value' };
Object.keys(obj).length === 0; // true(Symbol 属性不算)
Object.getOwnPropertySymbols(obj).length === 0; // false
最严谨的判断(考虑所有情况):
function isTrulyEmpty(obj) {
// 1. 排除 null 和非对象
if (obj === null || typeof obj !== 'object') return false;
// 2. 检查自有可枚举属性
if (Object.keys(obj).length > 0) return false;
// 3. 检查自有不可枚举属性(除非明确要忽略)
if (Object.getOwnPropertyNames(obj).length > 0) return false;
// 4. 检查 Symbol 属性
if (Object.getOwnPropertySymbols(obj).length > 0) return false;
return true;
}
面试标准回答模板
"判断空对象最常用的方法是
Object.keys(obj).length === 0,它检查对象是否有自有可枚举属性。如果需要考虑继承属性,可以用 for...in 配合 hasOwnProperty 过滤。
边界情况包括:对象可能有不可枚举属性(Object.defineProperty 定义)、Symbol 属性(Object.getOwnPropertySymbols 检查)、或继承自原型链的属性。最严谨的判断需要结合 Object.keys、Object.getOwnPropertyNames 和 Object.getOwnPropertySymbols。"
- Object 和其他数据类型的本质区别
深入理解:JavaScript 的类型系统架构
两种类型系统的视角:
视角 1:ECMAScript 规范(8 种类型)
┌─────────────────────────────────────┐
│ 数据类型 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 原始类型 │ │ 对象类型 │ │
│ │ (7种) │ │ (Object) │ │
│ │ │ │ │ │
│ │ • Undefined │ │ • 普通对象 │ │
│ │ • Null │ │ • 数组 │ │
│ │ • Boolean │ │ • 函数 │ │
│ │ • String │ │ • 日期 │ │
│ │ • Symbol │ │ • 正则 │ │
│ │ • Number │ │ • 错误对象 │ │
│ │ • BigInt │ │ • ... │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────┘
视角 2:运行时行为(typeof 操作符)
┌─────────────────────────────────────┐
│ typeof 返回值(7 种字符串) │
│ │
│ "undefined" | "boolean" | "number" │
│ "string" | "symbol" | "bigint" │
│ "object" | "function" │
│ │
│ 注意:null 是 "object"(历史 bug) │
│ 函数是 "function"(特殊处理) │
└─────────────────────────────────────┘
Object 的本质特征:
// 1. 引用类型(存储引用地址)
const obj1 = { a: 1 };
const obj2 = obj1;
obj2.a = 2;
console.log(obj1.a); // 2(共享引用)
// 2. 可扩展性(动态添加属性)
obj1.b = 3; // 随时添加新属性
// 3. 原型链继承
console.log(obj1.toString); // 继承自 Object.prototype
// 4. 装箱与拆箱(原始类型的对象包装)
const str = "hello"; // 原始字符串
str.toUpperCase(); // 自动装箱为 String 对象,调用方法后拆箱
// 5. 作为哈希表(键值对存储)
const map = {};
map['key'] = 'value';
map[123] = 'number key'; // 键会被转为字符串 '123'
Object 与原始类型的核心差异:
特性 原始类型 Object 存储 栈(值本身) 堆(通过引用访问) 比较 值比较(内容相同即相等) 引用比较(同一对象才相等) 可变性 不可变(重新赋值是替换) 可变(修改属性) 属性 无 可动态添加、删除 继承 无(包装对象有原型) 有原型链 typeof 返回具体类型 "object"(函数除外)
面试标准回答模板
"Object 是 JavaScript 中唯一的引用类型,与 7 种原始类型的本质区别在于存储方式、可变性和行为特征。原始类型存储在栈中,值不可变,比较时进行值比较;Object 存储在堆中,通过引用访问,值可变,比较时进行引用比较。
Object 具有动态属性系统,可以添加、删除、修改属性,通过原型链继承方法和属性。函数、数组、日期等都是 Object 的特殊类型,typeof 会特殊识别函数返回'function',其他都返回'object'。"
- Object.defineProperty 和 Proxy 的区别
深入理解:属性拦截的两种范式
Object.defineProperty(ES5):属性级别的劫持
const obj = {};
let internalValue;
Object.defineProperty(obj, 'name', {
// 数据描述符
get() {
console.log('Getting name');
return internalValue;
},
set(newVal) {
console.log('Setting name to', newVal);
internalValue = newVal;
},
enumerable: true,
configurable: true
});
obj.name = 'Tom'; // Setting name to Tom
console.log(obj.name); // Getting name → Tom
Proxy(ES6):对象级别的代理
const target = { name: 'Tom', age: 20 };
const proxy = new Proxy(target, {
// 拦截 get 操作
get(obj, prop) {
console.log('Getting', prop);
if (prop in obj) return obj[prop];
return 'Property not found';
},
// 拦截 set 操作
set(obj, prop, value) {
console.log('Setting', prop, 'to', value);
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be number');
}
obj[prop] = value;
return true; // 严格模式必须返回 true
},
// 拦截其他操作:has、deleteProperty、ownKeys 等
has(obj, prop) {
console.log('Checking', prop);
return prop in obj;
}
});
proxy.name; // Getting name → Tom
proxy.age = 25; // Setting age to 25
'age' in proxy; // Checking age → true
proxy.gender; // Getting gender → 'Property not found'
核心差异对比
特性 Object.defineProperty Proxy 拦截粒度 单个属性,需预先定义 整个对象,自动代理所有属性 新增属性 无法拦截,需递归定义 自动拦截,无需额外处理 数组操作 无法拦截索引操作、length 变化 完美拦截所有操作 嵌套对象 需深度递归定义 需配合递归代理,但机制更优雅 性能 较快(属性访问优化) 稍慢(代理层开销) 兼容性 IE9+ 现代浏览器,无 IE 支持 可撤销 不可 可用 Proxy.revocable 创建可撤销代理
Vue 框架的演进:Vue2 vs Vue3
// Vue 2 的响应式(Object.defineProperty 的局限)
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() { return val; },
set(newVal) {
val = newVal;
notifyUpdate(); // 通知更新
}
});
// 问题 1:无法检测新增属性,需 Vue.set
// 问题 2:无法检测数组索引操作和 length 变化,需重写数组方法
}
// Vue 3 的响应式(Proxy 的优势)
function reactive(target) {
return new Proxy(target, {
get(obj, key) {
track(obj, key); // 依赖收集
return obj[key];
},
set(obj, key, value) {
obj[key] = value;
trigger(obj, key); // 触发更新
}
// 自动处理新增属性、数组操作,无需特殊处理
});
}
面试标准回答模板
"Object.defineProperty 是 ES5 的属性级拦截,需要逐个属性定义 getter/setter,无法拦截新增属性、数组索引操作,需要递归处理嵌套对象。Proxy 是 ES6 的对象级代理,可以拦截对象的所有操作(get、set、has、delete 等),自动处理新增属性和数组操作,支持更多拦截器。
Vue 2 使用 Object.defineProperty,需要 Vue.set 处理新增属性,重写数组方法处理数组变更;Vue 3 改用 Proxy,解决了这些局限,代码更简洁,响应式能力更完整。但 Proxy 不兼容 IE,且性能略低于 Object.defineProperty。"
📋 第三梯队知识图谱
JavaScript 数组与对象进阶
├── 数组方法体系
│ ├── 分类记忆(8大类30+方法)
│ ├── 可变 vs 不可变(9个改变原数组)
│ ├── 遍历方法对比(for...in vs for...of, map vs forEach)
│ ├── reduce 万能场景(6大使用模式)
│ └── 实用操作(去重、扁平化)
├── 对象操作深入
│ ├── 空对象判断(边界情况处理)
│ ├── Object 本质特征(引用类型、可变性、原型链)
│ └── 属性拦截演进(defineProperty vs Proxy)
└── 迭代协议
└── Symbol.iterator 与 for...of 机制
第三梯队学习检查清单:
- 能按功能分类说出 15 个以上数组方法
- 能准确列出 9 个改变原数组的方法
- 能对比 for...in 和 for...of 的适用场景和返回差异
- 能解释 map 和 forEach 的核心区别(返回值、链式调用)
- 能用 reduce 实现至少 3 种不同场景(聚合、分组、管道)
- 能说出至少 3 种数组去重方法及适用场景
- 能手写递归和迭代两种扁平化实现
- 能处理空对象判断的边界情况(不可枚举属性、Symbol)
- 能解释 Object 与原始类型的 5 个本质区别
- 能对比 defineProperty 和 Proxy 的 6 个核心差异