前端面试必备手写题(下)
系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)(本文)
文章目录
前言
上一篇整理了深拷贝、防抖节流与发布订阅;本篇延续「能写能讲」路线,聚焦 Promise 手写、async/await 与 Generator 的关系、以及性能向的 LRU 缓存 与 虚拟列表。其中 Promise 要能说清三态、链式 then、错误穿透与 Promise.all 的失败语义;LRU 与虚拟列表则常出现在大厂手写或项目优化题中。以下按「Promise → async/Generator → LRU → 虚拟列表」展开,并与第 08 篇宏任务/微任务衔接阅读。
一、Promise 基础与三态
1.1 定义
- Promise 表示异步操作的最终结果,状态仅有三种:
pending(待定)、fulfilled(已成功)、rejected(已失败)。 - 状态只能从
pending→fulfilled/rejected,一旦定型(settled)不可再变。 .then(onFulfilled, onRejected)返回新的 Promise,从而支持链式调用。- 值穿透:若
onFulfilled不是函数,前一个 Promise 的 resolved 值会原样传递到下一个then(onRejected非函数时 rejected 值同理穿透)。
1.2 与事件循环的关系(复习)
new Promise(executor)里的executor同步执行。.then/.catch/.finally的回调进入微任务队列(见第 08 篇)。- 面试常考:「Promise 本身是同步的」指 executor 同步;异步感来自 then 回调延后到微任务。
console.log("1");
new Promise((resolve) => {
console.log("2");
resolve();
}).then(() => console.log("3"));
console.log("4");
// 1 → 2 → 4 → 3
二、手写 Promise(核心版)
面试不要求与规范逐行一致,但需覆盖:then 链式、值穿透、返回 thenable 时等待、错误捕获、Promise.all。
2.1 状态与构造函数
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class MyPromise {
#state = PENDING;
#value = undefined;
#callbacks = [];
constructor(executor) {
const resolve = (value) => {
if (this.#state !== PENDING) return;
this.#state = FULFILLED;
this.#value = value;
this.#runCallbacks();
};
const reject = (reason) => {
if (this.#state !== PENDING) return;
this.#state = REJECTED;
this.#value = reason;
this.#runCallbacks();
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
#runCallbacks() {
queueMicrotask(() => {
this.#callbacks.forEach(({ onFulfilled, onRejected, resolve, reject }) => {
if (this.#state === FULFILLED) {
this.#handle(onFulfilled, resolve, reject);
} else {
this.#handle(onRejected, resolve, reject);
}
});
this.#callbacks = [];
});
}
#handle(fn, resolve, reject) {
try {
const x =
typeof fn === "function" ? fn(this.#value) : this.#value;
this.#resolvePromise(x, resolve, reject);
} catch (e) {
reject(e);
}
}
#resolvePromise(x, resolve, reject) {
if (x instanceof MyPromise) {
x.then(resolve, reject);
return;
}
if (x && (typeof x === "object" || typeof x === "function")) {
let called = false;
try {
const then = x.then;
if (typeof then === "function") {
then.call(
x,
(y) => {
if (called) return;
called = true;
this.#resolvePromise(y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
return;
}
} catch (e) {
if (called) return;
reject(e);
return;
}
}
resolve(x);
}
then(onFulfilled, onRejected) {
const realOnFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (v) => v;
const realOnRejected =
typeof onRejected === "function" ? onRejected : (e) => {
throw e;
};
return new MyPromise((resolve, reject) => {
const run = () => {
this.#handle(
this.#state === FULFILLED ? realOnFulfilled : realOnRejected,
resolve,
reject
);
};
if (this.#state === PENDING) {
this.#callbacks.push({
onFulfilled: realOnFulfilled,
onRejected: realOnRejected,
resolve,
reject,
});
} else {
run();
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(onFinally) {
return this.then(
(v) => MyPromise.resolve(onFinally?.()).then(() => v),
(e) =>
MyPromise.resolve(onFinally?.()).then(() => {
throw e;
})
);
}
static resolve(val) {
if (val instanceof MyPromise) return val;
return new MyPromise((r) => r(val));
}
static reject(reason) {
return new MyPromise((_, rej) => rej(reason));
}
}
要点:
#resolvePromise:处理then回调返回值;若为 Promise 或 thenable,需等其状态再 resolve/reject 外层(模拟规范中的 Promise Resolution Procedure 简化版)。- 非函数回调:
then里用恒等 / 抛错实现值穿透与 rejected 穿透。 - 微任务用
queueMicrotask;不支持的环境可换setTimeout(fn, 0)近似(顺序与真实微任务略有差异,面试可说明)。
2.2 链式调用示例
MyPromise.resolve(1)
.then((x) => x + 1)
.then((x) => x * 3)
.then(console.log); // 6
MyPromise.resolve(1)
.then(() => {
throw new Error("fail");
})
.catch((e) => console.log(e.message)); // fail
2.3 手写 Promise.all
MyPromise.all = function (iterable) {
return new MyPromise((resolve, reject) => {
const arr = [...iterable];
if (arr.length === 0) return resolve([]);
const result = [];
let count = 0;
arr.forEach((p, i) => {
MyPromise.resolve(p).then(
(val) => {
result[i] = val;
if (++count === arr.length) resolve(result);
},
reject
);
});
});
};
面试聚焦:
- 一个 rejected 即整体 reject(快速失败),已成功的结果不会取消,但调用方通常已进入 catch。
- 传入非 Promise 会经
Promise.resolve包装。 - 空数组
Promise.all([])立即resolve([])。 - 与
Promise.allSettled对比:后者等全部结束,返回{ status, value/reason }[],不因单个失败而中断。
// 原生对比
Promise.all([
Promise.resolve(1),
Promise.reject(new Error("x")),
]).catch((e) => console.log(e.message)); // 'x' — 整体失败
Promise.allSettled([
Promise.resolve(1),
Promise.reject(new Error("x")),
]).then(console.log);
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error: x }
// ]
2.4 错误穿透
catch本质是then(null, onRejected)。- 链中
throw或返回 rejected Promise 会使后续 fulfilled 回调跳过,直到遇到 rejected 处理。 catch放在链中间只能捕获它之前未处理的 rejection;之后的then会拿到 catch 的返回值(或继续抛错)。
Promise.resolve()
.then(() => {
throw new Error("a");
})
.then(() => console.log("不会执行"))
.catch((e) => console.log(e.message)) // a
.then(() => console.log("catch 之后仍可 then")); // 会执行
三、async/await 与 Generator
3.1 async/await 是什么
async函数一定返回 Promise;函数内return v等价于Promise.resolve(v),throw e等价于Promise.reject(e)。await暂停当前 async 函数执行,等待 Promise 定型;resolved 得到值,rejected 相当于throw。await不阻塞主线程:等待期间事件循环仍可处理其他任务;async 函数在 await 之后的代码相当于then回调(微任务)。
const delay = (ms, val) =>
new Promise((r) => setTimeout(() => r(val), ms));
const fetchUser = async (id) => {
const profile = await delay(100, { id, name: "Tom" });
const posts = await delay(200, ["post1", "post2"]);
return { ...profile, posts };
};
fetchUser(1).then(console.log);
3.2 易混点
| 写法 | 行为 |
|---|---|
for 里连续 await |
串行,总耗时约为各次之和 |
await Promise.all([...]) |
并行,总耗时约为最慢的一次 |
async 里未 await 的 Promise |
不会自动等待,可能产生未捕获 rejection |
// 串行
for (const id of [1, 2, 3]) await delay(100, id);
// 并行
await Promise.all([1, 2, 3].map((id) => delay(100, id)));
3.3 Generator 与自动执行器(理解 async 本质)
Generator(function*)通过 yield 暂停、next() 恢复,返回 { value, done }。早期用 co 类库把 Generator 当成「同步写法写异步」的载体;async/await 可看作该模式的内置语法糖。
function* gen() {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
return a + b;
}
function run(generator) {
const g = generator();
function step(val) {
const result = g.next(val);
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value).then(
(data) => step(data),
(err) => g.throw(err)
);
}
return step();
}
run(gen).then(console.log); // 3(需调整 gen 内 yield 赋值逻辑时按题意修改)
更直观的「yield Promise」示例:
function* fetchData() {
const user = yield delay(100, { id: 1 });
const list = yield delay(100, ["a", "b"]);
return { user, list };
}
run(fetchData).then(console.log);
// { user: { id: 1 }, list: ['a', 'b'] }
面试一句话:async/await = Generator + 自动执行器 + Promise;不必手写 co,但要能解释「await 背后是把后续代码放进 then」。
四、LRU 缓存(Least Recently Used)
4.1 含义
LRU:容量满时淘汰最久未使用的项。要求 get / put 均为 O(1)(面试常用 哈希表 + 双向链表;工程与手写简答可用 Map 利用「插入顺序」)。
4.2 基于 Map 的实现
ES2015 的 Map 按插入顺序迭代;get 时先删再 set 可把项移到「最新」;满容时删 第一个 key(最久未用)。
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val);
return val;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest);
}
this.cache.set(key, value);
}
}
const lru = new LRUCache(2);
lru.put(1, 1);
lru.put(2, 2);
lru.get(1); // 1
lru.put(3, 3); // 淘汰 key 2
lru.get(2); // -1
复杂度:get、put 平均 O(1);空间 O(capacity)。
口述:Map 维护 key → value;每次访问把 key 挪到「尾部」;超容删「头部」最旧 key。
五、虚拟列表(Virtual List)
5.1 为什么需要
长列表若全量渲染 DOM(如 1 万行表格),会导致节点过多、滚动卡顿。思路:只渲染可视区域 + 缓冲的项,用占位高度撑开滚动条,滚动时复用节点并更新偏移。
5.2 核心公式
设每项固定高度 itemH,容器可视高度 viewH,滚动距离 scrollTop:
- 起始下标:
start = Math.floor(scrollTop / itemH) - 结束下标:
end = start + Math.ceil(viewH / itemH) + buffer(buffer 常取 1~2 行,减少白屏) - 总高度:
totalH = items.length * itemH - 每项定位:
top = i * itemH(绝对定位或transform: translateY)
5.3 简易实现(固定行高)
const createVirtualList = (container, items, itemH = 40, renderFn) => {
const viewH = container.clientHeight;
const totalH = items.length * itemH;
const padEl = document.createElement("div");
padEl.style.height = `${totalH}px`;
padEl.style.position = "relative";
container.appendChild(padEl);
const render = () => {
const start = Math.floor(container.scrollTop / itemH);
const end = Math.min(
start + Math.ceil(viewH / itemH) + 1,
items.length
);
padEl.innerHTML = "";
for (let i = start; i < end; i++) {
const el = renderFn(items[i], i);
el.style.position = "absolute";
el.style.top = `${i * itemH}px`;
el.style.height = `${itemH}px`;
padEl.appendChild(el);
}
};
container.addEventListener("scroll", render);
render();
};
工程增强(口述即可):动态行高需维护前缀和数组求每项 top;React 可用 react-window / react-virtualized;滚动用 transform 有时更利于合成层。
复杂度:渲染节点数 O(可视行数),与总条数 N 解耦;滚动时重算 O(1)(固定行高)。
六、速查与面试对照
| 主题 | 必记 |
|---|---|
| Promise 三态 | pending → fulfilled / rejected,不可逆 |
| executor / then | executor 同步;then 回调微任务 |
| 链式 then | 返回新 Promise;返回值被 resolve 包装 |
| Promise.all | 全 fulfilled 才 resolve;任一 reject 即 reject |
| allSettled | 全部结束,单个失败不影响整体 settle |
| async/await | 语法糖;并行用 Promise.all |
| LRU | Map 删头插尾维护顺序;O(1) get/put |
| 虚拟列表 | 只渲染可视区;start/end + 总高度占位 |
七、易混淆点归纳
- 「Promise 同步」:指 executor 同步,不是 then 同步。
Promise.allvsrace:all 等全部成功;race 谁先 settle 用谁(常用于超时)。catch返回值:若返回普通值,后续then走 fulfilled 链。- 循环 await:
for+await串行;勿误以为 async 函数会「自动并行」。 - LRU Map 法:面试够用;追问 O(1) 原理时再画双向链表 + 哈希表。
- 虚拟列表:固定行高实现简单;动态行高需测量 + 缓存高度。
八、思考与练习
1. 下面输出顺序是什么?
console.log("A");
Promise.resolve().then(() => console.log("B"));
console.log("C");
解析:A → C → B。then 回调为微任务,在本轮同步代码之后执行。
2. Promise.all([p1, p2, p3]) 中 p2 先 reject,p1、p3 仍会执行完吗?
解析:会(各 Promise 自身逻辑仍会走完),但 all 返回的 Promise 已 reject,结果通常被 catch 消费;与 allSettled「全部收尾」不同。
3. async function f() { return 1; } 的返回值类型是什么?
解析:Promise,等价 Promise.resolve(1)。
4. LRU 容量 2,操作 put(1,1) put(2,2) put(1,1) put(3,3) get(2),get(2) 返回什么?
解析:-1。put(1,1) 再次访问后 1 变为最新;put(3,3) 满容淘汰最久未用的 2。
5. 1 万条数据列表卡顿,虚拟列表主要优化的是什么?
解析:DOM 节点数量与重排重绘范围;将渲染量从 O(N) 降到 O(可视行数)。
总结
- Promise:三态不可逆;手写
then链、#resolvePromise、catch、Promise.all;弄清 all 快速失败 与 allSettled 区别。 - async/await:Promise 语法糖;并行
Promise.all、串行 for-await;Generator + 自动执行器帮助理解原理。 - LRU:Map 维护顺序,满容删最旧;O(1) get/put。
- 虚拟列表:按
scrollTop算 start/end,占位总高度,只渲染可视区。
续篇将展开 Promise 与 async/await 专题(race、allSettled、any、超时、重试、并发池等),并进入 数据结构基础 阶段。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)