ES6+ 高频面试题

ES6+ 高频面试题

1. let、const、var 的区别?

这是 ES6 面试中出现频率最高的题目之一。

特性 var let const
作用域 函数作用域 块级 作用域 {} 块级 作用域 {}
变量提升 ✅(值为 undefined ❌(暂时性死区 TDZ) ❌(暂时性死区 TDZ)
重复声明 ✅ 允许 ❌ 报错 ❌ 报错
重新赋值 ✅ 允许 ✅ 允许 ❌ 报错
全局属性 ✅(挂到 window
必须初始化
javascript 复制代码
// ✅ var 的问题1:函数作用域(没有块级作用域)
if (true) {
  var x = 1
}
console.log(x)  // 1(var 穿透了 if 块)

if (true) {
  let y = 1
}
console.log(y)  // ReferenceError(let 只在块内有效)

// ✅ var 的问题2:经典循环问题
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)  // 输出 3 3 3
  // 原因:var 没有块级作用域,3 个回调共享同一个 i,循环结束后 i = 3
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)  // 输出 0 1 2
  // 原因:let 有块级作用域,每轮循环都创建新的 i
}

// ✅ const 不是"常量",是"不可重新赋值"
const obj = { name: '张三' }
obj.name = '李四'  // ✅ 可以修改属性(对象的引用地址没变)
// obj = {}        // ❌ TypeError(不能重新赋值给新对象)

// 真正的"冻结"需要 Object.freeze
const frozen = Object.freeze({ name: '张三', address: { city: '北京' } })
frozen.name = '李四'         // 无效(严格模式报错)
frozen.address.city = '上海'  // ⚠️ 有效!Object.freeze 是浅冻结

// ✅ 暂时性死区(Temporal Dead Zone)详解
let a = 1
{
  // 这里开始就是 a 的 TDZ(虽然外层有 a,但内层的 let a 已经"占位")
  console.log(a)  // ReferenceError(不是 undefined!)
  let a = 2       // TDZ 结束
}
// TDZ 的意义:让错误更早暴露,避免 var 的"undefined 陷阱"

最佳实践:

  • 默认使用 const(明确不可重新赋值的意图)
  • 需要重新赋值时使用 let
  • 永远不要使用 var

💡 面试加分点: let/const 在底层也会被"提升"(在编译阶段分配内存),但在声明语句之前处于 TDZ,不同于 var 的提升为 undefinedfor 循环中 let 的特殊行为:JS 引擎会为每次迭代创建一个新的绑定,这是规范级别的设计,不是简单的闭包。


2. 箭头函数和普通函数的区别?

特性 普通函数 function 箭头函数 () => {}
this 指向 调用时动态确定 定义时继承外层 this(词法 this)
arguments 对象 ✅ 有 ❌ 没有(用 ...rest 替代)
new 调用 ✅ 可以当构造函数 ❌ 不能 new(没有 [[Construct]]
prototype ✅ 有 ❌ 没有
call/apply/bind 改变 this ✅ 有效 ❌ 无效
yield ✅ 可以做 Generator ❌ 不能
函数名推断 function.name ✅ 也有(const fn = () => {}; fn.name === 'fn'
javascript 复制代码
// ✅ 1. this 指向不同(最核心的区别)
const obj = {
  name: '张三',
  greet: function() { return this.name },       // '张三'(this = obj)
  greetArrow: () => this?.name,                 // undefined(this = 外层/全局)
}

// ✅ 箭头函数的 this 在定义时就固定了
function Timer() {
  this.seconds = 0
  // ❌ 普通函数:this 指向 window(setTimeout 的回调是独立调用)
  setInterval(function() { this.seconds++ }, 1000)  // this !== Timer 实例
  // ✅ 箭头函数:继承 Timer 构造函数中的 this
  setInterval(() => { this.seconds++ }, 1000)       // this === Timer 实例
}

// ✅ 2. 没有 arguments 对象
function normal() { console.log(arguments) }  // ✅ Arguments 对象
const arrow = (...args) => console.log(args)  // ✅ 用 rest 参数替代

// ✅ 3. 不能作为构造函数
const Foo = () => {}
// new Foo()  // TypeError: Foo is not a constructor

// ✅ 4. 没有 prototype
console.log((() => {}).prototype)  // undefined
console.log((function(){}).prototype)  // {} 有 prototype

// ✅ 5. call/apply/bind 不能改变箭头函数的 this
const fn = () => this
fn.call({ name: '李四' })  // 仍然是外层 this

// ========== 什么时候用普通函数,什么时候用箭头函数? ==========
// ✅ 用箭头函数:回调函数、数组方法、需要继承外层 this 时
arr.map(item => item * 2)
setTimeout(() => console.log('done'), 1000)

// ✅ 用普通函数:对象方法、需要动态 this、构造函数、Generator
const obj2 = {
  name: '张三',
  greet() { return this.name }  // ✅ 方法简写,this 指向 obj2
}

💡 面试加分点: 箭头函数适合用在需要固定 this 的场景(回调、事件处理),不适合用在需要动态 this 的场景(对象方法、原型方法)。Vue 的 methods 中不能用箭头函数(因为需要 this 指向组件实例),React 的函数组件中推荐用箭头函数。


3. 解构赋值的用法?

javascript 复制代码
// 数组解构
const [a, b, c] = [1, 2, 3]
const [first, , third] = [1, 2, 3]
const [x = 10, y = 20] = [1]        // 默认值:x=1, y=20
const [head, ...tail] = [1, 2, 3, 4] // head=1, tail=[2,3,4]

// 对象解构
const { name, age } = { name: '张三', age: 25 }
const { name: userName, age: userAge = 18 } = { name: '李四' }  // 重命名 + 默认值
const { a: { b: nested } } = { a: { b: 'deep' } }  // 嵌套解构

// 函数参数解构
function greet({ name, age = 18, role = 'user' } = {}) {
  return `${name}(${age}) - ${role}`
}
greet({ name: '张三', age: 25 })  // '张三(25) - user'

// 交换变量
let m = 1, n = 2;
[m, n] = [n, m]  // m=2, n=1

// 实际应用:API 响应解构
const { data: { list, total }, code, message } = await fetchList()

4. 扩展运算符(...)的用法?

javascript 复制代码
// 数组展开
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const merged = [...arr1, ...arr2]  // [1, 2, 3, 4, 5, 6]
const copy = [...arr1]             // 浅拷贝

// 对象展开
const obj1 = { a: 1, b: 2 }
const obj2 = { c: 3, d: 4 }
const mergedObj = { ...obj1, ...obj2 }  // { a:1, b:2, c:3, d:4 }
const updated = { ...obj1, b: 99 }      // { a:1, b:99 }(覆盖)

// rest 参数(收集剩余参数)
function sum(first, ...rest) {
  return rest.reduce((acc, val) => acc + val, first)
}
sum(1, 2, 3, 4)  // 10

// 将类数组转为数组
const nodeList = document.querySelectorAll('div')
const arr = [...nodeList]

// 字符串转数组
const chars = [..."hello"]  // ['h', 'e', 'l', 'l', 'o']

5. 模板字符串的用法?

javascript 复制代码
const name = '张三'
const age = 25

// 基本插值
const greeting = `Hello, ${name}! You are ${age} years old.`

// 多行字符串
const html = `
  <div class="card">
    <h2>${name}</h2>
    <p>Age: ${age}</p>
  </div>
`

// 表达式
const result = `${1 + 2 + 3}`  // '6'
const status = `Status: ${age >= 18 ? 'adult' : 'minor'}`

// 标签模板(高级用法)
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] ? `<strong>${values[i]}</strong>` : '')
  }, '')
}
const msg = highlight`Hello, ${name}! You are ${age} years old.`

6. Symbol 的用途?

javascript 复制代码
// 创建唯一标识符
const id1 = Symbol('id')
const id2 = Symbol('id')
id1 === id2  // false

// 作为对象属性键(避免命名冲突)
const USER_ID = Symbol('userId')
const user = { name: '张三', [USER_ID]: 12345 }
user[USER_ID]  // 12345

// Symbol 属性不会被普通枚举
Object.keys(user)  // ['name'](不包含 Symbol)
Object.getOwnPropertySymbols(user)  // [Symbol(userId)]

// 内置 Symbol(Well-known Symbols)
class MyArray {
  [Symbol.iterator]() {
    let index = 0
    const data = [1, 2, 3]
    return {
      next: () => index < data.length
        ? { value: data[index++], done: false }
        : { done: true }
    }
  }
}
for (const item of new MyArray()) {
  console.log(item)  // 1, 2, 3
}

7. Map 和 Set 的用法?

javascript 复制代码
// Set:不重复的值集合
const set = new Set([1, 2, 3, 2, 1])
console.log(set)  // Set {1, 2, 3}
set.add(4)
set.delete(1)
set.has(2)   // true
set.size     // 3

// 数组去重
const unique = [...new Set([1, 2, 2, 3, 3])]  // [1, 2, 3]

// 求交集、并集、差集
const a = new Set([1, 2, 3, 4])
const b = new Set([3, 4, 5, 6])
const union = new Set([...a, ...b])          // 并集:{1,2,3,4,5,6}
const intersection = new Set([...a].filter(x => b.has(x)))  // 交集:{3,4}
const difference = new Set([...a].filter(x => !b.has(x)))   // 差集:{1,2}

// Map:键值对集合(键可以是任意类型)
const map = new Map()
map.set('name', '张三')
map.set(1, 'number key')
map.set({ id: 1 }, 'object key')
map.get('name')   // '张三'
map.has('name')   // true
map.size          // 3

// 遍历
map.forEach((value, key) => console.log(key, value))
for (const [key, value] of map) {
  console.log(key, value)
}

8. Proxy 和 Reflect 的用法?

javascript 复制代码
const handler = {
  get(target, key) {
    console.log(`读取属性: ${key}`)
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    if (typeof value !== 'number') throw new TypeError('只能设置数字')
    return Reflect.set(target, key, value)
  },
}

const obj = new Proxy({ count: 0 }, handler)
obj.count        // 读取属性: count
obj.count = 10   // 设置属性: count = 10

// Vue3 响应式原理就是基于 Proxy
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)  // 依赖收集
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      const result = Reflect.set(target, key, value)
      trigger(target, key)  // 触发更新
      return result
    },
  })
}

9. 迭代器(Iterator)和生成器(Generator)?

javascript 复制代码
// 生成器函数:用 function* 定义,yield 暂停执行
function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i
  }
}

for (const num of range(0, 10, 2)) {
  console.log(num)  // 0, 2, 4, 6, 8
}

// 无限序列
function* fibonacci() {
  let [a, b] = [0, 1]
  while (true) {
    yield a;
    [a, b] = [b, a + b]
  }
}
const fib = fibonacci()
fib.next().value  // 0
fib.next().value  // 1
fib.next().value  // 1
fib.next().value  // 2

// 异步生成器
async function* fetchPages(url) {
  let page = 1
  while (true) {
    const data = await fetch(`${url}?page=${page++}`).then(r => r.json())
    if (!data.length) break
    yield data
  }
}

for await (const page of fetchPages('/api/users')) {
  console.log(page)
}

10. 可选链(?.)和空值合并(??)运算符?

javascript 复制代码
// 可选链 ?.:安全访问深层属性
const user = { profile: { address: { city: '北京' } } }
const city = user?.profile?.address?.city  // '北京'
const zip = user?.profile?.address?.zip   // undefined(不报错)

// 可选链调用方法
user?.profile?.getAvatar?.()

// 可选链访问数组
const firstItem = arr?.[0]

// 空值合并 ??:只有 null 和 undefined 才使用默认值
const count = 0 ?? 10   // 0(0 不是 null/undefined)
const name = '' ?? '匿名' // ''(空字符串不是 null/undefined)
const val = null ?? '默认'  // '默认'

// 对比 ||:0、''、false 都会使用默认值
const count2 = 0 || 10   // 10(0 是 falsy)

// 结合使用
const displayName = user?.profile?.nickname ?? user?.name ?? '匿名用户'

11. Class 类的用法?

javascript 复制代码
class Animal {
  // 静态属性
  static count = 0

  // 私有属性(ES2022)
  #name
  #age

  constructor(name, age) {
    this.#name = name
    this.#age = age
    Animal.count++
  }

  // getter/setter
  get name() { return this.#name }
  set name(value) {
    if (typeof value !== 'string') throw new TypeError('名字必须是字符串')
    this.#name = value
  }

  // 实例方法
  speak() {
    return `${this.#name} makes a sound.`
  }

  // 静态方法
  static create(name, age) {
    return new Animal(name, age)
  }

  // toString
  toString() {
    return `Animal(${this.#name}, ${this.#age})`
  }
}

// 继承
class Dog extends Animal {
  #breed

  constructor(name, age, breed) {
    super(name, age)  // 必须先调用 super
    this.#breed = breed
  }

  speak() {
    return `${this.name} barks!`  // 通过 getter 访问
  }

  // 调用父类方法
  parentSpeak() {
    return super.speak()
  }
}

const dog = new Dog('Rex', 3, 'Labrador')
dog.speak()       // 'Rex barks!'
dog.parentSpeak() // 'Rex makes a sound.'
Animal.count      // 1

12. 模块化(import/export)的用法?

javascript 复制代码
// 命名导出
export const PI = 3.14159
export function add(a, b) { return a + b }
export class Calculator { ... }

// 默认导出
export default class App { ... }

// 重新导出
export { add as sum } from './math.js'
export * from './utils.js'

// 命名导入
import { PI, add } from './math.js'
import { add as sum } from './math.js'  // 重命名

// 默认导入
import App from './App.js'

// 混合导入
import App, { PI, add } from './module.js'

// 导入所有
import * as math from './math.js'
math.PI   // 3.14159
math.add(1, 2)  // 3

// 动态导入(懒加载)
const loadModule = async () => {
  const { default: App } = await import('./App.js')
  return App
}

// 条件导入
const module = await import(condition ? './moduleA.js' : './moduleB.js')

13. 什么是 for...of 和 for...in?

javascript 复制代码
// for...in:遍历对象的可枚举属性(包括继承的)
const obj = { a: 1, b: 2, c: 3 }
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {  // 过滤继承属性
    console.log(key, obj[key])
  }
}

// for...of:遍历可迭代对象(Array、String、Map、Set、Generator)
const arr = [1, 2, 3]
for (const item of arr) {
  console.log(item)
}

// 遍历字符串
for (const char of 'hello') {
  console.log(char)  // h, e, l, l, o
}

// 遍历 Map
const map = new Map([['a', 1], ['b', 2]])
for (const [key, value] of map) {
  console.log(key, value)
}

// 遍历 Set
const set = new Set([1, 2, 3])
for (const item of set) {
  console.log(item)
}

// 带索引遍历数组
for (const [index, value] of arr.entries()) {
  console.log(index, value)
}

14. 什么是 Object 的新方法?

javascript 复制代码
// Object.assign:浅合并
const target = { a: 1 }
const source = { b: 2, c: 3 }
Object.assign(target, source)  // { a:1, b:2, c:3 }

// Object.keys / values / entries
const obj = { a: 1, b: 2, c: 3 }
Object.keys(obj)    // ['a', 'b', 'c']
Object.values(obj)  // [1, 2, 3]
Object.entries(obj) // [['a',1], ['b',2], ['c',3]]

// Object.fromEntries:entries 转对象
const entries = [['a', 1], ['b', 2]]
Object.fromEntries(entries)  // { a: 1, b: 2 }

// Map 转对象
const map = new Map([['a', 1], ['b', 2]])
Object.fromEntries(map)  // { a: 1, b: 2 }

// Object.freeze:冻结对象(浅冻结)
const frozen = Object.freeze({ a: 1, b: { c: 2 } })
frozen.a = 99      // 无效(严格模式报错)
frozen.b.c = 99    // 有效(浅冻结,嵌套对象未冻结)

// Object.create:创建指定原型的对象
const proto = { greet() { return `Hello, ${this.name}` } }
const person = Object.create(proto)
person.name = '张三'
person.greet()  // 'Hello, 张三'

// Object.getOwnPropertyDescriptor
const desc = Object.getOwnPropertyDescriptor(obj, 'a')
// { value: 1, writable: true, enumerable: true, configurable: true }

15. 什么是 Array 的新方法?

javascript 复制代码
const arr = [1, 2, 3, 4, 5]

// ES6+
arr.find(n => n > 3)       // 4(第一个满足条件的元素)
arr.findIndex(n => n > 3)  // 3(第一个满足条件的索引)
arr.includes(3)            // true
arr.flat()                 // 展平一层
[[1, 2], [3, [4, 5]]].flat(Infinity)  // [1, 2, 3, 4, 5]
arr.flatMap(n => [n, n * 2])  // [1,2, 2,4, 3,6, 4,8, 5,10]

// ES2022+
arr.at(-1)   // 5(支持负索引)
arr.at(-2)   // 4

// ES2023+
arr.findLast(n => n < 4)       // 3(从后往前找)
arr.findLastIndex(n => n < 4)  // 2

// 不修改原数组的新方法(ES2023)
arr.toSorted((a, b) => b - a)  // [5,4,3,2,1](不修改原数组)
arr.toReversed()               // [5,4,3,2,1](不修改原数组)
arr.toSpliced(1, 2, 99)        // [1,99,4,5](不修改原数组)
arr.with(2, 99)                // [1,2,99,4,5](替换指定索引)

// Array.from
Array.from({ length: 5 }, (_, i) => i)  // [0, 1, 2, 3, 4]
Array.from('hello')  // ['h', 'e', 'l', 'l', 'o']
Array.from(new Set([1, 2, 3]))  // [1, 2, 3]

16. Promise 的高级用法有哪些?

javascript 复制代码
// ✅ Promise 并发控制(限制同时执行的 Promise 数量)
async function concurrentLimit(tasks, limit) {
  const results = []
  const executing = new Set()

  for (const [index, task] of tasks.entries()) {
    const promise = Promise.resolve().then(() => task())
    results[index] = promise
    executing.add(promise)

    const clean = () => executing.delete(promise)
    promise.then(clean, clean)

    if (executing.size >= limit) {
      await Promise.race(executing)  // 等最快完成的一个
    }
  }

  return Promise.all(results)
}

// 使用:同时最多 3 个请求
const tasks = urls.map(url => () => fetch(url))
const results = await concurrentLimit(tasks, 3)

// ✅ Promise 串行执行
async function serial(tasks) {
  const results = []
  for (const task of tasks) {
    results.push(await task())
  }
  return results
}

// ✅ Promise 超时处理
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`超时 ${ms}ms`)), ms)
  )
  return Promise.race([promise, timeout])
}
await withTimeout(fetch('/api/data'), 5000)

// ✅ Promise 重试机制
async function retry(fn, retries = 3, delay = 1000) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn()
    } catch (err) {
      if (i === retries - 1) throw err
      await new Promise(r => setTimeout(r, delay * (i + 1)))  // 指数退避
    }
  }
}
await retry(() => fetch('/api/data'), 3, 1000)

💡 面试加分点: Promise 并发控制是面试手写题的高频考点。实际项目中可以使用 p-limitp-queue 等库。Promise 重试 + 指数退避(exponential backoff)是网络请求的最佳实践。


17. 什么是 WeakRef 和 FinalizationRegistry?

ES2021 引入的弱引用和垃圾回收回调机制,适合实现高级缓存资源清理

javascript 复制代码
// ✅ WeakRef:创建对象的弱引用(不阻止垃圾回收)
let target = { data: '重要数据' }
const weakRef = new WeakRef(target)

weakRef.deref()  // { data: '重要数据' }(对象还在)
target = null    // 移除强引用
// 某次 GC 后...
weakRef.deref()  // undefined(对象已被回收)

// ✅ FinalizationRegistry:对象被回收时收到通知
const registry = new FinalizationRegistry((value) => {
  console.log(`对象 "${value}" 已被垃圾回收`)
  // 可以在这里清理关联的资源(如关闭连接、释放文件句柄)
})

let obj = { name: '张三' }
registry.register(obj, '张三的数据')  // 注册监听
obj = null  // GC 后会触发回调

// ✅ 实际应用:基于 WeakRef 的缓存
class WeakCache {
  #cache = new Map()
  #registry = new FinalizationRegistry((key) => {
    this.#cache.delete(key)  // 对象被 GC 后自动清理缓存条目
  })

  set(key, value) {
    this.#cache.set(key, new WeakRef(value))
    this.#registry.register(value, key)
  }

  get(key) {
    const ref = this.#cache.get(key)
    return ref?.deref()  // 可能返回 undefined(已被回收)
  }
}

💡 面试加分点: WeakRefFinalizationRegistry 不保证回调何时执行(取决于 GC 策略),因此不能依赖它们做关键逻辑。主要用于性能优化(如缓存大对象、避免内存泄漏),不能替代显式的资源管理。


18. 什么是 structuredClone?

structuredClone (ES2022)是全局方法,用于执行深拷贝,基于浏览器的结构化克隆算法。

javascript 复制代码
// ✅ 基本使用
const original = {
  name: '张三',
  date: new Date(),
  regex: /test/g,
  nested: { a: { b: { c: 1 } } },
  arr: [1, [2, [3]]],
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
}
original.self = original  // 循环引用

const cloned = structuredClone(original)
cloned.nested.a.b.c = 99
console.log(original.nested.a.b.c)  // 1(完全独立)
console.log(cloned.self === cloned)  // true(循环引用正确处理)

// ✅ 支持的类型
// Date、RegExp、Map、Set、ArrayBuffer、TypedArray
// Blob、File、ImageData、循环引用

// ❌ 不支持的类型(会报错)
// Function、Symbol、DOM 节点、Error 对象
// structuredClone({ fn: () => {} })  // DataCloneError

// ✅ 对比其他深拷贝方案
// JSON.parse(JSON.stringify(x))  → 不支持 Date/RegExp/循环引用/undefined
// lodash.cloneDeep(x)            → 功能最全,但需要引入库
// structuredClone(x)             → 原生、快速、支持大部分类型 ✅

💡 面试加分点: structuredClone 可以传递第二个参数 { transfer }转移(而非克隆)ArrayBuffer,实现零拷贝传输,性能极高。


19. 什么是 Top-level await?

ES2022 允许在模块的顶层使用 await,无需包裹在 async 函数中。

javascript 复制代码
// ✅ 模块中可以直接使用 await(ES Module 中)
// config.js
const response = await fetch('/api/config')
export const config = await response.json()

// app.js
import { config } from './config.js'
// config 已经是解析后的数据,不需要额外 await
console.log(config.apiUrl)

// ✅ 实际应用场景

// 1. 动态导入
const locale = navigator.language
const messages = await import(`./i18n/${locale}.js`)

// 2. 条件导入
let db
if (process.env.NODE_ENV === 'production') {
  db = await import('./db-production.js')
} else {
  db = await import('./db-development.js')
}

// 3. 资源初始化
const connection = await createDatabaseConnection()
export { connection }

// ⚠️ 注意:Top-level await 只能在 ES Module 中使用
// 不能在 CommonJS(require)或普通 <script> 中使用
// 需要 <script type="module"> 或 .mjs 文件

20. ES6+ 中有哪些实用的新语法特性?

javascript 复制代码
// ✅ 1. 逻辑赋值运算符(ES2021)
let a = null
a ??= '默认值'     // a = a ?? '默认值'  → '默认值'
a ||= '备选'       // a = a || '备选'
a &&= '新值'       // a = a && '新值'

// ✅ 2. 数字分隔符(ES2021)
const billion = 1_000_000_000     // 10亿,等于 1000000000
const hex = 0xFF_FF_FF            // 更易读
const binary = 0b1010_0001_1000   // 二进制

// ✅ 3. Promise.withResolvers()(ES2024)
const { promise, resolve, reject } = Promise.withResolvers()
// 等价于手动创建:
// let resolve, reject
// const promise = new Promise((res, rej) => { resolve = res; reject = rej })
setTimeout(() => resolve('done'), 1000)
const result = await promise  // 'done'

// ✅ 4. Object.groupBy()(ES2024)
const people = [
  { name: '张三', age: 25 },
  { name: '李四', age: 30 },
  { name: '王五', age: 25 },
]
const grouped = Object.groupBy(people, person => person.age)
// { 25: [{ name: '张三', age: 25 }, { name: '王五', age: 25 }], 30: [...] }

// ✅ 5. Array.fromAsync()(ES2024)
const asyncArr = await Array.fromAsync(async function* () {
  yield 1; yield 2; yield 3
}())  // [1, 2, 3]

// ✅ 6. 管道运算符(Stage 2 提案)
// const result = value |> double |> addOne |> square
// 目前可用 pipe 函数模拟

// ✅ 7. 装饰器(ES2023,Stage 3)
// @logged
// class MyClass {
//   @validate
//   method() {}
// }

// ✅ 8. using 声明(ES2024,显式资源管理)
// {
//   using file = openFile('data.txt')
//   // 自动在块结束时调用 file[Symbol.dispose]()
// }

💡 面试加分点: 了解 TC39 提案流程(Stage 0-4)是加分项。Promise.withResolvers() 简化了很多需要在外部控制 resolve/reject 的场景。Object.groupBy() 终于让 JS 有了原生的分组功能(以前需要 Lodash 的 _.groupBy)。


21. 什么是 ES Module 和 CommonJS 的区别?

特性 ES Module (import/export) CommonJS (require/module.exports)
加载方式 编译时静态加载 运行时动态加载
输出 值的引用(动态绑定) 值的拷贝
顶层 this undefined module 对象
异步/同步 异步 同步
循环引用 支持(通过引用绑定) 支持(返回部分导出)
环境 浏览器 + Node.js Node.js
Tree Shaking ✅ 支持 ❌ 不支持
javascript 复制代码
// ========== ES Module ==========
// math.js
export let count = 0
export function increment() { count++ }

// app.js
import { count, increment } from './math.js'
console.log(count)  // 0
increment()
console.log(count)  // 1(值的引用 → 实时反映变化)

// ========== CommonJS ==========
// math.js
let count = 0
module.exports = { count, increment: () => { count++ } }

// app.js
const { count, increment } = require('./math.js')
console.log(count)  // 0
increment()
console.log(count)  // 0!(值的拷贝 → 不会反映变化)

// ========== 互操作 ==========
// Node.js 中使用 ES Module
// 方式1:文件后缀 .mjs
// 方式2:package.json 中设置 "type": "module"

// ES Module 中导入 CommonJS
import pkg from './cjs-module.cjs'  // 整体导入
// CommonJS 中导入 ES Module(Node.js 需要 import())
const esm = await import('./es-module.mjs')

💡 面试加分点: ES Module 的"编译时静态分析"是 Tree Shaking 的基础------打包工具(Webpack/Rollup/Vite)可以在编译时分析哪些 export 没有被 import,从而删除未使用的代码。CommonJS 的动态 require() 无法做到这一点。


22. 什么是装饰器(Decorator)?

装饰器 是一种特殊语法,用于修改类和类成员的行为,是一种元编程技术。ES2023 已经进入 Stage 3(接近标准化)。

javascript 复制代码
// ✅ 类装饰器(修改类的行为)
function logged(target, context) {
  // target: 被装饰的类
  // context: { kind: 'class', name: 'MyClass' }
  return class extends target {
    constructor(...args) {
      console.log(`创建 ${context.name} 实例`)
      super(...args)
    }
  }
}

@logged
class MyService {
  constructor(name) {
    this.name = name
  }
}
new MyService('test')  // '创建 MyService 实例'

// ✅ 方法装饰器(修改方法行为)
function measure(target, context) {
  return function(...args) {
    const start = performance.now()
    const result = target.call(this, ...args)
    const end = performance.now()
    console.log(`${context.name} 执行耗时: ${end - start}ms`)
    return result
  }
}

class Calculator {
  @measure
  heavyCalculation(n) {
    let sum = 0
    for (let i = 0; i < n; i++) sum += i
    return sum
  }
}

// ✅ 实际应用:常见装饰器模式
// @readonly    → 使属性不可修改
// @deprecated  → 标记废弃方法,调用时打印警告
// @debounce(300)  → 方法防抖
// @validate   → 参数校验
// @cache      → 方法缓存
// @inject     → 依赖注入(Angular/NestJS)

💡 面试加分点: TypeScript 中的装饰器(experimentalDecorators)与 TC39 标准装饰器语法略有不同。Angular、NestJS 等框架大量使用装饰器实现依赖注入和元数据声明。


23. 什么是 Private Class Fields(私有类字段)?

ES2022 引入了使用 # 前缀声明的真正私有字段,替代了之前约定俗成的 _ 前缀或 WeakMap 方案。

javascript 复制代码
class BankAccount {
  // 私有字段(类外部完全无法访问)
  #balance = 0
  #owner

  // 私有静态字段
  static #totalAccounts = 0

  constructor(owner, initialBalance) {
    this.#owner = owner
    this.#balance = initialBalance
    BankAccount.#totalAccounts++
  }

  // 公有方法可以访问私有字段
  deposit(amount) {
    if (amount <= 0) throw new Error('金额必须大于 0')
    this.#balance += amount
    this.#log(`存入 ${amount}`)
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error('余额不足')
    this.#balance -= amount
    this.#log(`取出 ${amount}`)
  }

  get balance() { return this.#balance }

  // 私有方法
  #log(action) {
    console.log(`[${this.#owner}] ${action},余额: ${this.#balance}`)
  }

  // 私有静态方法
  static #validateOwner(name) {
    return typeof name === 'string' && name.length > 0
  }

  static getTotal() { return BankAccount.#totalAccounts }
}

const account = new BankAccount('张三', 1000)
account.deposit(500)   // [张三] 存入 500,余额: 1500
account.balance        // 1500(通过 getter 访问)
// account.#balance    // SyntaxError!(外部无法访问私有字段)
// account.#log()      // SyntaxError!(外部无法调用私有方法)

// ✅ 检查对象是否有某个私有字段(in 操作符)
console.log(#balance in account)  // true

💡 面试加分点: # 私有字段是真正的硬私有(hard private),与 TypeScript 的 private 关键字不同------TS 的 private 只是编译时检查,运行时仍然可以访问。# 私有字段在运行时也无法访问,即使通过 Object.keys()JSON.stringify()Reflect.ownKeys() 也看不到。


24. 什么是 String 和 RegExp 的新方法?

javascript 复制代码
// ========== String 新方法 ==========

// ES6
'hello'.startsWith('hel')     // true
'hello'.endsWith('llo')       // true
'hello'.includes('ell')       // true
'ha'.repeat(3)                // 'hahaha'

// ES2017
'hello'.padStart(10, '*')     // '*****hello'
'42'.padStart(4, '0')         // '0042'(补零)
'hello'.padEnd(10, '-')       // 'hello-----'

// ES2019
'  hello  '.trimStart()       // 'hello  '
'  hello  '.trimEnd()         // '  hello'
'  hello  '.trim()            // 'hello'

// ES2020
'hello world'.matchAll(/\w+/g)  // 返回迭代器,包含所有匹配结果和捕获组

// ES2021
'hello world'.replaceAll('l', 'L')  // 'heLLo worLd'
// 之前需要用正则:'hello world'.replace(/l/g, 'L')

// ES2022
'hello world'.at(-1)          // 'd'(支持负索引)

// ========== RegExp 新特性 ==========

// ES2018: 命名捕获组
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const match = '2024-03-15'.match(dateRegex)
match.groups.year   // '2024'
match.groups.month  // '03'
match.groups.day    // '15'

// ES2018: 后行断言
/(?<=\$)\d+/.exec('$100')  // ['100'](匹配 $ 后面的数字)
/(?<!\$)\d+/.exec('€100')  // ['100'](匹配非 $ 后面的数字)

// ES2018: dotAll 模式(s 标志)
/hello.world/s.test('hello\nworld')  // true(. 也匹配换行符)

// ES2024: v 标志(Unicode 集合操作)
/[\p{Script=Greek}&&\p{Letter}]/v  // 希腊字母的交集

25. ES6+ 中的异步编程演进?

scss 复制代码
回调函数 → Promise → Generator + co → async/await → Top-level await
(ES5)     (ES6)     (ES6)            (ES2017)      (ES2022)
javascript 复制代码
// ❌ 回调地狱(Callback Hell)
fetchUser(1, (user) => {
  fetchPosts(user.id, (posts) => {
    fetchComments(posts[0].id, (comments) => {
      console.log(comments)  // 层层嵌套,难以维护
    })
  })
})

// ✅ Promise 链式调用
fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(err => console.error(err))

// ✅ Generator + co(过渡方案,已很少使用)
function* getData() {
  const user = yield fetchUser(1)
  const posts = yield fetchPosts(user.id)
  const comments = yield fetchComments(posts[0].id)
  return comments
}

// ✅ async/await(当前主流 ✅)
async function getData() {
  try {
    const user = await fetchUser(1)
    const posts = await fetchPosts(user.id)
    const comments = await fetchComments(posts[0].id)
    return comments
  } catch (err) {
    console.error(err)
  }
}

// ✅ 并行 + 串行结合
async function loadDashboard() {
  // 并行请求(互不依赖的请求同时发出)
  const [user, notifications] = await Promise.all([
    fetchUser(1),
    fetchNotifications()
  ])
  // 串行请求(依赖前面的结果)
  const posts = await fetchPosts(user.id)
  return { user, notifications, posts }
}

// ✅ for await...of(异步迭代)
async function readStream(stream) {
  for await (const chunk of stream) {
    console.log(chunk)
  }
}

💡 面试加分点: async/await 本质是 Generator + Promise 的语法糖。await 只暂停当前 async 函数内部的执行,不阻塞主线程。面试中常考"串行 vs 并行"的区别------多个独立的 await 是串行的,应该用 Promise.all 来并行化。


26. ES5 和 ES6 的继承有什么区别?

这是面试中对比原型链继承与 Class 继承的经典题目。

对比项 ES5(构造函数 + 原型链) ES6(class + extends)
语法 function + prototype class + extends + super
继承机制 先创建子类实例 this,再将父类属性添加到 this 先创建父类实例 thissuper()),再用子类构造函数修饰
原型链 手动设置 Child.prototype = Object.create(Parent.prototype) 自动建立原型链
静态方法继承 ❌ 需要手动拷贝 ✅ 自动继承(Child.__proto__ === Parent
super 关键字 ❌ 没有 ✅ 可调用父类构造函数和方法
可读性 较差,模板代码多 清晰简洁,接近传统 OOP
new 调用 可以不用 new(但可能出 bug) 必须用 new,否则报错
javascript 复制代码
// ========== ES5 继承(组合继承) ==========
function Animal(name) {
  this.name = name
  this.colors = ['black', 'white']
}
Animal.prototype.speak = function() {
  return this.name + ' makes a sound.'
}
// 静态方法需要手动添加
Animal.create = function(name) {
  return new Animal(name)
}

function Dog(name, breed) {
  Animal.call(this, name)  // ① 借用构造函数(继承实例属性)
  this.breed = breed
}
Dog.prototype = Object.create(Animal.prototype)  // ② 设置原型链(继承原型方法)
Dog.prototype.constructor = Dog                   // ③ 修复 constructor 指向
// ❌ 静态方法不会自动继承
// Dog.create  → undefined

Dog.prototype.bark = function() {
  return this.name + ' barks!'
}

var dog = new Dog('Rex', 'Labrador')
dog.speak()  // 'Rex makes a sound.'
dog.bark()   // 'Rex barks!'


// ========== ES6 继承(class + extends) ==========
class AnimalES6 {
  constructor(name) {
    this.name = name
    this.colors = ['black', 'white']
  }

  speak() {
    return `${this.name} makes a sound.`
  }

  static create(name) {
    return new AnimalES6(name)
  }
}

class DogES6 extends AnimalES6 {
  #breed  // 私有字段

  constructor(name, breed) {
    super(name)     // ✅ 必须先调用 super(),否则报错
    this.#breed = breed
  }

  bark() {
    return `${this.name} barks!`
  }

  // 调用父类方法
  introduce() {
    return `${super.speak()} I'm a ${this.#breed}.`
  }
}

const dog2 = new DogES6('Rex', 'Labrador')
dog2.speak()      // 'Rex makes a sound.'
dog2.bark()       // 'Rex barks!'
dog2.introduce()  // 'Rex makes a sound. I\'m a Labrador.'

// ✅ 静态方法自动继承
DogES6.create('Buddy')  // AnimalES6 { name: 'Buddy', colors: ['black', 'white'] }

核心区别图解:

kotlin 复制代码
ES5 继承本质:
  子类实例 this  →  复制父类属性  →  手动设置原型链
  (先有子类 this,再添加父类的东西)

ES6 继承本质:
  super() 创建父类实例  →  子类 constructor 修饰 this  →  自动原型链
  (先有父类 this,子类再加工)

这就是为什么 ES6 中 constructor 里必须先调用 super() 才能使用 this!

💡 面试加分点: ES6 的 class 本质上仍然是基于原型链的语法糖,但它做了很多自动化工作(原型链、constructor 修复、静态方法继承)。ES5 的"寄生组合继承"是最优方案,ES6 的 extends 内部实现机制与之类似。另外,ES6 的类不存在变量提升(let 语义),而 ES5 的函数声明会提升。


27. 扩展运算符(...)能展开基本数据类型吗?为什么?

这道题考察对**可迭代协议(Iterable Protocol)**的理解。

核心结论: 扩展运算符 ... 只能用于实现了 [Symbol.iterator] 接口的可迭代对象

数据类型 数组展开 [...x] 对象展开 {...x} 原因
Array [...[1,2,3]][1,2,3] {...[1,2]}{0:1, 1:2} Symbol.iterator
String [...'hello']['h','e','l','l','o'] {...'hi'}{0:'h', 1:'i'} Symbol.iterator
Set [...new Set([1,2])][1,2] ❌ 报错 Symbol.iterator,但 {...} 需要可枚举属性
Map [...new Map()] ❌ 报错 同上
Number [...123] 报错 {...123}{}(空对象) 没有 Symbol.iterator
Boolean [...true] 报错 {...true}{} 没有 Symbol.iterator
null/undefined ❌ 报错 {...null}{} 无法被迭代
Object [...{}] 报错 {...obj} 浅拷贝 普通对象没有 Symbol.iterator
javascript 复制代码
// ========== 数组上下文中的展开(需要可迭代协议)==========

// ✅ 可迭代对象才能用 [...x]
console.log([...[1, 2, 3]])          // [1, 2, 3]
console.log([...'hello'])            // ['h', 'e', 'l', 'l', 'o']
console.log([...new Set([1, 2, 3])]) // [1, 2, 3]

// ❌ 基本数据类型(数字、布尔)没有 Symbol.iterator
// [...123]    // TypeError: 123 is not iterable
// [...true]   // TypeError: true is not iterable
// [...null]   // TypeError: null is not iterable

// ⚠️ 为什么字符串可以?因为字符串有内置的迭代器!
console.log(typeof ''[Symbol.iterator])  // 'function' ✅
console.log(typeof (123)[Symbol.iterator])  // 'undefined' ❌


// ========== 对象上下文中的展开(不需要可迭代协议)==========
// {... } 语法使用的是 Object.assign 的语义,不需要 Symbol.iterator
console.log({ ...123 })       // {}(Number 包装对象没有可枚举属性)
console.log({ ...true })      // {}
console.log({ ...'hi' })      // { '0': 'h', '1': 'i' }(String 有索引属性)
console.log({ ...null })      // {}(安全处理)
console.log({ ...undefined }) // {}


// ========== 手动让基本类型可迭代 ==========
// 给 Number 原型添加迭代器(仅作演示,实际不推荐)
Number.prototype[Symbol.iterator] = function*() {
  for (let i = 0; i < this; i++) {
    yield i
  }
}
console.log([...5])  // [0, 1, 2, 3, 4] ✅ 现在可以了!

// ✅ 实际开发中:让自定义对象可迭代
class Range {
  constructor(start, end) {
    this.start = start
    this.end = end
  }
  [Symbol.iterator]() {
    let current = this.start
    const end = this.end
    return {
      next() {
        return current <= end
          ? { value: current++, done: false }
          : { done: true }
      }
    }
  }
}
console.log([...new Range(1, 5)])  // [1, 2, 3, 4, 5]

💡 面试加分点: 数组展开 [...x] 和对象展开 {...x} 的机制完全不同!数组展开依赖迭代协议Symbol.iterator),对象展开依赖可枚举属性Object.assign 语义)。这就是为什么 {...123} 不报错(返回空对象),而 [...123] 会报错的原因。


28. setTimeout、Promise、async/await 的区别?

这道题考察**宏任务(Macro Task)vs 微任务(Micro Task)**以及事件循环的理解。

对比项 setTimeout Promise.then async/await
任务类型 宏任务 微任务 微任务(基于 Promise)
执行时机 下一轮事件循环(至少延迟 4ms) 当前宏任务结束后,下一个宏任务之前 await 后面的代码等同于 .then()
回调注册 放入宏任务队列 放入微任务队列 await 之后的代码放入微任务队列
错误处理 无法被外层 try/catch 捕获 .catch() 链式捕获 try/catch 直接捕获
嵌套可读性 容易回调地狱 链式调用,较好 同步写法,最好 ✅
javascript 复制代码
// ========== 经典面试题:输出顺序 ==========
console.log('1 - 同步代码')              // ① 同步

setTimeout(() => {
  console.log('2 - setTimeout')          // ⑤ 宏任务
}, 0)

Promise.resolve()
  .then(() => {
    console.log('3 - Promise.then')      // ③ 微任务
  })

async function asyncFn() {
  console.log('4 - async 函数体(同步)')  // ② 同步(async 函数体在 await 之前是同步的)
  await Promise.resolve()
  console.log('5 - await 之后(微任务)')  // ④ 微任务
}
asyncFn()

console.log('6 - 同步代码结束')           // ② 同步

// 输出顺序:1 → 4 → 6 → 3 → 5 → 2

// 解析:
// 第一步:执行所有同步代码 → 1, 4, 6
// 第二步:清空微任务队列 → 3, 5
// 第三步:执行宏任务 → 2


// ========== 更复杂的嵌套场景 ==========
setTimeout(() => console.log('timeout1'), 0)

Promise.resolve().then(() => {
  console.log('promise1')
  setTimeout(() => console.log('timeout2'), 0)
  return Promise.resolve()
}).then(() => {
  console.log('promise2')
})

setTimeout(() => console.log('timeout3'), 0)

// 输出:promise1 → promise2 → timeout1 → timeout3 → timeout2
// 解析:
// 同步阶段:注册 timeout1、promise1 回调、timeout3
// 微任务:promise1 执行(注册 timeout2)→ promise2 执行
// 宏任务:timeout1 → timeout3 → timeout2(timeout2 后注册的)


// ========== 三者的错误处理对比 ==========

// ❌ setTimeout:外层 try/catch 无法捕获
try {
  setTimeout(() => { throw new Error('boom') }, 0)
} catch (e) {
  // 永远不会执行!错误发生在另一个宏任务中
}

// ✅ Promise:用 .catch() 捕获
Promise.reject(new Error('boom'))
  .catch(err => console.error('Promise caught:', err.message))

// ✅ async/await:用 try/catch 捕获(最直观)
async function safeFetch() {
  try {
    const data = await fetch('/api/data')
    return await data.json()
  } catch (err) {
    console.error('Fetch failed:', err.message)
    return null  // 降级处理
  }
}

执行顺序口诀:

javascript 复制代码
同步代码 → 微任务(Promise.then / await 之后 / MutationObserver)→ 宏任务(setTimeout / setInterval / I/O)

💡 面试加分点: await 并不是简单地等待 Promise 完成------它会暂停当前 async 函数的执行,将 await 之后的代码包装成 .then() 回调放入微任务队列,然后让出主线程继续执行外部的同步代码。这就是为什么 "4" 在 "6" 之前输出(async 函数体同步部分先执行),而 "5" 在 "6" 之后输出(await 之后是异步的)。


29. 详细介绍一下 Promise?

Promise 是 ES6 引入的异步编程解决方案,用于解决回调地狱(Callback Hell)问题。

三种状态

scss 复制代码
                   resolve(value)
  ┌──────────┐  ──────────────────→  ┌──────────────┐
  │  pending  │                      │  fulfilled   │
  │ (进行中)  │                      │  (已成功)     │
  └──────────┘  ──────────────────→  └──────────────┘
       │           reject(reason)         │
       │        ──────────────────→  ┌──────────────┐
       │                             │  rejected    │
       └─────────────────────────→   │  (已失败)     │
                                     └──────────────┘

状态一旦改变就不可逆!pending → fulfilled 或 pending → rejected

特点

特性 说明
状态不可逆 一旦从 pending 变为 fulfilledrejected,就永远不会再改变
不受外界影响 只有异步操作的结果可以决定当前状态
立即执行 new Promise(executor) 中的 executor 同步立即执行
链式调用 .then() 返回新的 Promise,支持链式调用
错误冒泡 错误会沿 .then() 链向下传播,直到被 .catch() 捕获

缺点

缺点 说明
无法取消 Promise 一旦创建就会立即执行,无法中途取消
错误必须捕获 如果不设置回调函数,Promise 内部抛出的错误不会反应到外部
状态不透明 处于 pending 状态时,无法得知进展到哪一个阶段
javascript 复制代码
// ========== 基本使用 ==========
const promise = new Promise((resolve, reject) => {
  // executor 同步执行
  console.log('1. executor 同步执行')

  // 模拟异步操作
  setTimeout(() => {
    const success = Math.random() > 0.5
    if (success) {
      resolve('操作成功')   // 状态变为 fulfilled
    } else {
      reject(new Error('操作失败'))  // 状态变为 rejected
    }
  }, 1000)
})

console.log('2. Promise 创建之后的同步代码')
// 输出顺序:1 → 2(executor 是同步的!)

promise
  .then(value => {
    console.log('成功:', value)
    return '处理后的数据'  // 返回值会被包装为新的 resolved Promise
  })
  .then(data => {
    console.log('链式调用:', data)
  })
  .catch(error => {
    console.error('失败:', error.message)
  })
  .finally(() => {
    console.log('无论成功失败都会执行')
  })


// ========== 状态不可逆示例 ==========
const p = new Promise((resolve, reject) => {
  resolve('first')   // ✅ 状态变为 fulfilled
  reject('second')   // ❌ 无效!状态已经改变,不可逆
  resolve('third')   // ❌ 无效!
})
p.then(val => console.log(val))  // 'first'


// ========== 错误冒泡机制 ==========
Promise.resolve(1)
  .then(val => {
    console.log(val)      // 1
    throw new Error('出错了')
  })
  .then(val => {
    console.log('不会执行') // 跳过!因为上面抛出了错误
  })
  .catch(err => {
    console.log('捕获:', err.message)  // '捕获: 出错了'
    return '恢复正常'
  })
  .then(val => {
    console.log(val)      // '恢复正常'(catch 返回值继续链式)
  })

💡 面试加分点: new Promise 的 executor 函数是同步执行 的(这是一个常考坑点),只有 .then() / .catch() 的回调才是异步的微任务。另外,resolve() 传入一个 Promise 时,外层 Promise 会"跟随"内层 Promise 的状态(称为 "Promise Resolution Procedure"),这是链式调用能工作的关键。


30. Promise.all()、Promise.race()、Promise.allSettled()、Promise.any() 的区别?

ES6+ 提供了 4 个 Promise 并发静态方法,适用于不同场景。

方法 成功条件 失败条件 返回值 引入版本
Promise.all() 全部成功 任一失败 所有结果数组(按顺序) ES6
Promise.race() 第一个完成(无论成功/失败) 同左 第一个完成的结果 ES6
Promise.allSettled() 永远成功 永远不会 reject 所有结果(含状态) ES2020
Promise.any() 任一成功 全部失败 第一个成功的结果 ES2021
javascript 复制代码
Promise.all():        全部 ✅ → ✅    任一 ❌ → ❌(快速失败)
Promise.race():       谁先完成就用谁(✅ 或 ❌ 都算)
Promise.allSettled(): 全部完成才返回(✅ 和 ❌ 都收集)
Promise.any():        任一 ✅ → ✅    全部 ❌ → ❌(AggregateError)
javascript 复制代码
const p1 = Promise.resolve('成功1')
const p2 = new Promise(r => setTimeout(() => r('成功2'), 1000))
const p3 = Promise.reject('失败3')
const p4 = new Promise(r => setTimeout(() => r('成功4'), 500))


// ========== 1. Promise.all() --- 全部成功才成功 ==========
// 适用场景:多个请求全部完成后再渲染页面
Promise.all([p1, p2, p4])
  .then(results => {
    console.log(results)  // ['成功1', '成功2', '成功4'](按入参顺序,非完成顺序)
  })

// 任一失败立即 reject(快速失败)
Promise.all([p1, p2, p3])
  .catch(err => {
    console.log(err)  // '失败3'(只拿到第一个失败的原因)
  })

// ✅ 实际场景:并行加载页面数据
async function loadPageData() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ])
  return { user, posts, comments }
}


// ========== 2. Promise.race() --- 赛跑机制 ==========
// 适用场景:超时控制、取最快响应
Promise.race([p2, p4])
  .then(result => {
    console.log(result)  // '成功4'(500ms 比 1000ms 快)
  })

// ✅ 实际场景:请求超时控制
function fetchWithTimeout(url, ms) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`请求超时 ${ms}ms`)), ms)
    )
  ])
}
// 5 秒内没返回就超时
const data = await fetchWithTimeout('/api/data', 5000)


// ========== 3. Promise.allSettled() --- 全部结果(不论成功失败) ==========
// 适用场景:批量操作,需要知道每一个的结果
Promise.allSettled([p1, p2, p3])
  .then(results => {
    console.log(results)
    // [
    //   { status: 'fulfilled', value: '成功1' },
    //   { status: 'fulfilled', value: '成功2' },
    //   { status: 'rejected',  reason: '失败3' }
    // ]
  })

// ✅ 实际场景:批量删除,统计成功/失败
async function batchDelete(ids) {
  const results = await Promise.allSettled(
    ids.map(id => deleteItem(id))
  )
  const succeeded = results.filter(r => r.status === 'fulfilled')
  const failed = results.filter(r => r.status === 'rejected')
  console.log(`成功 ${succeeded.length} 个,失败 ${failed.length} 个`)
}


// ========== 4. Promise.any() --- 任一成功即可 ==========
// 适用场景:多源请求,取最快的成功响应
Promise.any([p3, p2, p4])
  .then(result => {
    console.log(result)  // '成功4'(跳过失败的 p3,取最快成功的 p4)
  })

// 全部失败时抛出 AggregateError
Promise.any([
  Promise.reject('err1'),
  Promise.reject('err2'),
]).catch(err => {
  console.log(err instanceof AggregateError)  // true
  console.log(err.errors)  // ['err1', 'err2']
})

// ✅ 实际场景:多 CDN 取最快的
async function fetchFromFastestCDN(url) {
  return Promise.any([
    fetch(`https://cdn1.example.com${url}`),
    fetch(`https://cdn2.example.com${url}`),
    fetch(`https://cdn3.example.com${url}`),
  ])
}

💡 面试加分点: Promise.all 的参数不一定是数组,可以是任何具有 Iterator 接口的数据结构。入参中不是 Promise 的值会被自动 Promise.resolve() 包装。Promise.allSettled 在实际开发中非常实用------当你需要"批量操作 + 错误容忍"时,它比 Promise.all 更合适。


31. Promise 中 reject 和 catch 处理上有什么区别?

对比项 reject .catch()
本质 Promise 构造器的参数函数 Promise 实例方法
作用 将 Promise 状态改为 rejected抛出异常 捕获并处理异常
作用范围 只在 executor 或 .then 回调中 能捕获前面所有链上的错误
网络异常 不经过 reject,直接触发 catch ✅ 能捕获
javascript 复制代码
// ========== reject 和 catch 的关系 ==========

// 1. reject 后的值 → 进入 .then 的第二个回调或 .catch
new Promise((resolve, reject) => {
  reject('出错了')
}).then(
  val => console.log('成功:', val),
  err => console.log('then 第二个回调:', err)  // 'then 第二个回调: 出错了'
)

// 2. 如果 .then 没有第二个回调 → 进入 .catch
new Promise((resolve, reject) => {
  reject('出错了')
})
  .then(val => console.log('成功:', val))
  .catch(err => console.log('catch 捕获:', err))  // 'catch 捕获: 出错了'


// ========== .then 第二参数 vs .catch 的关键区别 ==========

// ❌ .then 的第二个回调:无法捕获第一个回调里的错误!
Promise.resolve('ok')
  .then(
    val => { throw new Error('then 里出错了') },  // ① 这里抛出错误
    err => console.log('不会执行')                 // ② 无法捕获 ① 的错误!
  )
  // 错误会继续向下传播...

// ✅ .catch:可以捕获前面所有链上的错误(包括 .then 回调中的错误)
Promise.resolve('ok')
  .then(val => { throw new Error('then 里出错了') })
  .catch(err => console.log('catch 捕获:', err.message))  // 'catch 捕获: then 里出错了'


// ========== 网络异常直接进入 catch ==========
new Promise((resolve, reject) => {
  // 网络异常不经过 reject,直接抛出错误
  fetch('https://invalid-url.example.com')
    .then(res => resolve(res))
    .catch(err => reject(err))  // 网络错误在这里被 reject
})
  .then(val => console.log('成功'))
  .catch(err => console.log('网络异常:', err.message))


// ========== 最佳实践:始终使用 .catch() ==========

// ❌ 不推荐:只用 .then 的第二参数处理错误
somePromise.then(
  data => processData(data),
  err => handleError(err)   // 无法捕获 processData 中的错误
)

// ✅ 推荐:在链尾使用 .catch()
somePromise
  .then(data => processData(data))
  .catch(err => handleError(err))  // 能捕获前面所有环节的错误

// ✅ 最佳:async/await + try/catch
async function fetchData() {
  try {
    const res = await fetch('/api/data')
    const data = await res.json()
    return processData(data)
  } catch (err) {
    // 统一捕获:网络错误、JSON 解析错误、processData 错误
    console.error('任何环节出错都在这里:', err.message)
  }
}

错误捕获流程图:

javascript 复制代码
Promise.reject('err')
  │
  ├─→ .then(onFulfilled, onRejected)
  │       │                   │
  │       │                   └─→ onRejected 处理(但无法捕获 onFulfilled 的错误)
  │       │
  │       └─→ onFulfilled 中 throw → 向下传播
  │
  └─→ .catch(onRejected)
          └─→ 捕获前面所有链上的错误(包括 .then 回调中的错误)✅

💡 面试加分点: 始终建议在 Promise 链的末尾 使用 .catch(),而不是在 .then() 中传第二个参数。原因:.catch() 能捕获前面所有 环节(包括 .then 成功回调中)抛出的错误,而 .then(null, onRejected) 只能捕获前面 Promise 的 reject,无法捕获同级 onFulfilled 中的错误。


32. new Promise 是同步还是异步?then 链呢?

这道题是高频坑点题,很多候选人会答错。

结论:

部分 执行方式 原因
new Promise(executor) 的 executor 函数 同步 立即执行 构造函数内部直接调用 executor
.then() / .catch() / .finally() 的回调 异步 微任务 回调被添加到微任务队列
resolve() / reject() 调用本身 同步(但不阻塞后续代码) 只改变状态,不立即执行回调
javascript 复制代码
// ========== 经典面试题:输出顺序 ==========
console.log('1')

const p = new Promise((resolve, reject) => {
  console.log('2')       // ✅ 同步执行!
  resolve('成功')
  console.log('3')       // ✅ resolve 后的代码仍然会执行(resolve 不是 return)
})

p.then(val => {
  console.log('4', val)  // 异步微任务
})

console.log('5')

// 输出顺序:1 → 2 → 3 → 5 → 4 成功
// 解析:
// 同步阶段:1 → 2(executor 同步)→ 3(resolve 不中断执行)→ 5
// 微任务阶段:4(.then 回调)


// ========== 更复杂的场景 ==========
console.log('start')

new Promise((resolve) => {
  console.log('executor 1')
  resolve()
}).then(() => {
  console.log('then 1')
  new Promise((resolve) => {
    console.log('executor 2')  // .then 回调中 new Promise 的 executor 也是同步的!
    resolve()
  }).then(() => {
    console.log('then 2')
  })
}).then(() => {
  console.log('then 3')
})

console.log('end')

// 输出:start → executor 1 → end → then 1 → executor 2 → then 2 → then 3
// 注意:then 3 在 then 2 之后,因为 then 3 需要等 then 1 返回的 Promise resolve


// ========== resolve 不等于 return ==========
const p2 = new Promise((resolve, reject) => {
  resolve('A')
  console.log('B')  // ✅ 仍然执行!resolve 不会中断函数
  // 如果要在 resolve 后停止,应该用 return resolve('A')
})
p2.then(console.log)

// 输出:B → A
// 'B' 是同步的,'A' 的 .then 回调是异步微任务


// ========== 最佳实践:resolve 后立即 return ==========
const p3 = new Promise((resolve, reject) => {
  if (someCondition) {
    return resolve('success')  // ✅ 用 return 阻止后续代码执行
  }
  // 只有条件不满足时才执行这里
  reject('failed')
})

💡 面试加分点: resolve() 后面的代码依然会执行(因为 resolve 只是一个普通函数调用,不是 return)。最佳实践是 return resolve(value),避免意外执行后续逻辑。另外,如果 resolve() 传入的是一个 Promise,外层 Promise 会异步地跟随这个内层 Promise 的状态------这会多产生一个微任务。


33. Promise 的 then 链为什么是异步的?

面试中可能追问"为什么 .then() 要设计成异步的",这考察你对事件循环底层设计的理解。

核心原因

原因 说明
一致性保证 无论 Promise 是同步 resolve 还是异步 resolve,.then() 回调的执行时机都是一致的(都在微任务阶段)
避免阻塞主线程 如果同步执行回调,大量 Promise 链会阻塞后续代码和 UI 渲染
符合异步编程模型 Promise 旨在管理异步操作,如果 .then() 同步执行就失去了意义
Zalgo 问题 一个 API 有时同步、有时异步地调用回调 → 极难调试的 bug
javascript 复制代码
// ========== 如果 .then 是同步的会怎样?(假设场景) ==========

// 假设 .then 是同步执行的:
let value = 0

// 已经 resolved 的 Promise
Promise.resolve().then(() => {
  value = 1
})

console.log(value)
// 现实(异步 .then):输出 0 ✅(可预测)
// 假设(同步 .then):输出 1 ❌(行为不一致,取决于 Promise 是否已 resolved)


// ========== Zalgo 问题演示 ==========
// "Zalgo" 指一个函数有时同步、有时异步地调用回调

// ❌ 坏的设计:有时同步有时异步
function badFetch(url, callback) {
  if (cache[url]) {
    callback(cache[url])  // 同步调用!
  } else {
    fetch(url).then(data => {
      cache[url] = data
      callback(data)      // 异步调用!
    })
  }
}

// 使用者的代码变得不可预测:
let loading = true
badFetch('/api/data', (data) => {
  loading = false
  render(data)
})
showSpinner(loading)
// 如果命中缓存:loading 已经是 false,showSpinner(false) → 不显示 loading
// 如果未命中缓存:loading 还是 true,showSpinner(true) → 显示 loading
// 行为不一致!这就是 Zalgo 问题

// ✅ Promise 的设计解决了 Zalgo:.then 永远是异步的
function goodFetch(url) {
  if (cache[url]) {
    return Promise.resolve(cache[url])  // 虽然值已经有了,但 .then 仍然异步执行
  }
  return fetch(url).then(data => {
    cache[url] = data
    return data
  })
}

// 使用者的代码行为一致:
let loading2 = true
goodFetch('/api/data').then(data => {
  loading2 = false
  render(data)
})
showSpinner(loading2)  // 永远是 true ✅ 行为一致、可预测


// ========== 微任务 vs 宏任务的选择 ==========
// Promise 使用微任务而非宏任务(setTimeout),原因:

// 微任务:在当前宏任务结束后、下一个宏任务开始前执行
// → 更快的执行时机,更好的性能

Promise.resolve().then(() => console.log('微任务'))  // 先执行
setTimeout(() => console.log('宏任务'), 0)           // 后执行

// 输出:微任务 → 宏任务
// 微任务的优先级高于宏任务,这让 Promise 链的执行更加紧凑


// ========== 微任务执行时机完整示例 ==========
console.log('A')

setTimeout(() => {
  console.log('B')
  Promise.resolve().then(() => console.log('C'))
}, 0)

Promise.resolve().then(() => {
  console.log('D')
  setTimeout(() => console.log('E'), 0)
})

console.log('F')

// 输出:A → F → D → B → C → E
// 解析:
// 同步:A → F
// 微任务:D(执行时注册了 setTimeout E)
// 宏任务1:B(执行后产生微任务 C)
// 微任务:C(宏任务1产生的微任务立即清空)
// 宏任务2:E

💡 面试加分点: Promise 的 .then() 使用微任务 (Microtask)而非宏任务(Macrotask),这是 Promises/A+ 规范的要求。微任务在当前宏任务执行完后立即 执行(不等下一轮事件循环),所以 Promise 链的执行比 setTimeout 更快。这也是为什么"先 promise 后 setTimeout"的原因。在 V8 引擎中,微任务队列的实现是 PromiseReactionJob

相关推荐
禁止摆烂_才浅1 小时前
JavaScript WebAPI(进阶)高频面试题
前端·javascript·面试
J船长1 小时前
Vscode/Cursor开了太多项目怕搞混?上Peacock啊
前端
禁止摆烂_才浅1 小时前
React Hooks 高频面试题
前端·react.js·面试
禁止摆烂_才浅1 小时前
React 高频面试题
前端·react.js·面试
禁止摆烂_才浅1 小时前
JavaScript 高级面试题
前端·javascript·面试
禁止摆烂_才浅1 小时前
JavaScript 基础 高频面试题
前端·javascript·面试
何时梦醒1 小时前
Docker 容器化入门:从「我电脑能跑」到「哪台机器都能跑」
后端·docker·面试
颜进强1 小时前
11 - 从需求拆解到 OpenSpec:为什么不要直接敲 /opsx:explore
前端·后端·ai编程
禁止摆烂_才浅1 小时前
HTML 高频面试题
前端·面试·html