手写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 自删除,这些边界条件才是面试官真正想听的。