手写三大核心:Promise、Event Bus、深拷贝

手写三大核心:Promise、Event Bus、深拷贝

这三道题是面试中的"照妖镜",能写出来才算真正理解。我给你逐行讲解。


一、手写深拷贝(最常考,也最容易暴露问题)

基础版(只考虑对象和数组)

javascript 复制代码
function deepClone(obj) {
  // 处理 null 和基本类型
  if (obj === null || typeof obj !== 'object') {
    return obj
  }
  
  // 处理数组和对象
  const clone = Array.isArray(obj) ? [] : {}
  
  for (let key in obj) {
    // 只拷贝自身属性,不拷贝原型链上的
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key])
    }
  }
  
  return clone
}

进阶版(解决循环引用 + 处理特殊类型)

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  // 处理 null 和 undefined
  if (obj === null || typeof obj !== 'object') return obj
  
  // 处理循环引用:如果已经拷贝过,直接返回
  if (hash.has(obj)) return hash.get(obj)
  
  // 处理 Date
  if (obj instanceof Date) return new Date(obj)
  
  // 处理 RegExp
  if (obj instanceof RegExp) return new RegExp(obj)
  
  // 处理函数(通常直接复用,不需要深拷贝)
  if (typeof obj === 'function') return obj
  
  // 创建新对象(保留原型链)
  const clone = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj))
  hash.set(obj, clone)
  
  // 拷贝 Symbol 类型的 key
  const symbols = Object.getOwnPropertySymbols(obj)
  for (let sym of symbols) {
    clone[sym] = deepClone(obj[sym], hash)
  }
  
  // 拷贝普通属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash)
    }
  }
  
  return clone
}

// 测试
const obj = { a: 1, b: { c: 2 } }
obj.self = obj  // 循环引用
const cloned = deepClone(obj)
console.log(cloned)  // 不会栈溢出

简化版(面试最优解)

javascript 复制代码
function deepClone(obj, cache = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj
  if (cache.has(obj)) return cache.get(obj)
  
  const clone = Array.isArray(obj) ? [] : {}
  cache.set(obj, clone)
  
  Reflect.ownKeys(obj).forEach(key => {
    clone[key] = deepClone(obj[key], cache)
  })
  
  return clone
}

关键点:

  • WeakMap 解决循环引用(不用 Map 是因为 WeakMap 的 key 是弱引用,不会阻止垃圾回收)
  • Reflect.ownKeys 可以拿到 Symbol 和不可枚举属性
  • 函数通常不需要深拷贝,直接用原函数

二、手写 Event Bus(发布订阅模式)

javascript 复制代码
class EventBus {
  constructor() {
    // 存储事件和对应的回调函数
    // 格式:{ eventName: [handler1, handler2, ...] }
    this.events = {}
  }
  
  // 订阅事件
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = []
    }
    this.events[eventName].push(callback)
    
    // 返回取消订阅的函数(方便 React useEffect 清理)
    return () => this.off(eventName, callback)
  }
  
  // 订阅一次(触发后自动移除)
  once(eventName, callback) {
    const onceWrapper = (...args) => {
      callback(...args)
      this.off(eventName, onceWrapper)
    }
    // 保存原始回调,以便在 off 时能正确移除
    onceWrapper.raw = callback
    this.on(eventName, onceWrapper)
  }
  
  // 触发事件
  emit(eventName, ...args) {
    const callbacks = this.events[eventName]
    if (!callbacks || callbacks.length === 0) return
    
    // 遍历执行(注意:防止在执行过程中数组被修改)
    callbacks.forEach(cb => {
      try {
        cb(...args)
      } catch (error) {
        console.error(`EventBus error in ${eventName}:`, error)
      }
    })
  }
  
  // 移除事件
  off(eventName, callback) {
    if (!callback) {
      // 如果没有传 callback,则移除整个事件
      delete this.events[eventName]
      return
    }
    
    const callbacks = this.events[eventName]
    if (!callbacks) return
    
    // 找到并移除
    const index = callbacks.findIndex(cb => cb === callback || cb.raw === callback)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
    
    // 如果该事件没有回调了,删除这个 key
    if (callbacks.length === 0) {
      delete this.events[eventName]
    }
  }
  
  // 清空所有事件
  clear() {
    this.events = {}
  }
}

// 使用示例
const bus = new EventBus()

const handler = (data) => console.log('收到:', data)
bus.on('click', handler)
bus.emit('click', { x: 100, y: 200 })  // 收到: { x: 100, y: 200 }

bus.once('once', () => console.log('只执行一次'))
bus.emit('once')  // 输出
bus.emit('once')  // 不输出

bus.off('click', handler)  // 移除

Vue 风格的事件总线:

javascript 复制代码
class VueEventBus {
  constructor() {
    this.events = new Map()
  }
  
  $on(event, handler) {
    if (!this.events.has(event)) {
      this.events.set(event, new Set())
    }
    this.events.get(event).add(handler)
  }
  
  $emit(event, ...args) {
    const handlers = this.events.get(event)
    if (handlers) {
      handlers.forEach(handler => handler(...args))
    }
  }
  
  $off(event, handler) {
    if (!handler) {
      this.events.delete(event)
      return
    }
    const handlers = this.events.get(event)
    if (handlers) {
      handlers.delete(handler)
      if (handlers.size === 0) this.events.delete(event)
    }
  }
  
  $once(event, handler) {
    const wrapper = (...args) => {
      handler(...args)
      this.$off(event, wrapper)
    }
    this.$on(event, wrapper)
  }
}

三、手写 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
    // 存储成功回调(支持多次 then 调用)
    this.onFulfilledCallbacks = []
    // 存储失败回调
    this.onRejectedCallbacks = []
    
    const resolve = (value) => {
      if (this.status === MyPromise.PENDING) {
        this.status = MyPromise.FULFILLED
        this.value = value
        // 执行所有成功回调
        this.onFulfilledCallbacks.forEach(fn => fn())
      }
    }
    
    const reject = (reason) => {
      if (this.status === MyPromise.PENDING) {
        this.status = MyPromise.REJECTED
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }
    
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }
  
  then(onFulfilled, onRejected) {
    // 参数可选,透传值
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
    
    // 返回新的 Promise,实现链式调用
    return new MyPromise((resolve, reject) => {
      const handleCallback = (callback, value, resolve, reject) => {
        // 微任务执行
        queueMicrotask(() => {
          try {
            const result = callback(value)
            // 如果返回的是 Promise,则等待其完成
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              resolve(result)
            }
          } catch (error) {
            reject(error)
          }
        })
      }
      
      if (this.status === MyPromise.FULFILLED) {
        handleCallback(onFulfilled, this.value, resolve, reject)
      } else if (this.status === MyPromise.REJECTED) {
        handleCallback(onRejected, this.reason, resolve, reject)
      } else {
        // pending 状态:存储回调
        this.onFulfilledCallbacks.push(() => {
          handleCallback(onFulfilled, this.value, resolve, reject)
        })
        this.onRejectedCallbacks.push(() => {
          handleCallback(onRejected, this.reason, resolve, reject)
        })
      }
    })
  }
  
  catch(onRejected) {
    return this.then(null, onRejected)
  }
  
  finally(callback) {
    return this.then(
      value => MyPromise.resolve(callback()).then(() => value),
      reason => MyPromise.resolve(callback()).then(() => { throw reason })
    )
  }
  
  // 静态方法
  static resolve(value) {
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => resolve(value))
  }
  
  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason))
  }
  
  static all(promises) {
    return new MyPromise((resolve, reject) => {
      if (!Array.isArray(promises)) {
        return reject(new TypeError('Argument must be an array'))
      }
      
      const results = []
      let completed = 0
      
      if (promises.length === 0) {
        resolve(results)
        return
      }
      
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          value => {
            results[index] = value
            completed++
            if (completed === promises.length) {
              resolve(results)
            }
          },
          reject
        )
      })
    })
  }
  
  static race(promises) {
    return new MyPromise((resolve, reject) => {
      if (!Array.isArray(promises)) {
        return reject(new TypeError('Argument must be an array'))
      }
      
      promises.forEach(promise => {
        MyPromise.resolve(promise).then(resolve, reject)
      })
    })
  }
  
  static allSettled(promises) {
    return new MyPromise((resolve) => {
      const results = []
      let completed = 0
      
      if (promises.length === 0) {
        resolve(results)
        return
      }
      
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          value => {
            results[index] = { status: 'fulfilled', value }
            completed++
            if (completed === promises.length) resolve(results)
          },
          reason => {
            results[index] = { status: 'rejected', reason }
            completed++
            if (completed === promises.length) resolve(results)
          }
        )
      })
    })
  }
  
  static any(promises) {
    return new MyPromise((resolve, reject) => {
      const errors = []
      let rejectedCount = 0
      
      if (promises.length === 0) {
        reject(new AggregateError([], 'All promises were rejected'))
      }
      
      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          resolve,
          error => {
            errors[index] = error
            rejectedCount++
            if (rejectedCount === promises.length) {
              reject(new AggregateError(errors, 'All promises were rejected'))
            }
          }
        )
      })
    })
  }
}

// 测试
const p = new MyPromise((resolve, reject) => {
  setTimeout(() => resolve('success'), 1000)
})
p.then(res => {
  console.log(res)  // 1秒后输出 'success'
  return 'next'
}).then(res => {
  console.log(res)  // 输出 'next'
})

四、面试时的策略

题目 必须实现的核心 加分项
深拷贝 递归 + 基础类型判断 循环引用(WeakMap) + Symbol + 原型链
Event Bus on/emit/off 基本逻辑 once + 取消订阅返回值 + 错误处理
Promise then 链式调用 + 状态管理 all/race + finally + 微任务

手写的核心价值不是代码完美,而是展示你对原理的理解。 把关键逻辑写清楚,比堆砌边界处理更重要。

相关推荐
星栈7 小时前
被Leptos弹窗逼疯后,我搞了一套零Props方案
前端·前端框架·全栈
不是山谷.:.7 小时前
Axios的【接口防抖 + 请求失败重试 + 弱网提示】三合一高阶版封装
前端·javascript·vue.js·笔记·elementui·typescript
超绝大帅哥7 小时前
babel降级|>, Object.groupBy
前端·javascript
23朵毒蘑菇7 小时前
前端自定义滚动条新星库出现了,看它亮还是不亮
前端·javascript
子兮曰7 小时前
GEO 生成式引擎优化完全指南:让你的内容成为 AI 的默认答案
前端·后端·seo
Cache技术分享8 小时前
412. Java 文件操作基础 - 用装饰者模式定制 BufferedReader 实现结构化文本读取
前端·后端
w_t_y_y8 小时前
VUE3(一)VUE3语法
前端·javascript·vue.js
builderwfy8 小时前
VUE子页面调用父页面实现方式
前端·javascript·vue.js