JavaScript 高级面试题

JavaScript 高级面试题

1. 闭包是什么?有什么应用场景?

闭包(Closure) 是指函数可以记住并访问其定义时的词法作用域 ,即使函数在其词法作用域之外执行。简单说:函数 + 它能访问的外部变量 = 闭包

闭包产生的条件:

  1. 存在函数嵌套
  2. 内部函数引用了外部函数的变量
  3. 内部函数被返回或传递到外部
javascript 复制代码
// ✅ 基本闭包:makeCounter 返回的函数"记住"了 count 变量
function makeCounter() {
  let count = 0  // 这个变量被闭包捕获,不会被垃圾回收
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
  }
}
const counter = makeCounter()
counter.increment() // 1
counter.increment() // 2
counter.getCount()  // 2
// count 对外部不可见,只能通过返回的方法访问

// ✅ 应用1:数据私有化(模拟私有变量)
function createPerson(name) {
  let _age = 0  // "私有"变量
  return {
    getName: () => name,
    getAge: () => _age,
    setAge: (age) => {
      if (typeof age !== 'number' || age < 0) throw new Error('无效年龄')
      _age = age
    },
  }
}
const person = createPerson('张三')
person.setAge(25)
person.getAge()  // 25
// person._age   // undefined(无法直接访问)

// ✅ 应用2:函数柯里化
const multiply = (a) => (b) => a * b
const double = multiply(2)
const triple = multiply(3)
double(5)  // 10
triple(5)  // 15

// ✅ 应用3:模块模式(IIFE + 闭包)
const module = (() => {
  let privateData = []
  const add = (item) => privateData.push(item)
  const getAll = () => [...privateData]  // 返回副本,防止外部修改
  const clear = () => { privateData = [] }
  return { add, getAll, clear }
})()

// ✅ 应用4:缓存(记忆化函数)
function memoize(fn) {
  const cache = new Map()
  return function(...args) {
    const key = JSON.stringify(args)
    if (cache.has(key)) return cache.get(key)
    const result = fn.apply(this, args)
    cache.set(key, result)
    return result
  }
}
const expensiveCalc = memoize((n) => {
  console.log('计算中...')
  return n * n
})
expensiveCalc(10) // '计算中...' → 100
expensiveCalc(10) // 100(直接从缓存返回)

⚠️ 闭包的注意事项:

javascript 复制代码
// ❌ 经典坑:循环中的闭包(var 没有块级作用域)
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)  // 输出 3 3 3
}

// ✅ 解决方案1:使用 let(推荐)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)  // 输出 0 1 2
}

// ✅ 解决方案2:使用 IIFE 创建新作用域
for (var i = 0; i < 3; i++) {
  ((j) => setTimeout(() => console.log(j), 0))(i)
}

// ⚠️ 闭包可能导致内存泄漏(闭包引用的变量不会被 GC 回收)
function createLeak() {
  const hugeData = new Array(1000000).fill('data')
  return () => hugeData.length  // hugeData 一直被引用,无法回收
}
// 解决:不再使用时将引用设为 null

💡 面试加分点: 闭包不是 JS 特有的,它是函数式编程中的基本概念。React Hooks(如 useState、useEffect)底层大量使用闭包。闭包的"陷阱"在于它会延长变量的生命周期------被闭包引用的变量不会被垃圾回收,因此需要注意内存管理。


2. 变量提升和函数提升是什么?

变量提升(Hoisting) 是指在代码执行前,JS 引擎会将变量声明和函数声明"提升"到当前作用域的顶部。这是执行上下文创建阶段的行为。

javascript 复制代码
// ========== var 变量提升 ==========
console.log(a)  // undefined(声明提升,赋值未提升)
var a = 10
console.log(a)  // 10

// 等价于引擎处理后:
// var a = undefined  ← 提升
// console.log(a)     // undefined
// a = 10             ← 赋值在原位置
// console.log(a)     // 10

// ========== 函数提升(整体提升,优先级高于 var)==========
foo()  // ✅ 'foo'(函数声明整体提升,可以在声明前调用)
function foo() { console.log('foo') }

bar()  // ❌ TypeError: bar is not a function
var bar = function() { console.log('bar') }
// bar 被当做变量提升为 undefined,所以调用会报错

// ========== let/const 暂时性死区(TDZ)==========
console.log(b)  // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 20
// let/const 也会"提升"(分配内存),但在声明前处于暂时性死区,不可访问

// ========== 面试经典题 ==========
var x = 1
function test() {
  console.log(x)  // undefined(不是 1!函数内 var x 被提升了)
  var x = 2
  console.log(x)  // 2
}
test()

// ========== 函数声明 vs var 同名时 ==========
console.log(typeof fn)  // 'function'(函数声明优先级高于 var)
var fn = 10
function fn() {}
console.log(typeof fn)  // 'number'(赋值覆盖了函数)

💡 面试加分点: let/const 实际上也会被"提升",但处于暂时性死区(Temporal Dead Zone),这个设计是为了让开发者养成"先声明后使用"的好习惯。函数声明的提升优先级高于 var,如果同名,函数声明会在变量赋值之前覆盖 var


3. 事件循环(Event Loop)是什么?

事件循环 是 JavaScript 实现异步非阻塞的核心机制。JS 是单线程语言,通过事件循环在一个线程中处理异步操作。

执行顺序:同步代码 → 微任务 → 宏任务(每轮循环:执行一个宏任务 → 清空所有微任务 → 渲染 → 下一个宏任务)

类型 常见任务 优先级
同步代码 普通代码、console.log 最高
微任务(Microtask) Promise.then/catch/finallyMutationObserverqueueMicrotask
宏任务(Macrotask) setTimeoutsetIntervalI/OrequestAnimationFrameMessageChannel
javascript 复制代码
事件循环流程:
┌───────────────────────────┐
│    执行同步代码(调用栈)    │
└────────────┬──────────────┘
             ↓
┌───────────────────────────┐
│  清空所有微任务队列         │ ← Promise.then / queueMicrotask
│ (如果微任务中产生新的微     │
│  任务,继续执行,直到清空)  │
└────────────┬──────────────┘
             ↓
┌───────────────────────────┐
│  浏览器渲染(如有需要)     │ ← requestAnimationFrame 在此执行
└────────────┬──────────────┘
             ↓
┌───────────────────────────┐
│  取出一个宏任务执行         │ ← setTimeout / setInterval
└────────────┬──────────────┘
             ↓
        回到第一步...
javascript 复制代码
// ✅ 基础示例
console.log('1')                                    // 同步
setTimeout(() => console.log('2'), 0)               // 宏任务
Promise.resolve().then(() => console.log('3'))       // 微任务
  .then(() => console.log('4'))                     // 微任务
console.log('5')                                    // 同步
// 输出顺序:1 → 5 → 3 → 4 → 2

// ✅ 复杂示例(高频面试题)
async function async1() {
  console.log('async1 start')      // 2 同步
  await async2()                    // await 后面的代码变成微任务
  console.log('async1 end')        // 6 微任务
}
async function async2() {
  console.log('async2')            // 3 同步(await 的表达式会立即执行)
}

console.log('script start')        // 1 同步
setTimeout(() => console.log('setTimeout'), 0)  // 8 宏任务

async1()

new Promise(resolve => {
  console.log('promise1')          // 4 同步(Promise 构造函数是同步的!)
  resolve()
}).then(() => console.log('promise2'))  // 7 微任务

console.log('script end')          // 5 同步

// 输出:script start → async1 start → async2 → promise1 → script end
//       → async1 end → promise2 → setTimeout

宏任务与微任务的关键区别:

  • 微任务 在当前宏任务执行完后立即执行,不需要等待渲染
  • 宏任务需要等到下一轮事件循环才执行
  • 微任务中产生的微任务会在当轮清空,不会等到下一轮
  • 如果微任务队列无限增长,会阻塞渲染和宏任务的执行

💡 面试加分点: await 本质是 Promise.then 的语法糖。Node.js 中还有 process.nextTick(优先级高于 Promise.then)和 setImmediate(在 I/O 回调后执行)。


4. 防抖(debounce)和节流(throttle)的区别?

对比 防抖(debounce) 节流(throttle)
核心思想 事件停止触发后 n 秒才执行 n 秒内只执行一次
比喻 电梯等人:有人进来就重新等 技能 CD:冷却好了才能放
适用场景 搜索框输入、窗口 resize 滚动事件、鼠标移动、按钮点击
javascript 复制代码
// ✅ 防抖:事件触发后等待 n 秒,若期间再次触发则重新计时
function debounce(fn, delay, immediate = false) {
  let timer = null
  return function(...args) {
    const callNow = immediate && !timer
    clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      if (!immediate) fn.apply(this, args)
    }, delay)
    if (callNow) fn.apply(this, args)  // 立即执行模式
  }
}

// ✅ 节流:n 秒内只执行一次(时间戳版)
function throttle(fn, interval) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= interval) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

// ✅ 节流(定时器版,最后一次也能执行)
function throttleTimer(fn, interval) {
  let timer = null
  return function(...args) {
    if (timer) return
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, interval)
  }
}

// ========== 使用示例 ==========

// 搜索框输入(防抖):用户停止输入 500ms 后才发请求
const handleSearch = debounce((value) => {
  console.log('搜索:', value)
}, 500)

// 页面滚动(节流):每 200ms 最多执行一次
const handleScroll = throttle(() => {
  console.log('滚动位置:', window.scrollY)
}, 200)

// 按钮提交(防抖 + 立即执行):点击立即执行,短时间内重复点击无效
const handleSubmit = debounce(() => {
  console.log('提交表单')
}, 1000, true)

window.addEventListener('scroll', handleScroll)

💡 面试加分点: Lodash 的 _.debounce_.throttle 提供了 leading(前缘触发)和 trailing(后缘触发)选项。React 中推荐使用 useDeferredValueuseTransition(React 18+)来替代手写防抖处理搜索场景。


5. 深拷贝和浅拷贝的区别?如何实现深拷贝?

对比 浅拷贝 深拷贝
定义 只拷贝第一层,嵌套对象共享引用 递归拷贝所有层级,完全独立
嵌套对象 修改会互相影响 修改互不影响

深拷贝方案对比:

方案 循环引用 函数 Symbol Date/RegExp 性能
JSON.parse/stringify ❌ 报错 ❌ 丢失 ❌ 丢失 ❌ 变字符串
structuredClone ❌ 报错
手写递归 自定义
Lodash _.cloneDeep 最完善
javascript 复制代码
// ========== 浅拷贝 ==========
const obj = { a: 1, b: { c: 2 } }
const shallow1 = Object.assign({}, obj)
const shallow2 = { ...obj }
shallow1.b.c = 99  // ⚠️ 会影响原对象!obj.b.c 也变成 99

// ========== 深拷贝方法1:JSON(最简单,但有局限) ==========
const deep1 = JSON.parse(JSON.stringify(obj))

// ========== 深拷贝方法2:structuredClone(现代浏览器推荐 ✅) ==========
const deep2 = structuredClone(obj)

// ========== 深拷贝方法3:手写递归(面试重点 ✅) ==========
function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj
  if (map.has(obj)) return map.get(obj)  // 处理循环引用
  if (obj instanceof Date) return new Date(obj.getTime())
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
  if (obj instanceof Map) {
    const mapClone = new Map()
    map.set(obj, mapClone)
    obj.forEach((val, key) => mapClone.set(deepClone(key, map), deepClone(val, map)))
    return mapClone
  }
  if (obj instanceof Set) {
    const setClone = new Set()
    map.set(obj, setClone)
    obj.forEach(val => setClone.add(deepClone(val, map)))
    return setClone
  }
  const clone = Array.isArray(obj) ? [] : {}
  map.set(obj, clone)
  for (const key of Reflect.ownKeys(obj)) {
    clone[key] = deepClone(obj[key], map)
  }
  return clone
}

// 测试
const original = { a: 1, b: { c: 2 }, d: new Date(), e: /test/g }
original.self = original  // 循环引用
const cloned = deepClone(original)
cloned.b.c = 99
console.log(original.b.c)  // 2(互不影响)

💡 面试加分点: 使用 WeakMap 而不是 Map 来存储已拷贝对象------WeakMap 的键是弱引用,拷贝完成后不会阻止垃圾回收。structuredClone 是 2022 年新增的全局方法,生产环境推荐使用。


6. Promise 的用法和原理?

Promise 是异步编程的核心方案,解决了回调地狱问题。它代表一个异步操作的最终结果

三种状态(不可逆):

  • pending(等待中)→ fulfilled(已成功):通过 resolve(value) 触发
  • pending(等待中)→ rejected(已失败):通过 reject(reason) 触发
javascript 复制代码
// ✅ 基本用法
const fetchUser = (id) => new Promise((resolve, reject) => {
  setTimeout(() => {
    if (id > 0) resolve({ id, name: '张三' })
    else reject(new Error('Invalid ID'))
  }, 1000)
})

// ✅ 链式调用
fetchUser(1)
  .then(user => user.name)
  .then(name => console.log(name))  // '张三'
  .catch(err => console.error(err))
  .finally(() => console.log('完成'))

// ========== Promise 静态方法 ==========

// ✅ Promise.all:全部成功才成功,任一失败则失败
Promise.all([fetchUser(1), fetchUser(2)])
  .then(([user1, user2]) => console.log(user1, user2))

// ✅ Promise.allSettled:等待所有完成(不管成功失败)
Promise.allSettled([fetchUser(1), fetchUser(-1)])
  .then(results => {
    results.forEach(r => {
      if (r.status === 'fulfilled') console.log('成功:', r.value)
      if (r.status === 'rejected') console.log('失败:', r.reason)
    })
  })

// ✅ Promise.race:第一个完成的(请求超时处理)
function fetchWithTimeout(promise, timeout) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('请求超时')), timeout)
    )
  ])
}

// ✅ Promise.any:第一个成功的(全部失败才失败)
Promise.any([fetchUser(-1), fetchUser(2)])
  .then(first => console.log('第一个成功:', first))
方法 成功条件 失败条件 结果类型
all 全部成功 任一失败 成功值数组
allSettled 全部完成 不会失败 {status, value/reason} 数组
race 第一个完成 第一个完成(如果是失败) 第一个的结果
any 第一个成功 全部失败 第一个成功的值

7. async/await 的用法?

javascript 复制代码
async function fetchUserData(userId) {
  try {
    const user = await fetchUser(userId)
    const posts = await fetchPosts(user.id)
    return { user, posts }
  } catch (error) {
    console.error('请求失败:', error)
    throw error
  }
}

// 并行请求(推荐,比串行快)
async function fetchAll() {
  const [user, posts] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
  ])
  return { user, posts }
}

// 错误处理封装(Go 风格)
const to = (promise) => promise.then(data => [null, data]).catch(err => [err, null])

async function main() {
  const [err, user] = await to(fetchUser(1))
  if (err) return console.error(err)
  console.log(user)
}

async/await 的本质:

  • async 函数总是返回一个 Promise
  • await 相当于 Promise.then 的语法糖
  • await 后面的代码相当于在 .then() 的回调中执行(微任务)

8. 垃圾回收机制是什么?

JavaScript 的内存管理是自动的。垃圾回收器(GC)会自动找出不再使用的内存并释放。

两种主要算法:

算法 原理 状态
标记清除(Mark-and-Sweep) 从根对象出发,标记所有可达对象,清除不可达的 ✅ 现代浏览器使用
引用计数 记录每个对象被引用的次数,为 0 时回收 ❌ 已废弃

V8 引擎的分代回收策略:

  • 新生代(Young Generation): 存活时间短的对象(Scavenge 算法)
  • 老生代(Old Generation): 存活时间长的对象(Mark-Sweep + Mark-Compact)
javascript 复制代码
// ========== 常见内存泄漏场景 ==========

// ❌ 1. 意外的全局变量
function leak() {
  leakedVar = '我是全局变量'  // 没有 var/let/const
}
// ✅ 修复:使用 'use strict' 或始终用 let/const

// ❌ 2. 未清除的定时器
const timer = setInterval(() => { /* ... */ }, 1000)
// ✅ 修复:clearInterval(timer)

// ❌ 3. 未移除的事件监听
element.addEventListener('click', handler)
// ✅ 修复:element.removeEventListener('click', handler)
// 或使用 AbortController 统一管理
const controller = new AbortController()
element.addEventListener('click', handler, { signal: controller.signal })
controller.abort()  // 一次性移除所有关联监听

// ❌ 4. 闭包持有大对象引用
function createLeak() {
  const bigData = new Array(1000000).fill('data')
  return () => bigData.length
}
// ✅ 修复:只保留需要的值
function createFixed() {
  const bigData = new Array(1000000).fill('data')
  const length = bigData.length
  return () => length
}

// ❌ 5. DOM 引用未清除
const elements = { button: document.getElementById('btn') }
document.body.removeChild(document.getElementById('btn'))
// ✅ 修复:elements.button = null

// ❌ 6. console.log 在生产环境
console.log(hugeObject)  // DevTools 开启时对象不会被回收

💡 面试加分点: Chrome DevTools 的 Memory 面板可以拍摄堆快照查找内存泄漏。WeakRefFinalizationRegistry(ES2021)允许创建弱引用和注册垃圾回收回调。


9. 内存泄漏的原因及处理方式?

内存泄漏是指不再需要的内存没有被及时释放,导致内存占用持续增长,最终可能导致页面卡顿甚至崩溃。

javascript 复制代码
// ========== 常见内存泄漏原因及解决方案 ==========

// 1️⃣ 意外的全局变量
function foo() {
  bar = 'hello'  // ❌ 未声明的变量自动成为全局变量
  this.baz = 'world'  // ❌ this 指向 window
}
// ✅ 解决:使用严格模式 'use strict',始终使用 let/const

// 2️⃣ 被遗忘的定时器和回调
const data = fetchHugeData()
const timerId = setInterval(() => {
  // ❌ data 被闭包引用,无法回收
  process(data)
}, 1000)
// ✅ 解决:组件销毁时清除定时器
// clearInterval(timerId)

// 3️⃣ 脱离 DOM 的引用
const btn = document.getElementById('button')
document.body.removeChild(btn)
// ❌ btn 变量仍然引用着已删除的 DOM 节点
// ✅ 解决:btn = null

// 4️⃣ 不合理的闭包
function outer() {
  const largeObj = { data: new Array(100000).fill('x') }
  return function inner() {
    // ❌ 即使 inner 不使用 largeObj,某些引擎仍可能保留整个闭包作用域
    console.log('hello')
  }
}
// ✅ 解决:确保闭包只引用必要的变量

// 5️⃣ Map/Set 存储对象引用
const cache = new Map()
function process(obj) {
  cache.set(obj, computeResult(obj))
  // ❌ obj 被 Map 强引用,即使外部不再需要也不会被回收
}
// ✅ 解决:使用 WeakMap/WeakSet
const weakCache = new WeakMap()

排查内存泄漏的工具:

  • Chrome DevTools → Performance → 观察内存曲线是否持续上升
  • Chrome DevTools → Memory → 拍摄堆快照(Heap Snapshot)对比
  • performance.memory API(仅 Chrome)

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

特性 普通函数 箭头函数
this 调用时动态确定 定义时继承外层 this
arguments ✅ 有 ❌ 没有(用 ...rest 替代)
new 调用 ✅ 可以 ❌ 不可以(没有 \[Construct])
prototype ✅ 有 ❌ 没有
yield ✅ 可以做生成器 ❌ 不可以
javascript 复制代码
// ========== this 指向差异(最核心的区别)==========
const obj = {
  name: '张三',
  // 普通函数:this 指向调用者
  greet() {
    console.log(this.name)  // '张三'
  },
  // 箭头函数:this 继承定义时的外层作用域
  greetArrow: () => {
    console.log(this.name)  // undefined(外层是全局/模块作用域)
  }
}

// ========== 回调场景中的优势 ==========
const timer = {
  seconds: 0,
  start() {
    // ❌ 普通函数:this 指向 window
    setInterval(function() {
      this.seconds++  // NaN(this 不是 timer)
    }, 1000)
    // ✅ 箭头函数:this 继承 start 的 this
    setInterval(() => {
      this.seconds++  // 正常(this 是 timer)
    }, 1000)
  }
}

// ========== arguments 差异 ==========
function normalFn() {
  console.log(arguments)  // ✅ Arguments 对象
}
const arrowFn = () => {
  // console.log(arguments)  // ❌ ReferenceError
}
const arrowWithRest = (...args) => {
  console.log(args)  // ✅ 使用 rest 参数替代
}

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

// ========== 不能用作生成器 ==========
// const gen = *() => {}  // ❌ SyntaxError

💡 面试加分点: 箭头函数的 this 不能通过 call/apply/bind 修改。在 React 类组件中,事件处理推荐用箭头函数避免 this 绑定问题。在对象字面量的方法中,不建议使用箭头函数(this 不会指向对象)。


11. 手写 call、apply、bind?

javascript 复制代码
// 手写 call
Function.prototype.myCall = function(context, ...args) {
  context = context ?? globalThis
  const key = Symbol('fn')
  context[key] = this
  const result = context[key](...args)
  delete context[key]
  return result
}

// 手写 apply
Function.prototype.myApply = function(context, args = []) {
  context = context ?? globalThis
  const key = Symbol('fn')
  context[key] = this
  const result = context[key](...args)
  delete context[key]
  return result
}

// 手写 bind
Function.prototype.myBind = function(context, ...outerArgs) {
  const fn = this
  return function(...innerArgs) {
    return fn.apply(context, [...outerArgs, ...innerArgs])
  }
}

// 测试
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`
}
greet.myCall({ name: '张三' }, 'Hello', '!')   // 'Hello, 张三!'
greet.myApply({ name: '李四' }, ['Hi', '?'])   // 'Hi, 李四?'
const bound = greet.myBind({ name: '王五' }, 'Hey')
bound('.')  // 'Hey, 王五.'

12. 手写 new 操作符?

javascript 复制代码
function myNew(Constructor, ...args) {
  // 1. 创建新对象,原型指向构造函数的 prototype
  const obj = Object.create(Constructor.prototype)
  // 2. 执行构造函数,this 指向新对象
  const result = Constructor.apply(obj, args)
  // 3. 如果构造函数返回对象,则返回该对象;否则返回新对象
  return result instanceof Object ? result : obj
}

function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`
}

const p = myNew(Person, '张三', 25)
p.greet()  // 'Hello, I'm 张三'
p instanceof Person  // true

13. 什么是柯里化(Currying)?

javascript 复制代码
// 柯里化:将多参数函数转换为一系列单参数函数

// 基本柯里化
const add = (a) => (b) => (c) => a + b + c
add(1)(2)(3)  // 6

// 通用柯里化函数
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    }
    return function(...moreArgs) {
      return curried.apply(this, [...args, ...moreArgs])
    }
  }
}

const sum = (a, b, c) => a + b + c
const curriedSum = curry(sum)
curriedSum(1)(2)(3)    // 6
curriedSum(1, 2)(3)    // 6
curriedSum(1)(2, 3)    // 6

// 实际应用:参数复用
const multiply = curry((a, b) => a * b)
const double = multiply(2)
const triple = multiply(3)
double(5)  // 10
triple(5)  // 15

14. 什么是函数式编程?常用方法有哪些?

javascript 复制代码
// 纯函数:相同输入总是返回相同输出,无副作用
const add = (a, b) => a + b  // 纯函数
let count = 0
const increment = () => ++count  // 非纯函数(有副作用)

// 不可变性
const original = [1, 2, 3]
const newArr = [...original, 4]  // 不修改原数组

// 高阶函数:接受或返回函数
const numbers = [1, 2, 3, 4, 5, 6]
const doubled = numbers.map(n => n * 2)       // [2, 4, 6, 8, 10, 12]
const evens = numbers.filter(n => n % 2 === 0) // [2, 4, 6]
const sum = numbers.reduce((acc, n) => acc + n, 0) // 21

// 组合(compose)与管道(pipe)
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x)
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x)

const double = x => x * 2
const addOne = x => x + 1
const square = x => x * x

const transform = pipe(double, addOne, square)
transform(3)  // ((3*2)+1)^2 = 49

15. 什么是 WeakMap 和 WeakSet?

javascript 复制代码
// WeakMap:键必须是对象,弱引用(不阻止垃圾回收)
const weakMap = new WeakMap()
let obj = { name: '张三' }
weakMap.set(obj, '关联数据')
weakMap.get(obj)  // '关联数据'
obj = null  // obj 被垃圾回收,weakMap 中的条目也自动删除

// 应用:存储私有数据
const _private = new WeakMap()
class Person {
  constructor(name, age) {
    _private.set(this, { age })
    this.name = name
  }
  getAge() { return _private.get(this).age }
}

// WeakSet:存储对象的弱引用集合
const weakSet = new WeakSet()
let element = document.querySelector('.btn')
weakSet.add(element)
weakSet.has(element)  // true
element = null  // 自动从 weakSet 中移除

// 应用:标记已处理的对象(防止重复处理)
const processed = new WeakSet()
function processOnce(obj) {
  if (processed.has(obj)) return
  processed.add(obj)
  // 处理逻辑...
}

WeakMap/WeakSet vs Map/Set 对比:

特性 Map/Set WeakMap/WeakSet
键类型 任意 只能是对象
引用类型 强引用 弱引用
可遍历 ❌(不可迭代)
size 属性
GC 回收 不回收 键对象无其他引用时自动回收

16. 手写 Promise?

javascript 复制代码
class MyPromise {
  static PENDING = 'pending'
  static FULFILLED = 'fulfilled'
  static REJECTED = 'rejected'

  constructor(executor) {
    this.status = MyPromise.PENDING
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value) => {
      if (this.status === MyPromise.PENDING) {
        this.status = MyPromise.FULFILLED
        this.value = value
        this.onFulfilledCallbacks.forEach(fn => fn(value))
      }
    }

    const reject = (reason) => {
      if (this.status === MyPromise.PENDING) {
        this.status = MyPromise.REJECTED
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn(reason))
      }
    }

    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
    onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e }

    return new MyPromise((resolve, reject) => {
      const handle = (fn, value) => {
        try {
          const result = fn(value)
          result instanceof MyPromise ? result.then(resolve, reject) : resolve(result)
        } catch (err) {
          reject(err)
        }
      }

      if (this.status === MyPromise.FULFILLED) {
        setTimeout(() => handle(onFulfilled, this.value))
      } else if (this.status === MyPromise.REJECTED) {
        setTimeout(() => handle(onRejected, this.reason))
      } else {
        this.onFulfilledCallbacks.push((value) => setTimeout(() => handle(onFulfilled, value)))
        this.onRejectedCallbacks.push((reason) => setTimeout(() => handle(onRejected, reason)))
      }
    })
  }

  catch(onRejected) { return this.then(null, onRejected) }

  static resolve(value) { return new MyPromise(resolve => resolve(value)) }
  static reject(reason) { return new MyPromise((_, reject) => reject(reason)) }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = []
      let count = 0
      promises.forEach((p, i) => {
        MyPromise.resolve(p).then(value => {
          results[i] = value
          if (++count === promises.length) resolve(results)
        }, reject)
      })
    })
  }
}

17. 什么是尾调用优化?

javascript 复制代码
// 普通递归:每次调用都会创建新的栈帧,可能导致栈溢出
function factorial(n) {
  if (n <= 1) return 1
  return n * factorial(n - 1)  // 不是尾调用(还需要乘法)
}

// 尾调用优化:函数的最后一步是调用另一个函数
function factorialTCO(n, acc = 1) {
  if (n <= 1) return acc
  return factorialTCO(n - 1, n * acc)  // 尾调用,可以复用栈帧
}

// 斐波那契数列(尾递归)
function fibonacci(n, a = 0, b = 1) {
  if (n === 0) return a
  return fibonacci(n - 1, b, a + b)
}
fibonacci(10)  // 55

💡 面试加分点: 尾调用优化(TCO)目前只有 Safari 完全支持,V8 引擎尚未实现。实际开发中可用循环代替递归来避免栈溢出。


18. 什么是 Proxy 和 Reflect?

javascript 复制代码
// Proxy:拦截对象操作(Vue3 响应式的核心)
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)
  },
  has(target, key) {
    console.log(`检查属性: ${key}`)
    return Reflect.has(target, key)
  },
  deleteProperty(target, key) {
    console.log(`删除属性: ${key}`)
    return Reflect.deleteProperty(target, key)
  }
}

const obj = new Proxy({ count: 0 }, handler)
obj.count        // 读取属性: count → 0
obj.count = 10   // 设置数字成功
'count' in obj   // 检查属性: count → true

// ✅ 实用示例:数据验证
function createValidator(schema) {
  return new Proxy({}, {
    set(target, key, value) {
      if (schema[key] && !schema[key](value)) {
        throw new Error(`${key} 验证失败`)
      }
      return Reflect.set(target, key, value)
    }
  })
}

const user = createValidator({
  name: (v) => typeof v === 'string' && v.length > 0,
  age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
})
user.name = '张三'  // ✅
user.age = 25       // ✅
// user.age = -1     // ❌ Error: age 验证失败

💡 面试加分点: Proxy 可以拦截 13 种操作(get/set/has/deleteProperty/apply/construct 等),比 Object.defineProperty 更强大。Vue3 用 Proxy 替代了 Vue2 的 Object.defineProperty 来实现响应式,解决了无法检测新增属性和数组索引修改的问题。


19. 浅比较和 Object.is()?

javascript 复制代码
// ========== Object.is():比 === 更精确 ==========
Object.is(NaN, NaN)   // true(=== 返回 false)
Object.is(+0, -0)     // false(=== 返回 true)
Object.is(1, 1)       // true(与 === 相同)

// ========== 浅比较(Shallow Equal)==========
// React 中 PureComponent / React.memo 使用的比较策略
function shallowEqual(objA, objB) {
  if (Object.is(objA, objB)) return true
  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) return false

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)
  if (keysA.length !== keysB.length) return false

  for (const key of keysA) {
    if (!Object.hasOwn(objB, key) || !Object.is(objA[key], objB[key])) {
      return false
    }
  }
  return true
}

shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 })                    // true
shallowEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })     // false(嵌套对象不同引用)

💡 面试加分点: React 的性能优化核心就是浅比较------React.memouseMemouseCallback 都依赖浅比较。这也是为什么 React 强调不可变数据


20. 手写数组扁平化(flat)?

javascript 复制代码
// ✅ 方法1:递归
function flatten(arr, depth = 1) {
  if (depth <= 0) return arr.slice()
  return arr.reduce((acc, item) => {
    return acc.concat(Array.isArray(item) ? flatten(item, depth - 1) : item)
  }, [])
}

// ✅ 方法2:迭代(使用栈)
function flattenIterative(arr) {
  const stack = [...arr]
  const result = []
  while (stack.length) {
    const item = stack.pop()
    Array.isArray(item) ? stack.push(...item) : result.unshift(item)
  }
  return result
}

// ✅ 方法3:Generator
function* flattenGen(arr, depth = Infinity) {
  for (const item of arr) {
    if (Array.isArray(item) && depth > 0) {
      yield* flattenGen(item, depth - 1)
    } else {
      yield item
    }
  }
}

// 测试
const nested = [1, [2, [3, [4, [5]]]]]
flatten(nested, Infinity)    // [1, 2, 3, 4, 5]
flattenIterative(nested)     // [1, 2, 3, 4, 5]
[...flattenGen(nested)]      // [1, 2, 3, 4, 5]
nested.flat(Infinity)        // [1, 2, 3, 4, 5](原生方法)

21. 手写数组去重的方法?

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

// ✅ 方法1:Set(最简洁,推荐)
const unique1 = [...new Set(arr)]  // [1, 2, 3, 4, 5]

// ✅ 方法2:filter + indexOf
const unique2 = arr.filter((item, index) => arr.indexOf(item) === index)

// ✅ 方法3:reduce
const unique3 = arr.reduce((acc, item) => {
  return acc.includes(item) ? acc : [...acc, item]
}, [])

// ✅ 方法4:Map(适合对象数组按某个字段去重)
function uniqueByKey(arr, key) {
  const map = new Map()
  return arr.filter(item => {
    const k = item[key]
    if (map.has(k)) return false
    map.set(k, true)
    return true
  })
}

const users = [
  { id: 1, name: '张三' },
  { id: 2, name: '李四' },
  { id: 1, name: '张三(重复)' },
]
uniqueByKey(users, 'id')  // [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]

// ⚠️ 注意:Set 使用 === 比较(引用类型去重无效)

22. 继承的实现方式有哪些?

javascript 复制代码
// ========== 1. 原型链继承 ==========
function Parent1() { this.colors = ['red', 'blue'] }
Parent1.prototype.say = function() { return 'hello' }
function Child1() {}
Child1.prototype = new Parent1()
// ❌ 缺点:所有子实例共享父类引用属性
const c1 = new Child1()
const c2 = new Child1()
c1.colors.push('green')
console.log(c2.colors)  // ['red', 'blue', 'green'] ← 被影响了

// ========== 2. 借用构造函数继承 ==========
function Parent2(name) { this.name = name; this.colors = ['red'] }
function Child2(name) { Parent2.call(this, name) }
// ❌ 缺点:无法继承原型上的方法

// ========== 3. 组合继承(最常用) ==========
function Parent3(name) { this.name = name }
Parent3.prototype.say = function() { return `I'm ${this.name}` }
function Child3(name, age) {
  Parent3.call(this, name)   // 继承实例属性
  this.age = age
}
Child3.prototype = new Parent3()     // 继承原型方法
Child3.prototype.constructor = Child3
// ❌ 缺点:Parent3 构造函数被调用了两次

// ========== 4. 寄生组合继承(最优方案)==========
function Parent4(name) { this.name = name }
Parent4.prototype.say = function() { return `I'm ${this.name}` }
function Child4(name, age) {
  Parent4.call(this, name)
  this.age = age
}
Child4.prototype = Object.create(Parent4.prototype)  // ✅ 不会调用 Parent4
Child4.prototype.constructor = Child4

const child = new Child4('张三', 25)
child.say()  // "I'm 张三"
child instanceof Parent4  // true

// ========== 5. ES6 class 继承(语法糖,底层是寄生组合继承)==========
class Parent5 {
  constructor(name) { this.name = name }
  say() { return `I'm ${this.name}` }
}
class Child5 extends Parent5 {
  constructor(name, age) {
    super(name)  // 必须先调用 super
    this.age = age
  }
}

// ========== 6. 混入(Mixin)继承 ==========
const Serializable = {
  serialize() { return JSON.stringify(this) }
}
const Loggable = {
  log() { console.log(this.toString()) }
}
class Base {}
Object.assign(Base.prototype, Serializable, Loggable)

💡 面试加分点: ES6 classextends 底层就是寄生组合继承。ES6 中子类必须在 constructor 中调用 super() 后才能使用 this


23. ES6 class 的核心知识点?

ES6 class 是基于原型继承的语法糖,提供了更清晰、更面向对象的写法,但本质仍然是构造函数 + 原型链。

23.1 基本语法与构造函数

javascript 复制代码
// ========== 基本 class 声明 ==========
class Person {
  // 构造函数:new 时自动调用
  constructor(name, age) {
    // 实例属性:每个实例独有
    this.name = name
    this.age = age
  }

  // 原型方法:定义在 Person.prototype 上,所有实例共享
  greet() {
    return `你好,我是 ${this.name},今年 ${this.age} 岁`
  }

  // 原型方法
  toString() {
    return `[Person: ${this.name}]`
  }
}

const p = new Person('张三', 25)
p.greet()  // '你好,我是 张三,今年 25 岁'

// ⚠️ class 不存在变量提升(与函数声明不同)
// new Foo()  // ❌ ReferenceError
// class Foo {}

// ⚠️ class 内部默认严格模式
// ⚠️ class 必须使用 new 调用,不能当普通函数用
// Person()  // ❌ TypeError: Class constructor Person cannot be invoked without 'new'

// ========== class 的本质 ==========
typeof Person  // 'function'
Person === Person.prototype.constructor  // true
// class 本质上就是构造函数

23.2 实例属性、静态属性与静态方法

javascript 复制代码
class Counter {
  // ✅ 实例属性(新写法,直接在类体中声明)
  count = 0
  name = 'default'

  // ✅ 静态属性:属于类本身,不属于实例
  static total = 0
  static MAX_COUNT = 100

  constructor(name) {
    this.name = name
    Counter.total++
  }

  // 实例方法
  increment() {
    if (this.count >= Counter.MAX_COUNT) {
      throw new Error('超过最大计数')
    }
    this.count++
  }

  // ✅ 静态方法:通过类名调用,不能通过实例调用
  static getTotal() {
    return Counter.total
  }

  // 静态方法常用于工厂方法
  static create(name) {
    return new Counter(name)
  }

  // 静态方法中的 this 指向类本身
  static reset() {
    this.total = 0  // this === Counter
  }
}

const c1 = new Counter('A')
const c2 = new Counter('B')
Counter.getTotal()  // 2
Counter.total       // 2

c1.increment()
c1.count  // 1
c2.count  // 0(实例属性互不影响)

// c1.getTotal()  // ❌ TypeError: c1.getTotal is not a function
// 静态方法只能通过类名调用

23.3 getter 和 setter

javascript 复制代码
class Temperature {
  #celsius = 0  // 私有属性

  constructor(celsius) {
    this.#celsius = celsius
  }

  // ✅ getter:访问属性时自动调用
  get fahrenheit() {
    return this.#celsius * 9 / 5 + 32
  }

  // ✅ setter:设置属性时自动调用(可加验证)
  set fahrenheit(value) {
    this.#celsius = (value - 32) * 5 / 9
  }

  get celsius() {
    return this.#celsius
  }

  set celsius(value) {
    if (typeof value !== 'number') throw new TypeError('必须是数字')
    if (value < -273.15) throw new RangeError('不能低于绝对零度')
    this.#celsius = value
  }
}

const temp = new Temperature(100)
temp.fahrenheit  // 212(调用 getter,像访问属性一样)
temp.fahrenheit = 32  // 调用 setter
temp.celsius    // 0

// temp.celsius = 'abc'  // ❌ TypeError: 必须是数字
// temp.celsius = -300   // ❌ RangeError: 不能低于绝对零度

23.4 私有属性和私有方法(#)

javascript 复制代码
class BankAccount {
  // ✅ 私有属性:以 # 开头,类外部完全不可访问
  #balance = 0
  #owner

  // ✅ 私有静态属性
  static #bankName = '中国银行'

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

  // ✅ 私有方法
  #validate(amount) {
    if (typeof amount !== 'number' || amount <= 0) {
      throw new Error('金额无效')
    }
  }

  deposit(amount) {
    this.#validate(amount)
    this.#balance += amount
    return this
  }

  withdraw(amount) {
    this.#validate(amount)
    if (amount > this.#balance) throw new Error('余额不足')
    this.#balance -= amount
    return this
  }

  get balance() {
    return this.#balance
  }

  // ✅ 私有静态方法
  static #log(message) {
    console.log(`[${this.#bankName}] ${message}`)
  }

  static createAccount(owner, balance) {
    this.#log(`创建账户: ${owner}`)
    return new BankAccount(owner, balance)
  }
}

const account = BankAccount.createAccount('张三', 1000)
account.deposit(500).withdraw(200)
account.balance  // 1300

// account.#balance   // ❌ SyntaxError: Private field
// account.#validate  // ❌ SyntaxError: Private field
// 私有成员在类外部完全不可访问,甚至子类也不能访问

// ✅ 检查对象是否拥有某个私有属性
class MyClass {
  #value
  static hasValue(obj) {
    return #value in obj  // in 运算符可检测私有属性
  }
}

23.5 继承(extends 和 super)

javascript 复制代码
class Animal {
  #name

  constructor(name) {
    this.#name = name
  }

  get name() { return this.#name }

  speak() {
    return `${this.#name} 发出声音`
  }

  // 静态方法也会被继承
  static create(name) {
    return new this(name)  // this 指向调用的类
  }
}

class Dog extends Animal {
  #breed

  constructor(name, breed) {
    // ⚠️ 子类 constructor 中必须先调用 super(),才能使用 this
    super(name)  // 调用父类构造函数
    this.#breed = breed
  }

  // 方法重写(Override)
  speak() {
    return `${this.name} 汪汪叫`
  }

  // 调用父类方法
  speakLoud() {
    return super.speak() + '(很大声)'  // super.method() 调用父类方法
  }

  get info() {
    return `${this.name} (${this.#breed})`
  }
}

class GuideDog extends Dog {
  #handler

  constructor(name, breed, handler) {
    super(name, breed)  // 调用 Dog 的 constructor
    this.#handler = handler
  }

  speak() {
    return `${this.name} 安静地引导 ${this.#handler}`
  }
}

const dog = new Dog('旺财', '柴犬')
dog.speak()      // '旺财 汪汪叫'
dog.speakLoud()  // '旺财 发出声音(很大声)'
dog.info         // '旺财 (柴犬)'

dog instanceof Dog     // true
dog instanceof Animal  // true

// 静态方法继承
const cat = Animal.create('小猫')  // Animal 实例
const puppy = Dog.create('小狗')   // Dog 实例(this 指向 Dog)

// ========== super 的两种用法 ==========
// 1. super() 作为函数:在子类 constructor 中调用父类构造函数
// 2. super.method():在子类方法中调用父类的同名方法

23.6 抽象类模式与多态

javascript 复制代码
// JS 没有原生 abstract 关键字,但可以模拟
class Shape {
  constructor(color = 'black') {
    // 模拟抽象类:不允许直接实例化
    if (new.target === Shape) {
      throw new Error('Shape 是抽象类,不能直接实例化')
    }
    this.color = color
  }

  // 模拟抽象方法:子类必须实现
  area() {
    throw new Error('子类必须实现 area() 方法')
  }

  // 模板方法模式:定义通用逻辑,具体步骤由子类实现
  describe() {
    return `这是一个 ${this.color} 的图形,面积为 ${this.area().toFixed(2)}`
  }
}

class Circle extends Shape {
  constructor(radius, color) {
    super(color)
    this.radius = radius
  }

  area() {
    return Math.PI * this.radius ** 2
  }
}

class Rectangle extends Shape {
  constructor(width, height, color) {
    super(color)
    this.width = width
    this.height = height
  }

  area() {
    return this.width * this.height
  }
}

// const s = new Shape()  // ❌ Error: Shape 是抽象类
const circle = new Circle(5, 'red')
const rect = new Rectangle(3, 4, 'blue')

// ✅ 多态:相同的方法调用,不同的行为
const shapes = [circle, rect]
shapes.forEach(shape => {
  console.log(shape.describe())
})
// '这是一个 red 的图形,面积为 78.54'
// '这是一个 blue 的图形,面积为 12.00'

23.7 Mixin 模式(多继承的替代方案)

javascript 复制代码
// JS 不支持多继承,但可以用 Mixin 实现类似效果

// 定义 Mixin 为"接受一个基类并返回扩展类"的函数
const Serializable = (Base) => class extends Base {
  toJSON() {
    const obj = {}
    for (const key of Object.keys(this)) {
      obj[key] = this[key]
    }
    return obj
  }

  serialize() {
    return JSON.stringify(this.toJSON())
  }
}

const Validatable = (Base) => class extends Base {
  validate() {
    for (const [key, value] of Object.entries(this)) {
      if (value === null || value === undefined) {
        throw new Error(`${key} 不能为空`)
      }
    }
    return true
  }
}

const Timestamped = (Base) => class extends Base {
  constructor(...args) {
    super(...args)
    this.createdAt = new Date()
    this.updatedAt = new Date()
  }

  touch() {
    this.updatedAt = new Date()
  }
}

// ✅ 组合多个 Mixin
class User extends Timestamped(Validatable(Serializable(class {}))) {
  constructor(name, email) {
    super()
    this.name = name
    this.email = email
  }
}

const user = new User('张三', 'zhangsan@example.com')
user.validate()    // true
user.serialize()   // '{"name":"张三","email":"zhangsan@example.com","createdAt":"...","updatedAt":"..."}'
user.touch()       // 更新时间戳

23.8 class vs 构造函数 对比

javascript 复制代码
// ========== 构造函数写法 ==========
function PersonFn(name) {
  this.name = name
}
PersonFn.prototype.greet = function() {
  return `Hello, ${this.name}`
}
PersonFn.create = function(name) {
  return new PersonFn(name)
}

// ========== class 写法 ==========
class PersonClass {
  constructor(name) {
    this.name = name
  }
  greet() {
    return `Hello, ${this.name}`
  }
  static create(name) {
    return new PersonClass(name)
  }
}
对比项 构造函数 class
语法 分散(原型方法需要单独定义) 集中(所有成员在一个代码块内)
提升 ✅ 函数声明会提升 ❌ 不提升(暂时性死区)
严格模式 需要手动开启 默认严格模式
new 调用 可以不用 new(但结果可能不对) 必须使用 new
方法可枚举 prototype 上的方法可枚举 方法不可枚举
私有成员 只能通过闭包/WeakMap 模拟 原生 # 私有属性和方法
继承 手动实现(寄生组合继承) extends + super

💡 面试加分点: ① class 只是语法糖,typeof 一个 class 仍然是 'function';② 私有属性 # 是真正的硬私有(hard private),不同于 TypeScript 的 private(编译后消失);③ new.target 可以检测是否通过 new 调用,以及具体是哪个类在被实例化(实现抽象类);④ 静态方法中的 this 指向类本身,且静态方法会被子类继承;⑤ 实际开发中 React 已从 class 组件转向函数组件 + Hooks,但理解 class 仍是面试必考。


24. ES6 新增了哪些主要特性?

javascript 复制代码
// 1. let/const 块级作用域
let x = 1
const PI = 3.14

// 2. 箭头函数
const add = (a, b) => a + b

// 3. 模板字符串
const name = '张三'
console.log(`Hello, ${name}!`)

// 4. 解构赋值
const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 }
const [first, ...others] = [1, 2, 3, 4]

// 5. 展开运算符
const arr1 = [1, 2]; const arr2 = [...arr1, 3, 4]
const obj1 = { a: 1 }; const obj2 = { ...obj1, b: 2 }

// 6. Promise
const p = new Promise((resolve, reject) => { resolve('ok') })

// 7. class 类
class Animal {
  constructor(name) { this.name = name }
  speak() { return `${this.name} speaks` }
}

// 8. 模块化(import/export)
// export default function add(a, b) { return a + b }
// import add from './add.js'

// 9. Symbol
const sym = Symbol('description')

// 10. Map/Set/WeakMap/WeakSet
const map = new Map([['key', 'value']])
const set = new Set([1, 2, 3])

// 11. for...of 循环
for (const item of [1, 2, 3]) { console.log(item) }

// 12. 默认参数
function greet(name = '世界') { return `Hello, ${name}!` }

// 13. Proxy/Reflect
const proxy = new Proxy({}, { get: (t, k) => `访问了 ${k}` })

// 14. Generator
function* gen() { yield 1; yield 2; yield 3 }

// 15. 可选链 ?.(ES2020)和空值合并 ??(ES2020)
const value = obj?.a?.b ?? '默认值'

25. forEach 和 map 的区别?

对比 forEach map
返回值 undefined 新数组
是否改变原数组 不改变(但回调中可以修改) 不改变
可否中断 ❌ 不能用 break ❌ 不能用 break
链式调用 ✅(返回数组可继续 .filter/.reduce)
使用场景 遍历执行副作用 转换数据生成新数组
javascript 复制代码
const numbers = [1, 2, 3, 4, 5]

// forEach:遍历,执行操作,不返回新数组
numbers.forEach((num, index) => {
  console.log(`索引 ${index}: ${num}`)
})

// map:转换数据,返回新数组
const doubled = numbers.map(num => num * 2)
console.log(doubled)  // [2, 4, 6, 8, 10]

// ✅ map 可链式调用
const result = numbers
  .map(n => n * 2)
  .filter(n => n > 5)
  .reduce((sum, n) => sum + n, 0)
console.log(result)  // 24 (6+8+10)

// ❌ forEach 的返回值是 undefined,不能链式调用
// numbers.forEach(n => n * 2).filter(...)  // TypeError

26. split() 和 join() 的区别?

javascript 复制代码
// split():字符串 → 数组(按分隔符拆分)
'hello world'.split(' ')      // ['hello', 'world']
'a,b,c'.split(',')            // ['a', 'b', 'c']
'hello'.split('')              // ['h', 'e', 'l', 'l', 'o']
'a-b-c'.split('-', 2)         // ['a', 'b'](限制返回数量)

// join():数组 → 字符串(用连接符合并)
['hello', 'world'].join(' ')  // 'hello world'
['a', 'b', 'c'].join(',')    // 'a,b,c'
['a', 'b', 'c'].join('')     // 'abc'
[1, 2, 3].join('-')          // '1-2-3'

// ✅ 经典用法:字符串反转
function reverseString(str) {
  return str.split('').reverse().join('')
}
reverseString('hello')  // 'olleh'

// ✅ URL 参数处理
const params = { name: '张三', age: 25 }
const queryString = Object.entries(params)
  .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
  .join('&')
// 'name=%E5%BC%A0%E4%B8%89&age=25'

27. 数组和字符串的常用方法?

javascript 复制代码
// ========== 数组常用方法 ==========

// 增删
const arr = [1, 2, 3]
arr.push(4)           // [1, 2, 3, 4](末尾添加,返回新长度)
arr.pop()             // [1, 2, 3](末尾删除,返回删除元素)
arr.unshift(0)        // [0, 1, 2, 3](头部添加)
arr.shift()           // [1, 2, 3](头部删除)
arr.splice(1, 1, 'a') // [1, 'a', 3](从索引1删除1个,插入'a')

// 查找
[1, 2, 3].indexOf(2)           // 1(找不到返回 -1)
[1, 2, 3].includes(2)          // true
[1, 2, 3].find(n => n > 1)     // 2(找到第一个满足条件的元素)
[1, 2, 3].findIndex(n => n > 1) // 1

// 转换
[1, 2, 3].map(n => n * 2)      // [2, 4, 6]
[1, 2, 3].filter(n => n > 1)   // [2, 3]
[1, 2, 3].reduce((a, b) => a + b, 0)  // 6

// 排序
[3, 1, 2].sort((a, b) => a - b)  // [1, 2, 3](升序)
[3, 1, 2].sort((a, b) => b - a)  // [3, 2, 1](降序)

// 其他
[1, [2, [3]]].flat(Infinity)   // [1, 2, 3]
[1, 2, 3].every(n => n > 0)    // true
[1, 2, 3].some(n => n > 2)     // true
Array.from({ length: 3 }, (_, i) => i)  // [0, 1, 2]

// ========== 字符串常用方法 ==========
const str = 'Hello World'
str.charAt(0)          // 'H'
str.indexOf('World')   // 6
str.includes('World')  // true
str.slice(0, 5)        // 'Hello'
str.substring(6)       // 'World'
str.toUpperCase()      // 'HELLO WORLD'
str.toLowerCase()      // 'hello world'
str.trim()             // 去除两端空格
str.replace('World', 'JS')  // 'Hello JS'
str.startsWith('Hello')     // true
str.endsWith('World')       // true
str.padStart(15, '*')       // '****Hello World'
str.repeat(2)               // 'Hello WorldHello World'

28. 迭代器协议(Iterator Protocol)是什么?

javascript 复制代码
// ✅ 手动实现一个可迭代对象
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 }
      }
    }
  }
}

for (const num of new Range(1, 5)) {
  console.log(num)  // 1, 2, 3, 4, 5
}
[...new Range(1, 5)]  // [1, 2, 3, 4, 5]

// ✅ 内置可迭代对象:Array、String、Map、Set、arguments、NodeList

// ✅ 普通对象默认不可迭代,但可以手动实现
const iterableObj = {
  a: 1, b: 2, c: 3,
  [Symbol.iterator]() {
    const entries = Object.entries(this)
    let index = 0
    return {
      next: () => index < entries.length
        ? { value: entries[index++], done: false }
        : { done: true }
    }
  }
}
for (const [key, value] of iterableObj) {
  console.log(key, value)  // a 1, b 2, c 3
}

29. 错误处理的最佳实践?

javascript 复制代码
// ========== try/catch/finally ==========
try {
  const data = JSON.parse(invalidJson)
} catch (error) {
  console.error(`解析失败: ${error.message}`)
} finally {
  console.log('清理完成')
}

// ✅ 自定义错误类型
class ValidationError extends Error {
  constructor(field, message) {
    super(message)
    this.name = 'ValidationError'
    this.field = field
  }
}

class NotFoundError extends Error {
  constructor(resource) {
    super(`${resource} 未找到`)
    this.name = 'NotFoundError'
    this.statusCode = 404
  }
}

// ✅ 按错误类型分别处理
try {
  validateAge('abc')
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`字段 ${error.field} 验证失败: ${error.message}`)
  } else if (error instanceof NotFoundError) {
    console.log(`404: ${error.message}`)
  } else {
    throw error  // 未知错误继续抛出
  }
}

// ✅ 全局错误捕获
window.addEventListener('error', (event) => {
  console.error('全局错误:', event.error)
})
window.addEventListener('unhandledrejection', (event) => {
  console.error('未捕获的 Promise 错误:', event.reason)
  event.preventDefault()
})

💡 面试加分点: 生产环境应配置全局错误捕获并上报到监控平台(如 Sentry)。async/await 中的错误必须用 try/catch 捕获。


30. cookie、sessionStorage、localStorage 的区别?

特性 cookie sessionStorage localStorage
存储大小 约 4KB 约 5MB 约 5MB
生命周期 可设置过期时间 页面会话期间(关闭标签页清除) 永久(手动清除)
服务端通信 ✅ 每次请求自动携带
作用域 同域名下所有标签页 当前标签页 同域名下所有标签页
API document.cookie sessionStorage.getItem/setItem localStorage.getItem/setItem
javascript 复制代码
// ========== cookie ==========
// 设置 cookie
document.cookie = 'username=张三; max-age=3600; path=/'  // 1小时后过期
document.cookie = 'token=abc123; secure; samesite=strict' // 安全设置

// 读取所有 cookie(返回字符串,需要手动解析)
console.log(document.cookie)  // 'username=张三; token=abc123'

// 封装 cookie 操作
function getCookie(name) {
  const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
  return match ? decodeURIComponent(match[1]) : null
}

// ========== localStorage ==========
localStorage.setItem('user', JSON.stringify({ name: '张三', age: 25 }))
const user = JSON.parse(localStorage.getItem('user'))
localStorage.removeItem('user')
localStorage.clear()  // 清除所有

// ========== sessionStorage ==========
sessionStorage.setItem('tempData', '临时数据')
sessionStorage.getItem('tempData')  // '临时数据'

// ✅ 监听 storage 变化(跨标签页通信)
window.addEventListener('storage', (event) => {
  console.log(`键 ${event.key} 从 ${event.oldValue} 变为 ${event.newValue}`)
})

💡 面试加分点: cookie 的 HttpOnly 属性可以防止 XSS 攻击读取 cookie;SameSite 属性可以防止 CSRF 攻击。localStoragestorage 事件可以实现跨标签页通信


31. 栈与队列、栈与堆的区别?

javascript 复制代码
// ========== 栈(Stack)vs 队列(Queue)==========
// 栈:后进先出(LIFO)--- 如浏览器的后退按钮、函数调用栈
// 队列:先进先出(FIFO)--- 如事件队列、消息队列

// 用数组模拟栈
const stack = []
stack.push(1)  // 入栈 [1]
stack.push(2)  // 入栈 [1, 2]
stack.push(3)  // 入栈 [1, 2, 3]
stack.pop()    // 出栈 3 → [1, 2]

// 用数组模拟队列
const queue = []
queue.push(1)    // 入队 [1]
queue.push(2)    // 入队 [1, 2]
queue.push(3)    // 入队 [1, 2, 3]
queue.shift()    // 出队 1 → [2, 3]

// ========== 栈内存 vs 堆内存 ==========
// 栈内存:存储基本类型值和引用地址,自动分配释放,速度快
// 堆内存:存储引用类型的实际数据,手动分配(GC 回收),速度较慢

let a = 10          // 基本类型 → 栈中直接存储值 10
let b = a           // 值拷贝 → 栈中新建一个 10
b = 20              // 修改 b 不影响 a
console.log(a)      // 10

let obj1 = { x: 1 } // 引用类型 → 堆中存对象,栈中存地址
let obj2 = obj1      // 地址拷贝 → obj2 和 obj1 指向同一个堆内存
obj2.x = 99          // 修改 obj2 影响 obj1(同一对象)
console.log(obj1.x)  // 99

32. 进程与线程的关系?

对比 进程(Process) 线程(Thread)
定义 系统资源分配的最小单位 CPU 调度执行的最小单位
关系 一个进程包含多个线程 线程属于某个进程
内存 独立内存空间 共享进程的内存
通信 IPC(进程间通信) 直接读写共享内存
开销 创建/销毁开销大 开销小
javascript 复制代码
// ========== 浏览器的多进程架构 ==========
// 浏览器主进程:UI、用户交互
// 渲染进程:每个标签页一个(页面渲染、JS 执行)
// GPU 进程:图形绘制
// 网络进程:网络请求
// 插件进程:浏览器插件

// ========== JS 是单线程的 ==========
// JS 主线程负责:DOM 操作、事件处理、定时器、网络回调等
// 通过事件循环(Event Loop)实现异步非阻塞

// ========== Web Worker:多线程方案 ==========
// 主线程
const worker = new Worker('worker.js')
worker.postMessage({ type: 'calculate', data: [1, 2, 3] })
worker.onmessage = (e) => {
  console.log('Worker 返回:', e.data)
}

// worker.js(独立线程,不能操作 DOM)
// self.onmessage = (e) => {
//   const result = heavyComputation(e.data)
//   self.postMessage(result)
// }

💡 面试加分点: 每个浏览器标签页通常是一个独立的渲染进程(进程隔离,一个页面崩溃不会影响其他页面)。JS 是单线程的原因是为了避免多线程操作 DOM 的复杂性。Web Worker 可以在后台线程执行耗时计算,但不能访问 DOM。


33. 数组随机排序(洗牌算法)?

javascript 复制代码
// ❌ 不推荐:sort + Math.random(不均匀)
const arr = [1, 2, 3, 4, 5]
arr.sort(() => Math.random() - 0.5)
// 这种方式的排序结果不是均匀分布的!

// ✅ Fisher-Yates 洗牌算法(均匀随机排列)
function shuffle(arr) {
  const result = [...arr]  // 不修改原数组
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    ;[result[i], result[j]] = [result[j], result[i]]  // 交换
  }
  return result
}

const original = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(shuffle(original))  // 每次都是随机顺序
console.log(original)           // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10](不变)

// ✅ 验证均匀性
function testShuffle(fn, arr, times = 100000) {
  const counts = {}
  for (let i = 0; i < times; i++) {
    const key = fn([...arr]).join(',')
    counts[key] = (counts[key] || 0) + 1
  }
  return counts
}

💡 面试加分点: Fisher-Yates 洗牌算法的时间复杂度是 O(n),空间复杂度 O(1)(原地交换)。sort(() => Math.random() - 0.5) 之所以不均匀,是因为排序算法(如快排)的比较次数和顺序会影响最终分布。


34. JavaScript 中常见的设计模式有哪些?

javascript 复制代码
// ========== 1. 单例模式(Singleton)==========
// 保证一个类只有一个实例
class Singleton {
  static instance = null
  constructor() {
    if (Singleton.instance) return Singleton.instance
    this.data = {}
    Singleton.instance = this
  }
  static getInstance() {
    if (!Singleton.instance) Singleton.instance = new Singleton()
    return Singleton.instance
  }
}
const a = Singleton.getInstance()
const b = Singleton.getInstance()
console.log(a === b)  // true

// 实际应用:全局状态管理、弹窗实例、日志记录器

// ========== 2. 观察者模式(Observer)==========
// 当对象状态变化时,自动通知所有依赖它的对象
class EventEmitter {
  constructor() {
    this.events = new Map()
  }
  on(event, callback) {
    if (!this.events.has(event)) this.events.set(event, [])
    this.events.get(event).push(callback)
    return this  // 支持链式调用
  }
  off(event, callback) {
    const callbacks = this.events.get(event)
    if (callbacks) {
      this.events.set(event, callbacks.filter(cb => cb !== callback))
    }
    return this
  }
  emit(event, ...args) {
    const callbacks = this.events.get(event)
    if (callbacks) callbacks.forEach(cb => cb(...args))
    return this
  }
  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args)
      this.off(event, wrapper)
    }
    return this.on(event, wrapper)
  }
}

const emitter = new EventEmitter()
emitter.on('login', (user) => console.log(`${user} 登录了`))
emitter.emit('login', '张三')  // '张三 登录了'

// 实际应用:Vue 的事件系统、Node.js 的 EventEmitter、自定义事件总线

// ========== 3. 发布-订阅模式 ==========
// 与观察者模式类似,但有一个中间的"消息中心"解耦
class PubSub {
  constructor() { this.channels = {} }
  subscribe(channel, callback) {
    if (!this.channels[channel]) this.channels[channel] = []
    this.channels[channel].push(callback)
    return () => this.unsubscribe(channel, callback)  // 返回取消订阅函数
  }
  unsubscribe(channel, callback) {
    this.channels[channel] = this.channels[channel]?.filter(cb => cb !== callback)
  }
  publish(channel, data) {
    this.channels[channel]?.forEach(cb => cb(data))
  }
}
// 实际应用:Redux、Vuex、消息队列

// ========== 4. 策略模式(Strategy)==========
// 定义一系列算法,让它们可以互相替换
const strategies = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
}
function calculate(strategy, a, b) {
  return strategies[strategy]?.(a, b) ?? null
}
calculate('add', 5, 3)       // 8
calculate('multiply', 5, 3)  // 15

// 实际应用:表单验证规则、价格计算策略、权限验证

// ========== 5. 代理模式(Proxy Pattern)==========
// 通过代理对象控制对目标对象的访问
function createCachedFetch(fetcher) {
  const cache = new Map()
  return new Proxy(fetcher, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args)
      if (cache.has(key)) return Promise.resolve(cache.get(key))
      return target.apply(thisArg, args).then(result => {
        cache.set(key, result)
        return result
      })
    }
  })
}

// ========== 6. 装饰器模式(Decorator)==========
// 动态地给对象添加功能,不修改原有代码
function withLogging(fn, name) {
  return function(...args) {
    console.log(`[${name}] 调用参数:`, args)
    const result = fn.apply(this, args)
    console.log(`[${name}] 返回结果:`, result)
    return result
  }
}

const add = (a, b) => a + b
const loggedAdd = withLogging(add, 'add')
loggedAdd(1, 2)
// [add] 调用参数: [1, 2]
// [add] 返回结果: 3

// 实际应用:日志记录、性能监控、权限校验、React 高阶组件(HOC)

💡 面试加分点: 观察者模式和发布-订阅模式的区别------观察者模式中主体直接通知观察者 (耦合),发布-订阅模式有事件中心作为中间层(解耦)。Vue2 的响应式系统用的是观察者模式(Dep/Watcher),Vue 组件间通信用 EventBus 是发布-订阅模式。


35. 手写 instanceof?

instanceof 检测构造函数的 prototype 是否出现在对象的原型链上。

javascript 复制代码
// ========== 手写 instanceof ==========
function myInstanceof(obj, Constructor) {
  // 基本类型直接返回 false
  if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
    return false
  }
  // 获取对象的原型
  let proto = Object.getPrototypeOf(obj)
  const prototype = Constructor.prototype

  // 沿原型链向上查找
  while (proto !== null) {
    if (proto === prototype) return true
    proto = Object.getPrototypeOf(proto)
  }
  return false
}

// 测试
function Animal(name) { this.name = name }
function Dog(name) {
  Animal.call(this, name)
}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog

const dog = new Dog('Rex')

console.log(myInstanceof(dog, Dog))     // true
console.log(myInstanceof(dog, Animal))  // true
console.log(myInstanceof(dog, Object))  // true
console.log(myInstanceof(dog, Array))   // false
console.log(myInstanceof([], Array))    // true
console.log(myInstanceof([], Object))   // true
console.log(myInstanceof(null, Object)) // false
console.log(myInstanceof(1, Number))    // false(基本类型)

// ========== instanceof 的注意事项 ==========
// 1. 基本类型无法使用(除非是包装对象)
42 instanceof Number           // false
new Number(42) instanceof Number  // true

// 2. 跨 iframe 失效(不同 window 的构造函数不同)
// iframe 中的 [] instanceof Array → false

// 3. 可以被 Symbol.hasInstance 修改
class EvenChecker {
  static [Symbol.hasInstance](num) {
    return typeof num === 'number' && num % 2 === 0
  }
}
console.log(4 instanceof EvenChecker)  // true
console.log(5 instanceof EvenChecker)  // false

💡 面试加分点: instanceof 的本质就是沿原型链查找------obj.__proto__.__proto__... 直到找到 Constructor.prototype 或到 nullSymbol.hasInstance 可以自定义 instanceof 的行为。


36. 手写 EventEmitter(发布-订阅模式)?

javascript 复制代码
class EventEmitter {
  constructor() {
    this._events = Object.create(null)  // 无原型的纯净对象
  }

  // 监听事件
  on(event, listener) {
    if (!this._events[event]) this._events[event] = []
    this._events[event].push(listener)
    return this
  }

  // 只监听一次
  once(event, listener) {
    const wrapper = (...args) => {
      listener.apply(this, args)
      this.off(event, wrapper)
    }
    wrapper._original = listener  // 保存原始引用,方便 off 移除
    return this.on(event, wrapper)
  }

  // 触发事件
  emit(event, ...args) {
    const listeners = this._events[event]
    if (!listeners || listeners.length === 0) return false
    // 拷贝一份,防止在回调中修改数组影响遍历
    const copy = [...listeners]
    copy.forEach(listener => listener.apply(this, args))
    return true
  }

  // 移除事件监听
  off(event, listener) {
    if (!listener) {
      // 不传 listener 则移除该事件的所有监听
      delete this._events[event]
    } else {
      this._events[event] = this._events[event]?.filter(
        fn => fn !== listener && fn._original !== listener
      )
    }
    return this
  }

  // 获取监听器数量
  listenerCount(event) {
    return this._events[event]?.length ?? 0
  }

  // 移除所有事件
  removeAllListeners() {
    this._events = Object.create(null)
    return this
  }
}

// ========== 测试 ==========
const bus = new EventEmitter()

function onLogin(user) {
  console.log(`${user} 登录了`)
}

// 基本使用
bus.on('login', onLogin)
bus.on('login', (user) => console.log(`欢迎 ${user}!`))
bus.emit('login', '张三')
// 张三 登录了
// 欢迎 张三!

// once:只触发一次
bus.once('init', () => console.log('初始化完成'))
bus.emit('init')  // '初始化完成'
bus.emit('init')  // 无输出(已移除)

// off:移除监听
bus.off('login', onLogin)
bus.emit('login', '李四')
// 欢迎 李四!(只有第二个监听器了)

// 链式调用
bus.on('a', () => {}).on('b', () => {}).emit('a')

// ========== 实际应用:组件间通信(事件总线)==========
// Vue2 中常用 EventBus
const EventBus = new EventEmitter()
// 组件 A 发送
EventBus.emit('data-updated', { id: 1, name: '新数据' })
// 组件 B 监听
EventBus.on('data-updated', (data) => console.log('收到更新:', data))

💡 面试加分点: Node.js 的 events.EventEmitter 是很多核心模块(如 fshttpstream)的基础。Vue2 实例本身就是一个 EventEmitter( on/on/ on/emit/$off)。内存泄漏风险:组件销毁时必须调用 off 移除监听器。


37. 如何实现并发控制?(控制 Promise 并发数量)

javascript 复制代码
// ========== 方法1:手写并发控制池 ==========
async function concurrentPool(tasks, maxConcurrency = 3) {
  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 cleanup = () => executing.delete(promise)
    promise.then(cleanup, cleanup)

    // 达到最大并发数时,等待其中一个完成
    if (executing.size >= maxConcurrency) {
      await Promise.race(executing)
    }
  }

  return Promise.all(results)
}

// 使用
const urls = Array.from({ length: 10 }, (_, i) => `/api/data/${i}`)
const tasks = urls.map(url => () => fetch(url).then(r => r.json()))

const results = await concurrentPool(tasks, 3)
// 最多同时 3 个请求在执行

// ========== 方法2:基于类的并发调度器 ==========
class TaskScheduler {
  constructor(maxConcurrency) {
    this.maxConcurrency = maxConcurrency
    this.running = 0
    this.queue = []
  }

  add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject })
      this._run()
    })
  }

  _run() {
    while (this.running < this.maxConcurrency && this.queue.length > 0) {
      const { task, resolve, reject } = this.queue.shift()
      this.running++
      Promise.resolve(task())
        .then(resolve, reject)
        .finally(() => {
          this.running--
          this._run()
        })
    }
  }
}

// 使用
const scheduler = new TaskScheduler(2)

function createTask(id, time) {
  return () => new Promise(resolve => {
    console.log(`任务 ${id} 开始`)
    setTimeout(() => {
      console.log(`任务 ${id} 完成`)
      resolve(id)
    }, time)
  })
}

scheduler.add(createTask(1, 1000))
scheduler.add(createTask(2, 500))
scheduler.add(createTask(3, 300))
scheduler.add(createTask(4, 400))
// 同时运行任务 1 和 2
// 任务 2 完成后开始任务 3
// 任务 3 完成后开始任务 4

// ========== 方法3:简洁实现(面试常用)==========
async function limitConcurrency(tasks, limit) {
  const results = new Array(tasks.length)
  let index = 0

  async function worker() {
    while (index < tasks.length) {
      const i = index++
      results[i] = await tasks[i]()
    }
  }

  // 创建 limit 个 worker 并行执行
  await Promise.all(Array.from({ length: limit }, worker))
  return results
}

// ========== 实用场景 ==========
// 1. 批量上传文件(限制同时上传数量)
// 2. 爬虫请求频率控制
// 3. 批量数据处理(避免内存溢出)
// 4. API 接口限流

💡 面试加分点: 浏览器对同一域名有并发连接限制(Chrome 是 6 个),但 JS 层面的 Promise 并发不受此限制(只是 HTTP 请求排队)。Promise.all 会同时启动所有 Promise,而并发池可以控制实际执行的数量。


38. 手写 JSON.stringify 和 JSON.parse?

javascript 复制代码
// ========== 手写 JSON.stringify ==========
function jsonStringify(value) {
  // null
  if (value === null) return 'null'

  // undefined、函数、Symbol → 返回 undefined(顶层)或被忽略(对象属性)
  if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
    return undefined
  }

  // 布尔值
  if (typeof value === 'boolean') return value.toString()

  // 数字
  if (typeof value === 'number') {
    if (Number.isNaN(value) || !Number.isFinite(value)) return 'null'
    return value.toString()
  }

  // 字符串
  if (typeof value === 'string') {
    return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"`
  }

  // BigInt → 报错
  if (typeof value === 'bigint') {
    throw new TypeError('BigInt value can\'t be serialized in JSON')
  }

  // 日期
  if (value instanceof Date) {
    return `"${value.toISOString()}"`
  }

  // 正则 → 空对象
  if (value instanceof RegExp) return '{}'

  // 数组
  if (Array.isArray(value)) {
    const items = value.map(item => {
      const result = jsonStringify(item)
      return result === undefined ? 'null' : result
    })
    return `[${items.join(',')}]`
  }

  // 对象
  if (typeof value === 'object') {
    // 如果有 toJSON 方法,使用它
    if (typeof value.toJSON === 'function') {
      return jsonStringify(value.toJSON())
    }
    const pairs = []
    for (const key of Object.keys(value)) {
      const result = jsonStringify(value[key])
      if (result !== undefined) {
        pairs.push(`${jsonStringify(key)}:${result}`)
      }
    }
    return `{${pairs.join(',')}}`
  }
}

// 测试
jsonStringify({ name: '张三', age: 25, fn: () => {} })
// '{"name":"张三","age":25}'(函数被忽略)

jsonStringify([1, undefined, null, '4'])
// '[1,null,null,"4"]'(undefined 变为 null)

// ========== 手写简易 JSON.parse ==========
// 方法1:使用 eval(不推荐,有安全风险)
function jsonParse1(str) {
  return eval(`(${str})`)
}

// 方法2:使用 Function 构造函数(稍安全)
function jsonParse2(str) {
  return new Function(`return ${str}`)()
}

// 测试
jsonParse2('{"name":"张三","age":25}')
// { name: '张三', age: 25 }

💡 面试加分点: JSON.stringify 会忽略 undefinedfunctionSymbol 类型的属性。对于循环引用会报错。toJSON() 方法可以自定义序列化行为------Date 对象就是通过 toJSON 返回 ISO 字符串的。实际手写 JSON.parse 非常复杂(需要词法分析器),面试中说明思路即可。


39. 手写深比较(isEqual)?

javascript 复制代码
// ========== 深比较:递归比较两个值是否相等 ==========
function isEqual(a, b) {
  // 1. 严格相等(处理基本类型、引用相同的情况)
  if (a === b) return true

  // 2. 处理 NaN(NaN !== NaN,但我们认为它们相等)
  if (Number.isNaN(a) && Number.isNaN(b)) return true

  // 3. 如果不是对象类型,且不严格相等,则不等
  if (typeof a !== 'object' || typeof b !== 'object') return false
  if (a === null || b === null) return false

  // 4. 类型必须一致
  const classA = Object.prototype.toString.call(a)
  const classB = Object.prototype.toString.call(b)
  if (classA !== classB) return false

  // 5. Date 对象
  if (a instanceof Date) return a.getTime() === b.getTime()

  // 6. RegExp 对象
  if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags

  // 7. Map
  if (a instanceof Map) {
    if (a.size !== b.size) return false
    for (const [key, val] of a) {
      if (!b.has(key) || !isEqual(val, b.get(key))) return false
    }
    return true
  }

  // 8. Set
  if (a instanceof Set) {
    if (a.size !== b.size) return false
    for (const val of a) {
      if (!b.has(val)) return false
    }
    return true
  }

  // 9. 数组和普通对象
  const keysA = Object.keys(a)
  const keysB = Object.keys(b)
  if (keysA.length !== keysB.length) return false

  for (const key of keysA) {
    if (!Object.hasOwn(b, key) || !isEqual(a[key], b[key])) {
      return false
    }
  }

  return true
}

// ========== 测试 ==========
// 基本类型
isEqual(1, 1)                     // true
isEqual('hello', 'hello')         // true
isEqual(NaN, NaN)                 // true

// 数组
isEqual([1, 2, 3], [1, 2, 3])    // true
isEqual([1, 2], [1, 2, 3])       // false

// 嵌套对象
isEqual(
  { a: 1, b: { c: 2, d: [3, 4] } },
  { a: 1, b: { c: 2, d: [3, 4] } }
)  // true

isEqual(
  { a: 1, b: { c: 2 } },
  { a: 1, b: { c: 3 } }
)  // false

// 特殊对象
isEqual(new Date('2026-01-01'), new Date('2026-01-01'))  // true
isEqual(/abc/gi, /abc/gi)                                // true
isEqual(new Map([['a', 1]]), new Map([['a', 1]]))        // true
isEqual(new Set([1, 2, 3]), new Set([1, 2, 3]))          // true

// ========== 浅比较 vs 深比较 ==========
// 浅比较:只比较第一层(React.memo、PureComponent 使用)
function shallowEqual(a, b) {
  if (Object.is(a, b)) return true
  if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) return false
  const keysA = Object.keys(a)
  if (keysA.length !== Object.keys(b).length) return false
  return keysA.every(key => Object.is(a[key], b[key]))
}

💡 面试加分点: 深比较在大型对象上性能较差,React 中推荐浅比较 + 不可变数据 的方案。Lodash 的 _.isEqual 还处理了 Buffer、ArrayBuffer、Error 等更多类型。面试中需要注意 NaN 和循环引用的处理。


40. Generator 生成器和异步迭代器是什么?

javascript 复制代码
// ========== Generator 基础 ==========
// Generator 函数使用 function* 声明,可以暂停执行并通过 yield 产出值

function* counter(start = 0) {
  let count = start
  while (true) {
    const reset = yield count++
    if (reset) count = start  // 可以通过 next() 传值给 yield
  }
}

const gen = counter(1)
gen.next()        // { value: 1, done: false }
gen.next()        // { value: 2, done: false }
gen.next()        // { value: 3, done: false }
gen.next(true)    // { value: 1, done: false }(重置了)

// ========== Generator 作为可迭代对象 ==========
function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i
  }
}

for (const n of range(1, 10, 2)) {
  console.log(n)  // 1, 3, 5, 7, 9
}
[...range(1, 5)]  // [1, 2, 3, 4, 5]

// ========== yield* 委托(遍历其他可迭代对象)==========
function* flatten(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* flatten(item)  // 委托给另一个 Generator
    } else {
      yield item
    }
  }
}
[...flatten([1, [2, [3, [4]]]])]  // [1, 2, 3, 4]

// ========== Generator 实现异步控制流(co 模式)==========
// 这就是 async/await 的前身
function co(generatorFn) {
  return function(...args) {
    const gen = generatorFn.apply(this, args)
    return new Promise((resolve, reject) => {
      function step(key, value) {
        try {
          const { value: result, done } = gen[key](value)
          if (done) return resolve(result)
          Promise.resolve(result).then(
            val => step('next', val),
            err => step('throw', err)
          )
        } catch (err) {
          reject(err)
        }
      }
      step('next')
    })
  }
}

// 使用 co + Generator 实现类似 async/await 的效果
const fetchData = co(function*() {
  const user = yield fetch('/api/user').then(r => r.json())
  const posts = yield fetch(`/api/posts/${user.id}`).then(r => r.json())
  return { user, posts }
})
fetchData().then(console.log)

// ========== 异步迭代器(Async Iterator)==========
// 使用 for await...of 遍历异步数据流

async function* asyncRange(start, end) {
  for (let i = start; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100))
    yield i
  }
}

// 消费异步迭代器
for await (const num of asyncRange(1, 5)) {
  console.log(num)  // 每 100ms 输出一个:1, 2, 3, 4, 5
}

// ========== 实用:分页获取所有数据 ==========
async function* fetchAllPages(url) {
  let page = 1
  let hasMore = true
  while (hasMore) {
    const response = await fetch(`${url}?page=${page}`)
    const data = await response.json()
    yield* data.items  // 逐个 yield 每一项
    hasMore = data.hasMore
    page++
  }
}

// 使用
for await (const item of fetchAllPages('/api/users')) {
  console.log(item)
  // 自动翻页,逐条处理,内存友好
}

// ========== 实用:流式读取响应体 ==========
async function* readStream(response) {
  const reader = response.body.getReader()
  const decoder = new TextDecoder()
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    yield decoder.decode(value, { stream: true })
  }
}

const response = await fetch('/api/stream')
for await (const chunk of readStream(response)) {
  console.log('收到数据块:', chunk)
}

💡 面试加分点: async/await 的底层实现就是 Generator + 自动执行器(co)。异步迭代器 for await...of 在 Node.js 中处理文件流、数据库查询结果非常常见。yield* 可以将控制权委托给另一个 Generator 或可迭代对象。


41. 手写 Promise 限流与重试?

javascript 复制代码
// ========== 请求重试(带指数退避)==========
async function retry(fn, options = {}) {
  const { maxRetries = 3, baseDelay = 1000, maxDelay = 30000 } = options
  let lastError

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn()
    } catch (err) {
      lastError = err
      if (attempt === maxRetries) break
      // 指数退避 + 随机抖动
      const delay = Math.min(
        baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
        maxDelay
      )
      console.log(`第 ${attempt + 1} 次重试,等待 ${delay.toFixed(0)}ms...`)
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }

  throw lastError
}

// 使用
const data = await retry(
  () => fetch('/api/unstable').then(r => {
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    return r.json()
  }),
  { maxRetries: 3, baseDelay: 1000 }
)

// ========== 批量请求(并发限流 + 重试)==========
async function batchRequest(urls, { concurrency = 5, retries = 2 } = {}) {
  const results = new Array(urls.length)
  const errors = []
  let index = 0

  async function worker() {
    while (index < urls.length) {
      const i = index++
      try {
        results[i] = await retry(
          () => fetch(urls[i]).then(r => r.json()),
          { maxRetries: retries }
        )
      } catch (err) {
        errors.push({ index: i, url: urls[i], error: err })
        results[i] = null
      }
    }
  }

  await Promise.all(
    Array.from({ length: Math.min(concurrency, urls.length) }, worker)
  )

  return { results, errors }
}

// 使用
const urls = Array.from({ length: 100 }, (_, i) => `/api/item/${i}`)
const { results, errors } = await batchRequest(urls, {
  concurrency: 5,
  retries: 2,
})
console.log(`成功: ${results.filter(Boolean).length}, 失败: ${errors.length}`)

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

// 或用 reduce 实现
function serialReduce(tasks) {
  return tasks.reduce(
    (chain, task) => chain.then(results =>
      task().then(result => [...results, result])
    ),
    Promise.resolve([])
  )
}

// ========== Promise 超时包装 ==========
function withTimeout(promise, ms, message = '操作超时') {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(message)), ms)
  )
  return Promise.race([promise, timeout])
}

// 使用
const result = await withTimeout(
  fetch('/api/slow-endpoint'),
  5000,
  '请求超时,请稍后重试'
)

// ========== Promise 缓存(避免重复请求)==========
function createCachedRequest(fn, ttl = 60000) {
  const cache = new Map()

  return async function(...args) {
    const key = JSON.stringify(args)
    const cached = cache.get(key)

    if (cached && Date.now() - cached.timestamp < ttl) {
      return cached.data  // 缓存未过期,直接返回
    }

    // 避免并发时重复请求(缓存 Promise 而非结果)
    if (cached?.pending) return cached.pending

    const pending = fn(...args)
    cache.set(key, { pending, timestamp: Date.now() })

    try {
      const data = await pending
      cache.set(key, { data, timestamp: Date.now() })
      return data
    } catch (err) {
      cache.delete(key)
      throw err
    }
  }
}

const cachedFetch = createCachedRequest(
  (id) => fetch(`/api/users/${id}`).then(r => r.json()),
  30000  // 30 秒缓存
)

await cachedFetch(1)  // 发起请求
await cachedFetch(1)  // 使用缓存(30秒内)

💡 面试加分点: 指数退避(Exponential Backoff)是重试的最佳实践------避免短时间内大量重试导致服务端雪崩。加上随机抖动(Jitter)可以防止多个客户端同时重试。缓存 Promise 而非结果可以避免竞态条件(多个相同请求同时发起时只执行一次)。

相关推荐
daols884 分钟前
vue 实现基于 vxe-table 构建多维度产品对比表
前端·javascript·vue.js
前端 贾公子7 分钟前
第08章:中间件(5)
服务器·前端·javascript
黄敬峰21 分钟前
一文讲透 JWT 登录鉴权:token 的「颁发 → 存储 → 携带 → 校验」完整闭环
面试
tech_zjf1 小时前
当 AI 把 Next.js Route 越写越快:我为什么做了 next-route-kit
前端·后端
常宇佳1 小时前
vue3 @代指src路径设置
前端·typescript·vue
LayZhangStrive1 小时前
融360 一面
java·面试·后端开发
砚凝霜1 小时前
软考网络工程师|案例分析:Eth‑Trunk 链路聚合、iStack 堆叠、CSS 集群核心考点总结
前端·css·网络
二级小助手1 小时前
二级Web前端选择题高频真题20道与考场避坑笔记
javascript·css3·html5·web前端·计算机二级·二级web·web真题
珐恩AI-人工智能2 小时前
大模型意图召回偏差分析:GEO如何解决“有收录却不触发问答曝光”的难题
大数据·前端·人工智能·html·流量运营·geo优化
城管不管2 小时前
重生——第十次面试之开源中国一面挂
java·linux·开发语言·算法·面试·职场和发展·开源