ES6+ 高频面试题

15 阅读25分钟

ES6+ 高频面试题

1. let、const、var 的区别?

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

特性varletconst
作用域函数作用域块级作用域 {}块级作用域 {}
变量提升✅(值为 undefined)❌(暂时性死区 TDZ)❌(暂时性死区 TDZ)
重复声明✅ 允许❌ 报错❌ 报错
重新赋值✅ 允许✅ 允许❌ 报错
全局属性✅(挂到 window)❌❌
必须初始化否否是
// ✅ 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 的提升为 undefined。for 循环中 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')
// ✅ 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. 解构赋值的用法?

// 数组解构
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. 扩展运算符(...)的用法?

// 数组展开
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. 模板字符串的用法?

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 的用途?

// 创建唯一标识符
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 的用法?

// 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 的用法?

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)?

// 生成器函数:用 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. 可选链(?.)和空值合并(??)运算符?

// 可选链 ?.:安全访问深层属性
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 类的用法?

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)的用法?

// 命名导出
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?

// 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 的新方法?

// 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 的新方法?

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 的高级用法有哪些?

// ✅ 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-limit、p-queue 等库。Promise 重试 + 指数退避(exponential backoff)是网络请求的最佳实践。


17. 什么是 WeakRef 和 FinalizationRegistry?

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

// ✅ 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(已被回收)
  }
}

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


18. 什么是 structuredClone?

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

// ✅ 基本使用
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 函数中。

// ✅ 模块中可以直接使用 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+ 中有哪些实用的新语法特性?

// ✅ 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)
加载方式编译时静态加载运行时动态加载
输出值的引用(动态绑定)值的拷贝
顶层 thisundefinedmodule 对象
异步/同步异步同步
循环引用支持(通过引用绑定)支持(返回部分导出)
环境浏览器 + Node.jsNode.js
Tree Shaking✅ 支持❌ 不支持
// ========== 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(接近标准化)。

// ✅ 类装饰器(修改类的行为)
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 方案。

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 的新方法?

// ========== 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+ 中的异步编程演进?

回调函数 → Promise → Generator + co → async/await → Top-level await
(ES5)     (ES6)     (ES6)            (ES2017)      (ES2022)
// ❌ 回调地狱(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 + prototypeclass + extends + super
继承机制先创建子类实例 this,再将父类属性添加到 this先创建父类实例 this(super()),再用子类构造函数修饰
原型链手动设置 Child.prototype = Object.create(Parent.prototype)自动建立原型链
静态方法继承❌ 需要手动拷贝✅ 自动继承(Child.__proto__ === Parent)
super 关键字❌ 没有✅ 可调用父类构造函数和方法
可读性较差,模板代码多清晰简洁,接近传统 OOP
new 调用可以不用 new(但可能出 bug)必须用 new,否则报错
// ========== 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'] }

核心区别图解:

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
// ========== 数组上下文中的展开(需要可迭代协议)==========

// ✅ 可迭代对象才能用 [...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)**以及事件循环的理解。

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

执行顺序口诀:

同步代码 → 微任务(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)问题。

三种状态

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

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

特点

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

缺点

缺点说明
无法取消Promise 一旦创建就会立即执行,无法中途取消
错误必须捕获如果不设置回调函数,Promise 内部抛出的错误不会反应到外部
状态不透明处于 pending 状态时,无法得知进展到哪一个阶段
// ========== 基本使用 ==========
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
Promise.all():        全部 ✅ → ✅    任一 ❌ → ❌(快速失败)
Promise.race():       谁先完成就用谁(✅ 或 ❌ 都算)
Promise.allSettled(): 全部完成才返回(✅ 和 ❌ 都收集)
Promise.any():        任一 ✅ → ✅    全部 ❌ → ❌(AggregateError)
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✅ 能捕获
// ========== 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)
  }
}

错误捕获流程图:

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() 调用本身同步(但不阻塞后续代码)只改变状态,不立即执行回调
// ========== 经典面试题:输出顺序 ==========
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
// ========== 如果 .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。