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的提升为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') |
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-limit、p-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(已被回收)
}
}
💡 面试加分点:
WeakRef和FinalizationRegistry不保证回调何时执行(取决于 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 |
先创建父类实例 this(super()),再用子类构造函数修饰 |
| 原型链 | 手动设置 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 变为 fulfilled 或 rejected,就永远不会再改变 |
| 不受外界影响 | 只有异步操作的结果可以决定当前状态 |
| 立即执行 | 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。