主题
附录 A1 · 30 道高频手写题完整解答
每道题统一按 题目 / 考察点 / 思路 / 完整代码(带注释)/ 复杂度分析 / 加分实现 展开。 所有代码可在浏览器/Node 中直接运行;配套
examples/目录提供独立文件。
Q1:手写 debounce(防抖)
题目:实现一个 debounce(fn, wait, immediate?),wait 毫秒内重复触发只执行最后一次。
考察点:闭包、this 绑定、setTimeout、参数透传、边界(立即执行 / 取消)
思路:
- 用闭包持有
timer变量 - 返回一个新函数,每次触发先
clearTimeout(timer),再setTimeout重新计时 - 用
fn.apply(this, args)透传this和参数 - 进阶:
immediate=true时第一次立即执行,wait 内的后续触发被忽略;提供cancel()
完整代码(节选,完整版见 examples/01-debounce.js):
js
function debounce(fn, wait = 300, immediate = false) {
let timer = null;
function debounced(...args) {
if (timer) clearTimeout(timer);
if (immediate) {
const callNow = !timer;
timer = setTimeout(() => (timer = null), wait);
if (callNow) return fn.apply(this, args);
} else {
timer = setTimeout(() => fn.apply(this, args), wait);
}
}
debounced.cancel = () => { clearTimeout(timer); timer = null; };
return debounced;
}复杂度:时间 O(1) per call,空间 O(1)
加分实现:
- 支持
immediate(leading edge) - 提供
cancel()与flush() - Promise 化:返回 Promise,让调用方能拿到
fn的结果 - 与 React 一起用时,记得
useMemo/useCallback包裹避免每次 render 都创建新实例
Q2:手写 throttle(节流)
题目:实现 throttle(fn, wait),单位时间内只执行一次。
考察点:闭包、时间戳 vs 定时器两种实现、leading/trailing 边界
思路:
| 方案 | 第一次 | 最后一次 | 适用场景 |
|---|---|---|---|
| 时间戳 | 立即 | 不触发 | 滚动加载 |
| 定时器 | 延迟 | 触发 | resize 完成 |
| 双剑合璧 | 立即 | 触发 | 通用 |
完整代码(节选):
js
function throttle(fn, wait = 300, opts = { leading: true, trailing: true }) {
let timer = null, lastTime = 0;
return function (...args) {
const now = Date.now();
if (!lastTime && !opts.leading) lastTime = now;
const remaining = wait - (now - lastTime);
if (remaining <= 0) {
timer && (clearTimeout(timer), (timer = null));
lastTime = now;
fn.apply(this, args);
} else if (!timer && opts.trailing) {
timer = setTimeout(() => {
lastTime = opts.leading ? Date.now() : 0;
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}复杂度:时间 O(1) per call
加分实现:
- 基于
requestAnimationFrame(动画场景,60fps 自然节流) - 与 debounce 配合:一段时间触发用节流,停止触发用防抖
Q3:手写 deepClone(深拷贝,处理循环引用)
题目:写一个深拷贝,能处理普通对象、数组、Date、RegExp、Map、Set、Symbol、循环引用。
考察点:递归、WeakMap 缓存、特殊类型处理、原型链保留
思路:
- 基本类型直接返回
- 用
WeakMap缓存原对象 → 新对象,递归前先查;命中则返回(解决循环引用) - 特殊类型分别处理:
Date、RegExp直接new;Map/Set单独构造 - 普通对象用
Object.create(getPrototypeOf(target))保留原型 - 用
Reflect.ownKeys同时拿到字符串 key 和 Symbol key
完整代码:见 examples/03-deep-clone.js
js
function deepClone(target, hash = new WeakMap()) {
if (target === null || typeof target !== "object") return target;
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target.source, target.flags);
if (hash.has(target)) return hash.get(target);
let cloneTarget;
if (target instanceof Map) {
cloneTarget = new Map(); hash.set(target, cloneTarget);
target.forEach((v, k) => cloneTarget.set(deepClone(k, hash), deepClone(v, hash)));
return cloneTarget;
}
if (target instanceof Set) {
cloneTarget = new Set(); hash.set(target, cloneTarget);
target.forEach((v) => cloneTarget.add(deepClone(v, hash)));
return cloneTarget;
}
cloneTarget = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));
hash.set(target, cloneTarget);
Reflect.ownKeys(target).forEach((k) => (cloneTarget[k] = deepClone(target[k], hash)));
return cloneTarget;
}复杂度:时间 O(n)(n 为属性总数),空间 O(n)
加分实现:
structuredClone:浏览器 / Node 17+ 内置,能克隆几乎所有结构化数据(含循环引用)- 不可变方式(immer):写时复制(CoW),性能更好
- JSON 法:
JSON.parse(JSON.stringify(x)),会丢undefined、function、Date变字符串、循环引用报错——只能用于纯数据
Q4:手写 shallowClone(浅拷贝)
题目:实现浅拷贝。
考察点:理解"浅"的含义、for...in vs Object.keys vs Reflect.ownKeys
思路:只拷贝第一层,嵌套对象仍是引用。
js
function shallowClone(target) {
if (target === null || typeof target !== "object") return target;
const cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) cloneTarget[key] = target[key];
}
return cloneTarget;
}复杂度:O(n)
加分实现:
- 内置 API:
{...obj}、Object.assign({}, obj)、Array.from(arr)、arr.slice() - 完整保留 Symbol/不可枚举属性:用
Reflect.ownKeys+Object.getOwnPropertyDescriptor
Q5:手写 new
题目:不使用 new 关键字,实现 new 的功能。
考察点:原型链、构造函数、构造函数返回值规则
思路:
- 创建空对象
Object.create(Constructor.prototype)链接原型Constructor.apply(obj, args)执行构造函数- 如果返回值是对象 → 返回该对象;否则返回 obj
js
function myNew(Constructor, ...args) {
if (typeof Constructor !== "function") throw new TypeError();
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result !== null && (typeof result === "object" || typeof result === "function") ? result : obj;
}复杂度:O(1)(不含构造函数本身)
加分回答:
- 解释
Object.create的内部机制(即 Q29) class不能用myNew调用(class 必须用 new)- 箭头函数没有
[[Construct]],传入会报错——验证写:if (!Constructor.prototype) throw ...
Q6:手写 instanceof
题目:手写 instanceof。
考察点:原型链查找
思路:沿着 Object.getPrototypeOf(left) 一直向上找,直到等于 right.prototype 或 null。
js
function myInstanceof(left, right) {
if (left === null || (typeof left !== "object" && typeof left !== "function")) return false;
let proto = Object.getPrototypeOf(left);
while (proto) {
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}复杂度:O(d)(d 为原型链深度)
加分:解释 Symbol.hasInstance 可重写 instanceof 行为:
js
class Even {
static [Symbol.hasInstance](x) { return x % 2 === 0; }
}
console.log(4 instanceof Even); // trueQ7:手写 call
题目:实现 Function.prototype.myCall。
考察点:this 绑定原理、避免污染 context
思路:把 fn 挂到 context 的临时属性上,调用,再删。临时 key 用 Symbol 避免覆盖。
js
Function.prototype.myCall = function (context, ...args) {
context = context == null ? globalThis : Object(context);
const key = Symbol("fn");
context[key] = this;
const result = context[key](...args);
delete context[key];
return result;
};复杂度:O(1)
加分:
- 解释 ES5 时代用
'__fn__' + Math.random()防冲突,ES6 用Symbol更优雅 context = Object(context):处理传入原始值(数字、字符串)的情况
Q8:手写 apply
题目:实现 Function.prototype.myApply。
考察点:与 call 的区别(参数为数组 / 类数组)
思路:与 call 几乎一致,只是参数解包方式不同。
js
Function.prototype.myApply = function (context, args) {
context = context == null ? globalThis : Object(context);
const key = Symbol("fn");
context[key] = this;
const result = args == null ? context[key]() : context[key](...args);
delete context[key];
return result;
};加分:参数校验,args 必须是数组或类数组(length 属性)。
Q9:手写 bind
题目:实现 Function.prototype.myBind。
考察点:柯里化、配合 new 调用时 this 的正确指向、prototype 继承
思路:
- 返回新函数
boundFn - 调用时合并
bindArgs和callArgs - 关键:判断
this instanceof boundFn,如果是 new 调用,this 应指向新实例 - 设置
boundFn.prototype = Object.create(fn.prototype),让 new 出来的实例能访问原型
js
Function.prototype.myBind = function (context, ...bindArgs) {
const fn = this;
function boundFn(...callArgs) {
const isNew = this instanceof boundFn;
return fn.apply(isNew ? this : context, [...bindArgs, ...callArgs]);
}
if (fn.prototype) boundFn.prototype = Object.create(fn.prototype);
return boundFn;
};加分:
- 原生
bind的特殊性:Function.prototype.bind返回的函数prototype为 undefined(不可被new用作 super class) - 性能:原生 bind 比 myBind 快 N 倍——能用原生用原生
Q10:手写 Promise(含 then/catch/finally)
题目:按 Promise A+ 规范实现一个 MyPromise。
考察点:状态机、微任务、resolvePromise 边界
核心规则:
- 三态:pending / fulfilled / rejected,不可逆
- then 必须返回新 Promise,支持链式
- then 回调必须异步执行(用
queueMicrotask) - then 的回调返回值要走
resolvePromise处理(区分 thenable / 普通值 / 自身循环引用)
完整代码:见 examples/10-promise.js(130 行带注释完整 A+ 实现)
关键片段:
js
class MyPromise {
constructor(executor) {
this.state = "pending";
this.cbs = { fulfilled: [], rejected: [] };
const resolve = (v) => {
if (this.state !== "pending") return;
this.state = "fulfilled"; this.value = v;
this.cbs.fulfilled.forEach((cb) => cb());
};
// ... reject 类似
try { executor(resolve, reject); } catch (e) { reject(e); }
}
}复杂度:每个 then O(1);空间 O(n)(n 为回调数)
加分:
- 通过 promises-aplus-tests 全套用例
- 实现
Promise.try、Promise.withResolvers(ES2024) - 解释为什么用
queueMicrotask而不是setTimeout(微任务 vs 宏任务)
Q11:手写 Promise.all
题目:实现 Promise.all(iterable)。
考察点:异步并发收集、按顺序保留结果、任一失败立即返回
思路:用 count 计数,所有都完成时 resolve;任一 reject 立即整体 reject。
js
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const arr = Array.from(promises);
if (arr.length === 0) return resolve([]);
const results = []; let count = 0;
arr.forEach((p, i) => {
Promise.resolve(p).then(
(v) => { results[i] = v; if (++count === arr.length) resolve(results); },
reject
);
});
});
}复杂度:时间 O(n),空间 O(n)
加分:
- 兼容非 Promise 值(
Promise.resolve(p)包装) - 兼容 iterable(不只是数组)
- 注意:reject 后其他 Promise 仍在执行,并不会"取消"——加分点是知道没有原生取消机制,需要 AbortController 配合
Q12:手写 Promise.race
题目:实现 Promise.race。
思路:谁先 settled 就谁的结果。
js
function promiseRace(promises) {
return new Promise((resolve, reject) => {
Array.from(promises).forEach((p) => Promise.resolve(p).then(resolve, reject));
});
}复杂度:O(n) 注册
加分:
- 实现"超时控制":
Promise.race([request, timeout(5000)]) - 空数组:原生
Promise.race([])返回永远 pending 的 Promise
Q13:手写 Promise.allSettled
题目:实现 Promise.allSettled,返回每个 Promise 的最终状态。
思路:与 all 类似,但每个都收集,且永不 reject。
js
function promiseAllSettled(promises) {
return new Promise((resolve) => {
const arr = Array.from(promises);
if (arr.length === 0) return resolve([]);
const results = []; let count = 0;
arr.forEach((p, i) => {
Promise.resolve(p).then(
(v) => { results[i] = { status: "fulfilled", value: v }; if (++count === arr.length) resolve(results); },
(r) => { results[i] = { status: "rejected", reason: r }; if (++count === arr.length) resolve(results); }
);
});
});
}复杂度:O(n)
加分:和 all 配合的"批量任务,失败不中断"的真实场景,常用于初始化页面多个独立请求。
Q14:手写 Promise.any
题目:实现 Promise.any,任一 fulfilled 即 resolve;全部 rejected 抛 AggregateError。
js
function promiseAny(promises) {
return new Promise((resolve, reject) => {
const arr = Array.from(promises);
if (arr.length === 0) return reject(new AggregateError([], "All promises rejected"));
const errors = []; let count = 0;
arr.forEach((p, i) => {
Promise.resolve(p).then(resolve, (r) => {
errors[i] = r;
if (++count === arr.length) reject(new AggregateError(errors, "All promises rejected"));
});
});
});
}加分:解释 AggregateError(ES2021 新增),能携带多个错误。
Q15:手写柯里化 currying
题目:把 add(a, b, c) 转换成 add(a)(b)(c) 或任意组合形式。
考察点:闭包、fn.length、递归
js
function curry(fn, ...preset) {
return function curried(...args) {
const all = [...preset, ...args];
if (all.length >= fn.length) return fn.apply(this, all);
return curry(fn, ...all);
};
}复杂度:每次调用 O(n)(创建新数组)
加分:
- 占位符
_:curry(add)(1, _, 3)(2)也能工作(见examples/15-curry.js) - 理解柯里化的实战价值:参数复用、惰性求值、函数式编程
Q16:手写 compose / pipe
题目:
compose(f, g, h)(x) === f(g(h(x)))—— 从右到左pipe(f, g, h)(x) === h(g(f(x)))—— 从左到右
js
const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)));
const pipe = (...fns) => fns.reduce((a, b) => (...args) => b(a(...args)));复杂度:执行 O(n);构造 O(1)
加分:
- 异步版:
composeAsync = (...fns) => x => fns.reduceRight((p, fn) => p.then(fn), Promise.resolve(x)) - Redux 中间件
applyMiddleware就是 compose 的经典应用
Q17:手写 EventEmitter(发布订阅)
题目:实现 on / once / off / emit。
考察点:闭包、事件队列、once 的去除技巧
js
class EventEmitter {
constructor() { this.events = Object.create(null); }
on(e, fn) { (this.events[e] ||= []).push(fn); return this; }
off(e, fn) { if (!fn) delete this.events[e]; else this.events[e] = this.events[e]?.filter(f => f !== fn && f.__origin !== fn) || []; return this; }
once(e, fn) { const w = (...a) => { fn(...a); this.off(e, w); }; w.__origin = fn; this.on(e, w); return this; }
emit(e, ...a){ this.events[e]?.slice().forEach(fn => fn(...a)); return this; }
}复杂度:on/off O(n) per remove;emit O(n)
加分:
emit内slice()是为了应对回调里又 off 自己导致迭代错乱- 支持通配符
*、命名空间user:click - 提供
eventNames()、listenerCount()等管理 API
Q18:数组扁平化(多种实现)
题目:[1,[2,[3,[4]]]] → [1,2,3,4]。
多种实现:
js
const flatRecursive = (arr, d = 1) => arr.reduce((a, c) =>
a.concat(Array.isArray(c) && d > 0 ? flatRecursive(c, d - 1) : c), []);
const flatStack = (arr) => {
const s = [...arr], r = [];
while (s.length) {
const v = s.pop();
Array.isArray(v) ? s.push(...v) : r.push(v);
}
return r.reverse();
};
const flatStr = (arr) => arr.toString().split(",").map(Number); // 仅纯数字
const flatSpread = (arr) => { while (arr.some(Array.isArray)) arr = [].concat(...arr); return arr; };
const flatNative = (arr, d = 1) => arr.flat(d);复杂度:时间 O(n);递归版空间 O(d)(栈深度)
加分:
- 大数组慎用递归(爆栈)→ 用栈版
- 用
Array.prototype.flat(Infinity)一行搞定
Q19:数组去重(多种实现)
js
const u1 = (a) => [...new Set(a)]; // Set,最简
const u2 = (a) => a.filter((v, i) => a.indexOf(v) === i); // NaN 会丢
const u3 = (a) => a.reduce((r, v) => r.includes(v) ? r : [...r, v], []); // NaN 也能去重
const u4 = (a, k = (x) => x) => { // 按字段去重对象数组
const m = new Map();
return a.filter((v) => !m.has(k(v)) && m.set(k(v), 1));
};复杂度:u1/u4 O(n);u2/u3 O(n²)
加分:
- 解释
Set用 SameValueZero 算法(NaN === NaN为 true) - 大数组对象去重:用 Map 缓存,避免 O(n²)
Q20:isEqual(深度相等)
思路:递归比对,支持 NaN、Date、RegExp、循环引用。
js
function isEqual(a, b, seen = new WeakMap()) {
if (Object.is(a, b)) return true; // NaN===NaN 也是 true
if (a === null || b === null) return false;
if (typeof a !== "object" || typeof b !== "object") return false;
if (a instanceof Date) return b instanceof Date && a.getTime() === b.getTime();
if (a instanceof RegExp) return b instanceof RegExp && a.toString() === b.toString();
if (seen.get(a) === b) return true;
seen.set(a, b);
if (Array.isArray(a) !== Array.isArray(b)) return false;
const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
return ka.length === kb.length && ka.every((k) => kb.includes(k) && isEqual(a[k], b[k], seen));
}复杂度:O(n)(n 为属性总数)
加分:lodash 的 _.isEqual 还处理 Map/Set/ArrayBuffer/正则 lastIndex 等更多类型。
Q21:千分位格式化
js
const fmt1 = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const fmt2 = (n) => n.toLocaleString("en-US");正则解释:\B 非单词边界,(?=(\d{3})+(?!\d)) 后面跟着 1+ 个三位数字组且后面不再有数字。
加分:处理小数(先按 . split)、负数、大数(用字符串而非 number 输入)。
Q22:URL 参数解析
js
function parseQuery(url) {
const result = {};
const q = url.split("?")[1]?.split("#")[0];
if (!q) return result;
q.split("&").forEach((pair) => {
const [k, v = ""] = pair.split("=");
const key = decodeURIComponent(k), val = decodeURIComponent(v);
if (result[key] !== undefined) result[key] = [].concat(result[key], val);
else result[key] = val;
});
return result;
}加分:
- 现代写法:
new URL(url).searchParams - 反向:
stringifyQuery({a:1,b:[2,3]})→'a=1&b=2&b=3' - 处理
[]形式 / 嵌套对象(参考qs库)
Q23:模板字符串解析 render
js
function render(tpl, data) {
return tpl.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) =>
key.split(".").reduce((o, k) => (o == null ? "" : o[k]), data) ?? ""
);
}加分:
- 支持
{{#if cond}}...{{/if}}与{{#each list}}...{{/each}}(见examples/23-render-template.js) - 编译为 Function 加速:
new Function('data', 'with(data){return \'+ tpl +'`}')` - 注意 XSS:用户输入要做 HTML 转义
Q24:sleep
js
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await sleep(1000);加分:
- 同步阻塞版(
while(Date.now()<end){})—— 仅讲原理,生产严禁 - 可取消版:暴露
cancel()方法
Q25:异步并发限流 asyncPool
题目:限制同时执行的 Promise 数量为 limit。
js
async function asyncPool(limit, tasks) {
const results = []; const executing = new Set();
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p); executing.add(p);
p.finally(() => executing.delete(p));
if (executing.size >= limit) await Promise.race(executing);
}
return Promise.all(results);
}思路:
- 每次启动一个任务推入 results 和 executing
- 当 executing 满时,
await Promise.race(executing)等任意一个完成,留出位置给下一个
复杂度:时间 O(n);并发数恒为 limit
加分:
- 大文件分片上传的核心算法
- p-limit 库就是这种实现
- 注意
task传入的是函数,不是已启动的 Promise(否则无法控制启动时机)
Q26:LazyMan(链式 + 异步)
题目:见 examples/26-lazy-man.js,要求支持 sleepFirst(插队到队首)。
思路:
- 任务队列
queue - 链式方法都返回
this,并往队列加任务 setTimeout(0)让构造完链式才开始 next,保证 sleepFirst 能插到队首- 每个任务完成后调用
this.next()
加分:
- 用
async/await重写更清晰:构造一个 Promise 链,每个 then 包一个步骤 - 用 Generator + 调度器实现
Q27:LRU Cache
题目:实现 get/put,容量满时淘汰最久未使用项。
js
class LRUCache {
constructor(cap) { this.cap = cap; this.map = new Map(); }
get(k) {
if (!this.map.has(k)) return -1;
const v = this.map.get(k);
this.map.delete(k); this.map.set(k, v);
return v;
}
put(k, v) {
if (this.map.has(k)) this.map.delete(k);
else if (this.map.size >= this.cap) this.map.delete(this.map.keys().next().value);
this.map.set(k, v);
}
}复杂度:get/put 均 O(1)(Map 内部用 hash + 链表)
加分:
- 经典实现是双向链表 + HashMap,自己实现一遍
- LRU 的应用:浏览器 HTTP 缓存、React
useMemo、Vue keep-alive、数据库连接池
Q28:手写 JSON.parse(简易版)
两种思路:
- eval 法(不安全):
new Function('return ' + str)() - 递归下降解析器(完整版见
examples/28-json-parse.js)
核心 token:{ } [ ] , : string number true false null
加分:
- 解释为什么不能直接用 eval(XSS / 注入)
- 真正的 JSON.parse 是 V8 用 C++ 写的状态机,速度极快
Q29:手写 Object.create
js
function objectCreate(proto, props) {
function F() {}
F.prototype = proto;
const obj = new F();
if (proto === null) Object.setPrototypeOf(obj, null);
if (props) Object.defineProperties(obj, props);
return obj;
}加分:解释 Object.create(null) 创建无原型对象的常见用途(避免 __proto__ 污染、做纯字典)。
Q30:红绿灯(异步流程控制)
题目:红 3s → 绿 1s → 黄 2s 循环。
js
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function light(c, ms) { console.log(c); await sleep(ms); }
async function trafficLight() {
while (true) {
await light("RED", 3000);
await light("GREEN", 1000);
await light("YELLOW", 2000);
}
}加分版本:
- Promise.then 链式版本(不用 async/await)
- Generator + 调度器版本(更接近 koa 中间件思路)
- 加
AbortSignal支持外部停止
附:30 道题快速速记表
| # | 题目 | 一句话核心 |
|---|---|---|
| 1 | debounce | 闭包 + clearTimeout 重置 |
| 2 | throttle | 时间戳 / 定时器 / RAF |
| 3 | deepClone | 递归 + WeakMap 防循环 |
| 4 | shallowClone | for...in + hasOwnProperty |
| 5 | new | Object.create + apply + 返回值判断 |
| 6 | instanceof | 沿原型链查 |
| 7 | call | Symbol 临时挂 + delete |
| 8 | apply | 参数为数组 |
| 9 | bind | 闭包 + this instanceof boundFn |
| 10 | Promise | 状态机 + queueMicrotask + resolvePromise |
| 11 | Promise.all | 计数 + 任一 reject 即整体 reject |
| 12 | Promise.race | 谁先 settled 就用谁 |
| 13 | allSettled | 永不 reject,全收集 |
| 14 | any | 任一 fulfilled 即 resolve,全 reject 抛 AggregateError |
| 15 | curry | fn.length 判断够不够 |
| 16 | compose | reduce 串起来 |
| 17 | EventEmitter | events Map + once 用 wrapper.__origin |
| 18 | flat | 递归 / 栈 / reduce |
| 19 | unique | Set / Map 按字段 |
| 20 | isEqual | Object.is + 递归 + WeakMap |
| 21 | 千分位 | \B(?=(\d{3})+(?!\d)) |
| 22 | URL 参数 | split('&') + decodeURIComponent |
| 23 | render | replace + 正则 + reduce 取值 |
| 24 | sleep | new Promise + setTimeout |
| 25 | asyncPool | for + Promise.race(executing) |
| 26 | LazyMan | 任务队列 + sleepFirst 用 unshift |
| 27 | LRU | Map.delete + set 提到尾部 |
| 28 | JSON.parse | 递归下降 |
| 29 | Object.create | 空构造函数挂 prototype |
| 30 | 红绿灯 | async/await + while 循环 |
写到最后:手写题练到能"边讲边写"才算真懂。建议把这 30 道题录成自己的视频,一遍讲解一遍写代码——能讲明白的题才是真的会。