文章

手写Promise.all/深拷贝/EventEmitter深度解析

Promise.all 克制并发、深拷贝驯服引用、EventEmitter 打通事件通信——三道手写题对应异步控制、数据操作、事件通信三大能力。 边界处理比主流程更能体现功力深浅。

手写Promise.all/深拷贝/EventEmitter深度解析

一句话概括

Promise.all 克制并发、深拷贝驯服引用、EventEmitter 打通组件——三道手写题分别对应”异步控制、数据操作、事件通信”三大核心能力,其边界处理比主流程更能体现功力深浅。

核心知识点

1. Promise.all:并发 + 保序 + 快速失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    const arr = Array.from(promises);
    if (arr.length === 0) return resolve([]);

    const results = new Array(arr.length);
    let done = 0, failed = false;

    arr.forEach((p, i) => {
      Promise.resolve(p).then(
        value => {
          if (failed) return;         // 已失败,忽略后继
          results[i] = value;         // 按索引填入,保证顺序
          done++;
          if (done === arr.length) resolve(results);
        },
        reason => {
          if (!failed) { failed = true; reject(reason); }  // 快速失败
        }
      );
    });
  });
}

三个关键点:索引保序(比 Promise 完成顺序保证结果顺序)、快速失败(有一个 reject 就立刻 reject)、非 Promise 自动包装(Promise.resolve(p) 处理普通值)。注意快速失败不会”取消”其他 Promise——它们仍在执行。

2. 深拷贝:循环引用 + 特殊对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function deepClone(obj, cache = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;

  // 循环引用:原对象 → 已拷贝的副本
  if (cache.has(obj)) return cache.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 m = new Map(); cache.set(obj, m);
    obj.forEach((v, k) => m.set(deepClone(k, cache), deepClone(v, cache)));
    return m;
  }
  if (obj instanceof Set) {
    const s = new Set(); cache.set(obj, s);
    obj.forEach(v => s.add(deepClone(v, cache)));
    return s;
  }

  // 普通对象/数组
  const cloned = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
  cache.set(obj, cloned);

  for (const key of [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)]) {
    cloned[key] = deepClone(obj[key], cache);
  }
  return cloned;
}

这道题真正的难点不在递归,在循环引用(WeakMap 存映射)和特殊类型(Date/RegExp/Map/Set/Symbol key)。structuredClone() 是浏览器原生方案,但缺省不支持函数和 DOM 节点。

3. EventEmitter:on / once / off / emit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class EventEmitter {
  constructor() {
    this._events = new Map();     // eventName → [listener, ...]
    this._maxListeners = 10;
  }

  on(event, listener) {
    if (!this._events.has(event)) this._events.set(event, []);
    this._events.get(event).push(listener);
    return this;
  }

  once(event, listener) {
    const wrapper = (...args) => {
      this.off(event, wrapper);
      listener.apply(this, args);
    };
    wrapper._original = listener;  // 保留原始引用,用于 off 匹配
    return this.on(event, wrapper);
  }

  off(event, listener) {
    if (!this._events.has(event)) return this;
    this._events.set(event,
      this._events.get(event).filter(l => l !== listener && l._original !== listener)
    );
    return this;
  }

  emit(event, ...args) {
    if (!this._events.has(event)) return false;
    // 复制一份再遍历,防止 emit 过程中 off 导致迭代异常
    [...this._events.get(event)].forEach(fn => fn.apply(this, args));
    return true;
  }
}

once 的精髓在装饰器模式——包装函数执行后自删除。off 需要同时检查 listener 和 wrapper._original 才能正确卸载 once 注册的监听器。emit 时复制数组,避免回调中 off 修改原数组导致跳项。

4. Promise.allSettled:不快速失败

1
2
3
4
5
6
7
8
9
10
function promiseAllSettled(promises) {
  return Promise.all(
    Array.from(promises).map(p =>
      Promise.resolve(p).then(
        value => ({ status: 'fulfilled', value }),
        reason => ({ status: 'rejected', reason })
      )
    )
  );
}

与 Promise.all 的最大区别:allSettled 等所有 Promise 都结束(不管成功失败),返回每个的状态。适合”尽量多拿数据”的场景(如批量 API 调用,失败几个 UI 照样展示)。

5. 深拷贝迭代版(防爆栈)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function deepCloneIterative(obj) {
  const root = Array.isArray(obj) ? [] : {};
  const stack = [[obj, root]];
  const cache = new WeakMap([[obj, root]]);

  while (stack.length) {
    const [source, target] = stack.pop();
    for (const key of [...Object.keys(source), ...Object.getOwnPropertySymbols(source)]) {
      const val = source[key];
      if (val && typeof val === 'object') {
        if (cache.has(val)) { target[key] = cache.get(val); }
        else {
          const copy = Array.isArray(val) ? [] : {};
          cache.set(val, copy);
          target[key] = copy;
          stack.push([val, copy]);
        }
      } else {
        target[key] = val;
      }
    }
  }
  return root;
}

递归版遇到 10000 层嵌套会爆栈。迭代版用显式栈模拟递归,多少层都能扛。

其实你每天都在用

  • Promise.all:页面加载同时请求用户信息 + 配置 + 权限,三个接口并行,等都拿到再渲染
  • 深拷贝:Redux reducer 里 { ...state, items: newItems } 不够时,需要深拷贝嵌套对象——如果直接改原对象 Redux 认为没变化不触发更新
  • EventEmitter:Vue 的 $emit / $on、Node.js 的 EventEmitter、WebSocket 消息分发——本质上都是发布订阅
  • Promise.allSettled:提交表单时同时上传多张图片,部分失败仍展示上传成功的

常见误解(FAQ)

  • ❌ 误区:「Promise.all 会取消已启动的 Promise」 不会。JavaScript 的 Promise 一旦启动无法取消(除非配合 AbortController)。reject 只是让 Promise.all 返回的 Promise 变为 rejected,其他请求仍在后台执行。

  • ❌ 误区:「JSON.parse(JSON.stringify(obj)) 是万能的深拷贝」 不是。Date 变字符串、undefined 和函数丢失、RegExp 变空对象、循环引用直接报错。只在纯 JSON 数据(无循环、无特殊类型)时可用。

  • ❌ 误区:「EventEmitter 的 emit 是异步的」 不是。emit 是同步遍历执行监听器。如果监听器里是 async 函数,Promise 会被创建但不被 await。想异步执行用 setImmediate 或 process.nextTick 包一层。

  • ❌ 误区:「once 就是 on + 计数器」 不是。once 的关键是包装函数自删除(执行后 off(event, wrapper)),还必须在 wrapper._original 存原始引用——否则用户调 off(event, originalFn) 找不到目标。

一句话总结

手写题从来不是考”会不会写”,而是考”懂不懂边界”——Promise.all 的快速失败和索引保序、深拷贝的循环引用和特殊类型、EventEmitter 的 once 自删除,这些边界条件才是面试官真正想听的。

本文由作者按照 CC BY 4.0 进行授权