本文精选 6 道前端高频 JS 面试题,覆盖数组/字符串方法、类型转换、拷贝、闭包等核心考点,每道题附带原理说明 + 代码示例 + 面试要点,建议收藏反复记忆。
一、JS 数组常用方法有哪些?
数组方法按功能可分为 增、删、查、遍历、其他 五类,面试时按类别列举不易遗漏。下面逐个解释。
分类速查
| 类别 | 方法 | 是否原地修改 |
|---|---|---|
| 增 | push、unshift、splice、concat、解构 | push/unshift/splice 是,concat/解构 否 |
| 删 | pop、shift、splice、slice | pop/shift/splice 是,slice 否 |
| 改 | splice、sort、reverse | 是 |
| 查 | indexOf、lastIndexOf、find、findIndex、includes | 否 |
| 遍历 | forEach、map、filter、some、every、reduce、reduceRight | 都不改原数组 |
| 其他 | sort、reverse、toReversed、toString、join、flat | sort/reverse 是,其余否 |
增
1. push()
在数组尾部添加一个或多个元素,原地修改,返回新长度。
const arr = [1, 2];
const len = arr.push(3, 4); // arr: [1,2,3,4], len: 4
2. unshift()
在数组头部添加一个或多个元素,原地修改,返回新长度。性能比 push 差(需移动所有元素)。
const arr = [2, 3];
arr.unshift(0, 1); // arr: [0,1,2,3]
3. splice()
万能方法,可增/删/改。原地修改,返回被删除的元素数组。
语法:splice(起始索引, 删除个数, ...插入的元素)
const arr = [1, 2, 3];
// 增:从索引1删0个,插入 'a'
arr.splice(1, 0, 'a'); // [1, 'a', 2, 3]
// 删:从索引1删1个
arr.splice(1, 1); // [1, 2, 3],返回 ['a']
// 改:从索引0删1个,插入 'x'
arr.splice(0, 1, 'x'); // ['x', 2, 3]
4. concat()
合并两个或多个数组,返回新数组,不修改原数组。
const a = [1, 2];
const b = a.concat([3, 4], [5]); // [1,2,3,4,5], a 不变
5. 解构赋值(展开运算符 ...)
ES6 语法,将数组展开后放入新数组,返回新数组,不修改原数组。
const a = [1, 2];
const b = [...a, 3, 4]; // [1,2,3,4], a 不变
删
6. pop()
删除数组尾部最后一个元素,原地修改,返回被删除的元素。空数组返回 undefined。
const arr = [1, 2, 3];
const last = arr.pop(); // last: 3, arr: [1, 2]
7. shift()
删除数组头部第一个元素,原地修改,返回被删除的元素。性能比 pop 差(需移动所有元素)。
const arr = [1, 2, 3];
const first = arr.shift(); // first: 1, arr: [2, 3]
8. slice()
截取数组的一部分,返回新数组,不修改原数组。
语法:slice(起始索引, 结束索引)(不含结束索引,支持负数)
const arr = [1, 2, 3, 4, 5];
arr.slice(1, 3); // [2, 3]
arr.slice(-2); // [4, 5] 从倒数第2个到最后
arr.slice(); // [1,2,3,4,5] 浅拷贝整个数组
splice见上方第 3 个,既能增也能删。
改
splice 也能改(见上方),此外还有:
9. sort()
对数组排序,原地修改,返回排序后的数组。默认按字符串 Unicode 排序,数字排序需传比较函数。
const nums = [3, 1, 10, 2];
nums.sort(); // [1, 10, 2, 3] ← 按字符串排,10 排在 2 前面!
nums.sort((a, b) => a - b); // [1, 2, 3, 10] ← 升序
nums.sort((a, b) => b - a); // [10, 3, 2, 1] ← 降序
10. reverse()
反转数组元素的顺序,原地修改,返回反转后的数组。
const arr = [1, 2, 3];
arr.reverse(); // [3, 2, 1]
查
11. indexOf()
从前往后查找元素,返回第一个匹配的索引,找不到返回 -1。使用严格相等 === 比较。
[1, 2, 3, 2].indexOf(2); // 1
[1, 2, 3].indexOf(4); // -1
[1, 2, 3].indexOf(2, 2); // 3(从索引2开始查)
12. lastIndexOf()
从后往前查找元素,返回最后一个匹配的索引,找不到返回 -1。
[1, 2, 3, 2].lastIndexOf(2); // 3
13. find()
返回第一个满足条件的元素,找不到返回 undefined。参数是回调函数。
const users = [{name: 'A', age: 18}, {name: 'B', age: 25}];
const adult = users.find(u => u.age >= 20); // {name: 'B', age: 25}
14. findIndex()
返回第一个满足条件的元素的索引,找不到返回 -1。参数是回调函数。
const users = [{name: 'A', age: 18}, {name: 'B', age: 25}];
const idx = users.findIndex(u => u.age >= 20); // 1
15. includes()
判断数组是否包含某元素,返回 boolean。比 indexOf() !== -1 更语义化。使用严格相等,但不能查 NaN(indexOf 也查不到 NaN,includes 可以)。
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true(includes 能识别 NaN)
[1, 2, NaN].indexOf(NaN); // -1(indexOf 查不到 NaN)
遍历
16. forEach()
遍历数组每个元素执行回调,无返回值(返回 undefined)。不能 break/continue。
[1, 2, 3].forEach((item, index, arr) => {
console.log(index, item); // 0 1 / 1 2 / 2 3
});
17. map()
遍历数组每个元素,对每个元素执行回调后返回新数组,不修改原数组。数组长度与原数组一致。
const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2); // [2, 4, 6], nums 不变
18. filter()
遍历数组,返回满足条件的元素组成的新数组,不修改原数组。
const nums = [1, 2, 3, 4, 5];
const evens = nums.filter(n => n % 2 === 0); // [2, 4]
19. some()
判断数组中是否至少有一个元素满足条件,返回 boolean。找到一个满足的就停止遍历。
const nums = [1, 2, 3, 4];
const hasEven = nums.some(n => n % 2 === 0); // true(2 满足就返回)
20. every()
判断数组中是否所有元素都满足条件,返回 boolean。找到一个不满足的就停止遍历。
const nums = [2, 4, 6];
const allEven = nums.every(n => n % 2 === 0); // true
const nums2 = [2, 4, 5];
nums2.every(n => n % 2 === 0); // false(5 不满足)
21. reduce()
从左到右遍历,将每个元素累积计算为一个值。万能方法,可做求和、计数、分组、扁平化。
语法:reduce((累加器, 当前元素, 索引, 数组) => 新累加器, 初始值)
const nums = [1, 2, 3, 4];
// 求和
const sum = nums.reduce((acc, cur) => acc + cur, 0); // 10
// 求最大值
const max = nums.reduce((acc, cur) => Math.max(acc, cur)); // 4
// 按属性分组
const users = [{role: 'admin', name: 'A'}, {role: 'user', name: 'B'}, {role: 'admin', name: 'C'}];
const grouped = users.reduce((acc, cur) => {
(acc[cur.role] = acc[cur.role] || []).push(cur.name);
return acc;
}, {});
// { admin: ['A', 'C'], user: ['B'] }
22. reduceRight()
与 reduce 相同,但从右到左遍历。适用于需要从尾部开始累积的场景。
const arr = [[1, 2], [3, 4], [5, 6]];
const flat = arr.reduceRight((acc, cur) => acc.concat(cur), []);
// [5, 6, 3, 4, 1, 2] ← 从右往左拼
其他
23. sort() / reverse()
见上方"改"部分。
24. toReversed()
ES2023 新增,返回反转后的新数组,不修改原数组。是 reverse() 的不可变版本。
const arr = [1, 2, 3];
const reversed = arr.toReversed(); // [3, 2, 1], arr 仍为 [1, 2, 3]
25. toString()
将数组转换为字符串,元素之间用逗号拼接。等价于 join(',')。
[1, 2, 3].toString(); // '1,2,3'
['a', 'b'].toString(); // 'a,b'
26. join()
将数组所有元素用指定分隔符拼接成字符串,返回字符串,不修改原数组。
[1, 2, 3].join('-'); // '1-2-3'
[1, 2, 3].join(''); // '123'
[1, 2, 3].join(', '); // '1, 2, 3'
[1, 2, 3].join(); // '1,2,3'(默认逗号)
27. flat()
将嵌套数组"拍平"到指定深度,返回新数组,不修改原数组。默认深度为 1。传 Infinity 可拍平任意深度。
const arr = [1, [2, [3, [4]]]];
arr.flat(); // [1, 2, [3, [4]]] 默认深度1
arr.flat(2); // [1, 2, 3, [4]] 深度2
arr.flat(Infinity); // [1, 2, 3, 4] 全部拍平
// 常配合 map 使用
const nums = [1, 2, 3];
nums.map(n => [n, n * 2]).flat(); // [1, 2, 2, 4, 3, 6]
// ES2019 也可用 flatMap()
nums.flatMap(n => [n, n * 2]); // 同上
面试要点
sort默认按字符串排序:[3, 1, 10].sort()→[1, 10, 3],需传比较函数(a, b) => a - bmapvsforEach:前者有返回值不修改原数组,后者无返回值reduce万能累加:可以求和、计数、扁平化数组、按属性分组includesvsindexOf:includes能正确识别NaN,indexOf不能slicevssplice:slice只截取不改原数组,splice增删改都原地修改push/popvsunshift/shift:前者操作尾部 O(1),后者操作头部 O(n)
二、字符串常用方法有哪些?
原理分类
字符串方法与数组类似,但字符串是原始类型,所有方法都不修改原字符串,返回新值。
分类速查
| 类别 | 方法 |
|---|---|
| 增 | concat(拼接,不如用 +) |
| 删 | slice(start, end)、substring(start, end) |
| 改 | replace、trim、trimStart/trimEnd、padStart/padEnd、toLowerCase/toUpperCase |
| 查 | indexOf、lastIndexOf、includes、startsWith、endsWith |
| 其他 | split(转数组)、match(正则匹配)、search(正则查找) |
代码示例
const str = ' Hello World ';
// 删/截取
str.slice(0, 5); // 'Hello' 支持负数
str.substring(0, 5); // 'Hello' 不支持负数
// 改
str.trim(); // 'Hello World'
str.replace('World', 'JS'); // ' Hello JS '
str.padStart(15, '*'); // '*** Hello World '
str.toLowerCase(); // ' hello world '
// 查
str.includes('Hello'); // true
str.startsWith(' '); // true
str.indexOf('World'); // 8
// 其他
str.split(' '); // 按空格拆数组
'abc123'.match(/\d+/); // ['123']
面试要点
slicevssubstring:slice支持负数索引,substring会自动交换大小参数replace默认只替换第一个:全局替换需用正则/pattern/g- 字符串不可变性:
str[0] = 'h'无效,不会报错但不生效
三、JS 类型转换机制
原理
JS 类型分为 原始类型 和 引用类型,类型转换分为 隐式转换 和 显式转换。
类型总览
原始类型(7种):string, number, boolean, null, undefined, symbol, bigint
引用类型:object(含 array, function, date, regexp, Map, Set 等)
隐式转换规则
隐式转换发生在运算符、比较、条件语句中,核心规则如下:
| 场景 | 转换方向 |
|---|---|
| 条件语句(if/while/三元) | 其他类型 → boolean |
算术运算(-, *, /) | 其他类型 → number |
+ 二元运算 | 若任一为 string → 拼接;否则 → number |
| 引用类型 → 原始类型 | 调用 ToPrimitive() → valueOf() → toString() |
ToPrimitive 转换流程
// 引用类型转原始类型时,V8 内部调用 ToPrimitive(obj)
// 步骤:
// 1. 先调 obj.valueOf(),若返回原始类型则用它
// 2. 否则调 obj.toString(),若返回原始类型则用它
// 3. 都不是则报错 TypeError
toString 的三个版本
// 1. Object.prototype.toString
({}).toString(); // '[object Object]'
// 2. Array.prototype.toString → 逗号拼接
[1, 2, 3].toString(); // '1,2,3'
// 3. 包装类的 toString → 直接值
new String('abc').toString(); // 'abc'
new Number(42).toString(); // '42'
代码示例
// 条件隐式转 boolean
if ({}) console.log('对象都是truthy'); // 输出,任何引用类型转boolean都是true
// + 运算符的特殊行为
console.log(1 + '2'); // '12' 有string就拼接
console.log(1 + 2 + '3'); // '33' 先算 1+2=3,再 3+'3'='33'
console.log('1' + 2 + 3); // '123' 第一步就变成字符串了
// 算术运算隐式转 number
console.log('5' - 3); // 2
console.log(true + 1); // 2 (true→1)
console.log(null + 1); // 1 (null→0)
console.log(undefined + 1); // NaN (undefined→NaN)
// 显式转换
Number('123'); // 123
String(123); // '123'
Boolean(0); // false
面试要点
+和-的区别:+遇到 string 就拼接,-一律做减法null和undefined的 number 转换:null→0,undefined→NaN- 空数组
[]:[] + []→'' + ''→'';[] + {}→'' + '[object Object]'→'[object Object]'
四、== 与 === 的区别
原理
==(宽松相等) | ===(严格相等) | |
|---|---|---|
| 转换 | 会进行类型转换再比较 | 不转换,直接比较类型和值 |
| 适用 | 基本不用(除了判断 null/undefined) | 优先使用 |
== 隐式转换规则
比较时按以下顺序尝试转换:
1. null == undefined → true(特殊规则)
2. number 和 string → 把 string 转 number 再比
3. boolean 和其他 → 把 boolean 转 number 再比
4. 引用类型和原始类型 → 把引用类型转原始类型(ToPrimitive)再比
5. 同类型 → 直接比值
经典面试题
// 以下表达式的结果?
console.log(null == undefined); // true (特殊规则,记住就行)
console.log(null === undefined); // false(类型不同)
console.log(1 == '1'); // true('1'→1)
console.log(1 === '1'); // false
console.log(true == 1); // true(true→1)
console.log(true === 1); // false
console.log([] == false); // true
// 解释:[] → ''(toString),'' → 0(ToNumber),false → 0,0==0 → true
console.log({} == '[object Object]'); // true
// 解释:{} 调 toString() → '[object Object]',同值
面试要点
- 推荐始终使用
===,避免隐式转换的坑 ==唯一合理的使用场景:x == null同时判断null和undefinedNaN不等于任何值,包括它自己:NaN === NaN→false,需用Number.isNaN()判断
五、JS 中的拷贝
原理
JS 类型决定了拷贝行为不同:
原始类型 → 值存在调用栈 → 拷贝就是赋值,天然深拷贝
引用类型 → 值存在堆内存,栈中存引用地址 → 拷贝分浅/深
- 浅拷贝:只拷贝第一层,引用类型的属性只复制地址,新旧对象共享堆内存
- 深拷贝:层层递归拷贝,每层都创建新的堆内存,互不影响
浅拷贝方法
const obj = { a: 1, b: { c: 2 } };
// 1. 解构赋值
const s1 = { ...obj };
// 2. Object.assign
const s2 = Object.assign({}, obj);
// 3. slice(数组)
const arr = [1, 2, [3, 4]];
const s3 = arr.slice();
// 4. concat(数组)
const s4 = [].concat(arr);
// 验证:改嵌套属性会互相影响
s1.b.c = 999;
console.log(obj.b.c); // 999 ← 原对象也变了!
深拷贝方法
const obj = { a: 1, b: { c: 2 }, d: [1, 2] };
// 1. JSON 序列化(最快但有限制)
const d1 = JSON.parse(JSON.stringify(obj));
// ⚠️ 不能处理:函数、symbol、undefined、bigint、循环引用、Date、RegExp
// 2. structuredClone(原生 API,推荐)
const d2 = structuredClone(obj);
// ⚠️ 不能处理:函数、Symbol
// ✅ 支持:Date、RegExp、Map、Set、循环引用
// 3. MessageChannel(可以处理函数以外的大部分)
function deepClone(obj) {
return new Promise(resolve => {
const { port1, port2 } = new MessageChannel();
port2.onmessage = e => resolve(e.data);
port1.postMessage(obj);
});
}
手写深拷贝原理
function deepClone(target, map = new WeakMap()) {
// 原始类型直接返回
if (typeof target !== 'object' || target === null) return target;
// 循环引用检测
if (map.has(target)) return map.get(target);
// 创建新对象(数组或对象)
const clone = Array.isArray(target) ? [] : {};
map.set(target, clone);
// 递归拷贝每个属性
for (const key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], map);
}
}
return clone;
}
面试要点
- JSON 方案的局限:面试常问不能拷贝什么,函数/symbol/undefined/bigint/循环引用是高频点
structuredClone:ES2022 原生支持,除了函数和 Symbol 基本都能处理,推荐优先使用- 循环引用:手写深拷贝必须用
WeakMap记录已拷贝对象,否则会栈溢出
六、JS 中的闭包
原理
闭包 = 函数 + 它被创建时的词法环境
正常情况:函数执行完毕 → 执行上下文销毁 → 内部变量消失
闭包情况:内部函数引用了外部函数的变量 → 外部函数执行完毕后,
V8 不会销毁这些变量,而是把它们保存在一个"变量集合"中 → 这个集合就是闭包
产生条件
- 函数 A 内部定义了函数 B
- 函数 B 引用了函数 A 的变量
- 函数 B 被返回或传递出去(让它在 A 外部执行)
代码示例
// 经典计数器闭包
function counter() {
let count = 0; // 被闭包捕获的变量
return {
inc() { return ++count; },
dec() { return --count; },
get() { return count; }
};
}
const c = counter();
c.inc(); // 1
c.inc(); // 2
c.dec(); // 1
// count 被永久保存在闭包中,外部无法直接访问
实际应用场景
// 1. 封装私有变量(模拟 class 的 private)
const bankAccount = (function() {
let balance = 0; // 私有变量
return {
deposit(n) { balance += n; },
withdraw(n) { balance -= n; },
getBalance() { return balance; }
};
})();
// 2. 防抖(debounce)
function debounce(fn, delay) {
let timer = null; // 闭包保存 timer
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 3. 节流(throttle)
function throttle(fn, interval) {
let lastTime = 0; // 闭包保存上次执行时间
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
经典坑:循环闭包
// ❌ 坑:三个按钮都打印 "按钮3"
for (var i = 0; i < 3; i++) {
document.getElementById('btn' + i).onclick = function() {
console.log('按钮' + i); // i 是同一个,循环结束后 i=3
};
}
// ✅ 解法1:用 let(块级作用域,每次循环 i 都是新的)
for (let i = 0; i < 3; i++) {
btn.onclick = function() { console.log('按钮' + i); };
}
// ✅ 解法2:IIFE 闭包
for (var i = 0; i < 3; i++) {
(function(index) {
btn.onclick = function() { console.log('按钮' + index); };
})(i);
}
闭包的优缺点
| 优点 | 缺点 |
|---|---|
| 封装私有变量 | 变量常驻内存,可能内存泄漏 |
| 延长变量生命周期 | 滥用会增加内存消耗 |
| 实现模块化(IIFE) | 调试时作用域链复杂 |
面试要点
- 闭包的本质:函数捕获了定义时的词法环境,导致变量无法被 GC 回收
- 内存泄漏场景:闭包持有大量数据的 DOM 引用未释放,需手动置 null
- 防抖节流必问闭包:
timer和lastTime都依赖闭包持久化
总结
以上 6 道题是 JS 面试的核心高频考点,记忆建议:
- 数组/字符串方法 → 按增删改查遍历分类记,不容易漏
- 类型转换 → 记住
ToPrimitive流程和+的拼接特性 ==vs===→ 优先===,特殊情况(null/undefined)例外- 拷贝 → 区分浅/深,知道 JSON 和 structuredClone 的局限
- 闭包 → 理解原理,能手写防抖节流,能解释循环坑
下一期分享 作用域、原型链、继承、this 四道经典题,敬请关注 👋