Skip to content

附录 A1 · 30 道高频手写题完整解答

每道题统一按 题目 / 考察点 / 思路 / 完整代码(带注释)/ 复杂度分析 / 加分实现 展开。 所有代码可在浏览器/Node 中直接运行;配套 examples/ 目录提供独立文件。


Q1:手写 debounce(防抖)

题目:实现一个 debounce(fn, wait, immediate?),wait 毫秒内重复触发只执行最后一次。

考察点:闭包、this 绑定、setTimeout、参数透传、边界(立即执行 / 取消)

思路

  1. 用闭包持有 timer 变量
  2. 返回一个新函数,每次触发先 clearTimeout(timer),再 setTimeout 重新计时
  3. fn.apply(this, args) 透传 this 和参数
  4. 进阶: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 缓存、特殊类型处理、原型链保留

思路

  1. 基本类型直接返回
  2. WeakMap 缓存 原对象 → 新对象,递归前先查;命中则返回(解决循环引用)
  3. 特殊类型分别处理:DateRegExp 直接 newMap/Set 单独构造
  4. 普通对象用 Object.create(getPrototypeOf(target)) 保留原型
  5. 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)),会丢 undefinedfunctionDate 变字符串、循环引用报错——只能用于纯数据

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 的功能。

考察点:原型链、构造函数、构造函数返回值规则

思路

  1. 创建空对象
  2. Object.create(Constructor.prototype) 链接原型
  3. Constructor.apply(obj, args) 执行构造函数
  4. 如果返回值是对象 → 返回该对象;否则返回 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.prototypenull

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); // true

Q7:手写 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 继承

思路

  1. 返回新函数 boundFn
  2. 调用时合并 bindArgscallArgs
  3. 关键:判断 this instanceof boundFn,如果是 new 调用,this 应指向新实例
  4. 设置 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 边界

核心规则

  1. 三态:pending / fulfilled / rejected,不可逆
  2. then 必须返回新 Promise,支持链式
  3. then 回调必须异步执行(用 queueMicrotask
  4. 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.tryPromise.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)

加分

  • emitslice() 是为了应对回调里又 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(深度相等)

思路:递归比对,支持 NaNDateRegExp、循环引用。

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);
}

思路

  1. 每次启动一个任务推入 results 和 executing
  2. 当 executing 满时,await Promise.race(executing) 等任意一个完成,留出位置给下一个

复杂度:时间 O(n);并发数恒为 limit

加分

  • 大文件分片上传的核心算法
  • p-limit 库就是这种实现
  • 注意 task 传入的是函数,不是已启动的 Promise(否则无法控制启动时机)

Q26:LazyMan(链式 + 异步)

题目:见 examples/26-lazy-man.js,要求支持 sleepFirst(插队到队首)。

思路

  1. 任务队列 queue
  2. 链式方法都返回 this,并往队列加任务
  3. setTimeout(0) 让构造完链式才开始 next,保证 sleepFirst 能插到队首
  4. 每个任务完成后调用 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(简易版)

两种思路

  1. eval 法(不安全):new Function('return ' + str)()
  2. 递归下降解析器(完整版见 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 道题快速速记表

#题目一句话核心
1debounce闭包 + clearTimeout 重置
2throttle时间戳 / 定时器 / RAF
3deepClone递归 + WeakMap 防循环
4shallowClonefor...in + hasOwnProperty
5newObject.create + apply + 返回值判断
6instanceof沿原型链查
7callSymbol 临时挂 + delete
8apply参数为数组
9bind闭包 + this instanceof boundFn
10Promise状态机 + queueMicrotask + resolvePromise
11Promise.all计数 + 任一 reject 即整体 reject
12Promise.race谁先 settled 就用谁
13allSettled永不 reject,全收集
14any任一 fulfilled 即 resolve,全 reject 抛 AggregateError
15curryfn.length 判断够不够
16composereduce 串起来
17EventEmitterevents Map + once 用 wrapper.__origin
18flat递归 / 栈 / reduce
19uniqueSet / Map 按字段
20isEqualObject.is + 递归 + WeakMap
21千分位\B(?=(\d{3})+(?!\d))
22URL 参数split('&') + decodeURIComponent
23renderreplace + 正则 + reduce 取值
24sleepnew Promise + setTimeout
25asyncPoolfor + Promise.race(executing)
26LazyMan任务队列 + sleepFirst 用 unshift
27LRUMap.delete + set 提到尾部
28JSON.parse递归下降
29Object.create空构造函数挂 prototype
30红绿灯async/await + while 循环

写到最后:手写题练到能"边讲边写"才算真懂。建议把这 30 道题录成自己的视频,一遍讲解一遍写代码——能讲明白的题才是真的会。