高频 JS 面试题,答完这些就够了

0 阅读13分钟

本文精选 6 道前端高频 JS 面试题,覆盖数组/字符串方法、类型转换、拷贝、闭包等核心考点,每道题附带原理说明 + 代码示例 + 面试要点,建议收藏反复记忆。


一、JS 数组常用方法有哪些?

数组方法按功能可分为 增、删、查、遍历、其他 五类,面试时按类别列举不易遗漏。下面逐个解释。

分类速查

类别方法是否原地修改
pushunshiftspliceconcat、解构push/unshift/splice 是,concat/解构 否
popshiftspliceslicepop/shift/splice 是,slice 否
splicesortreverse
indexOflastIndexOffindfindIndexincludes
遍历forEachmapfiltersomeeveryreducereduceRight都不改原数组
其他sortreversetoReversedtoStringjoinflatsort/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 更语义化。使用严格相等,但不能查 NaNindexOf 也查不到 NaNincludes 可以)。

[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 - b
  • map vs forEach:前者有返回值不修改原数组,后者无返回值
  • reduce 万能累加:可以求和、计数、扁平化数组、按属性分组
  • includes vs indexOfincludes 能正确识别 NaNindexOf 不能
  • slice vs spliceslice 只截取不改原数组,splice 增删改都原地修改
  • push/pop vs unshift/shift:前者操作尾部 O(1),后者操作头部 O(n)

二、字符串常用方法有哪些?

原理分类

字符串方法与数组类似,但字符串是原始类型,所有方法都不修改原字符串,返回新值。

分类速查

类别方法
concat(拼接,不如用 +
slice(start, end)substring(start, end)
replacetrimtrimStart/trimEndpadStart/padEndtoLowerCase/toUpperCase
indexOflastIndexOfincludesstartsWithendsWith
其他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']

面试要点

  • slice vs substringslice 支持负数索引,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 就拼接,- 一律做减法
  • nullundefined 的 number 转换null→0undefined→NaN
  • 空数组 [][] + []'' + ''''[] + {}'' + '[object Object]''[object Object]'

四、===== 的区别

原理

==(宽松相等)===(严格相等)
转换会进行类型转换再比较不转换,直接比较类型和值
适用基本不用(除了判断 null/undefined)优先使用

== 隐式转换规则

比较时按以下顺序尝试转换:

1. null == undefinedtrue(特殊规则)
2. numberstring   → 把 stringnumber 再比
3. boolean 和其他     → 把 booleannumber 再比
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 同时判断 nullundefined
  • NaN 不等于任何值,包括它自己:NaN === NaNfalse,需用 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 不会销毁这些变量,而是把它们保存在一个"变量集合"中 → 这个集合就是闭包

产生条件

  1. 函数 A 内部定义了函数 B
  2. 函数 B 引用了函数 A 的变量
  3. 函数 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
  • 防抖节流必问闭包timerlastTime 都依赖闭包持久化

总结

以上 6 道题是 JS 面试的核心高频考点,记忆建议:

  1. 数组/字符串方法 → 按增删改查遍历分类记,不容易漏
  2. 类型转换 → 记住 ToPrimitive 流程和 + 的拼接特性
  3. == vs === → 优先 ===,特殊情况(null/undefined)例外
  4. 拷贝 → 区分浅/深,知道 JSON 和 structuredClone 的局限
  5. 闭包 → 理解原理,能手写防抖节流,能解释循环坑

下一期分享 作用域、原型链、继承、this 四道经典题,敬请关注 👋