先说个丢人的。上个月面某大厂,一面八股背得贼溜,事件循环、闭包、原型链一套一套的,面试官连连点头。我心里正飘呢,二面轻描淡写一句:「来,手写个 Promise,then catch finally 都实现,循环引用处理一下」。
我当场裂开。
平时 await 一把梭,真让我从零写一个,手比脑子快——resolvePromise 里 thenable 和循环引用那块直接卡死,憋了十分钟写了个四不像,连微任务都忘包了。出来我就知道,这 offer 悬了。
不是委屈,是后怕。我写前端三四年,Promise 天天用,真让我造一遍轮子居然造不利索。这哪是面试翻车,是我一直赖在「会用就行」的舒适区,从没把轮子拆开看过。
说真的,手写题这东西特别能验人。你框架用得再溜,手写一道响应式源码,是骡子是马立马现形。面试官也精,与其听你吹项目多牛,不如扔一道题看你能不能把原理落到代码上。所以这几年前端面试,手写题和源码解析一直是硬通货——你刷十篇业务总结,不如真刀真枪手写五道题来得实在。
出来之后越想越气,跟自己较上了劲:与其每次面试前临时抱佛脚背八股,不如把高频手写题自己写一遍、跑一遍、讲明白一遍。一个周末肝完,攒了 30 道,顺手把 Vue3 / React18 原理脑图也画了。今天直接白送,别灌水,纯干货。
我特意没整「100 题」那种凑数合集。30 道,每道带代码 + 解析 + 考点三件套,复制进 node 就能跑。比看别人贴的结论强十倍——你跑一遍才会发现「卧槽我这里少判了个状态」。
上真家伙
Promise 核心实现(这题不过,别的都白搭)
class MyPromise {
static PENDING = 'pending';
static FULFILLED = 'fulfilled';
static REJECTED = 'rejected';
constructor(executor) {
this.state = MyPromise.PENDING;
this.value = undefined; this.reason = undefined;
this.onFulfilledCallbacks = []; this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === MyPromise.PENDING) {
this.state = MyPromise.FULFILLED; this.value = value;
this.onFulfilledCallbacks.forEach((fn) => fn());
}
};
const reject = (reason) => {
if (this.state === MyPromise.PENDING) {
this.state = MyPromise.REJECTED; this.reason = reason;
this.onRejectedCallbacks.forEach((fn) => fn());
}
};
try { executor(resolve, reject); } catch (e) { reject(e); }
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
onRejected = typeof onRejected === 'function' ? onRejected : (r) => { throw r; };
const promise2 = new MyPromise((resolve, reject) => {
const handle = (fn, val) => queueMicrotask(() => {
try { const x = fn(val); resolvePromise(promise2, x, resolve, reject); }
catch (e) { reject(e); }
});
if (this.state === MyPromise.FULFILLED) handle(onFulfilled, this.value);
else if (this.state === MyPromise.REJECTED) handle(onRejected, this.reason);
else {
this.onFulfilledCallbacks.push(() => handle(onFulfilled, this.value));
this.onRejectedCallbacks.push(() => handle(onRejected, this.reason));
}
});
return promise2;
}
catch(onRejected) { return this.then(null, onRejected); }
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) return reject(new TypeError('Chaining cycle'));
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; resolvePromise(promise2, y, resolve, reject);
}, (e) => { if (called) return; called = true; reject(e); });
} else resolve(x);
} catch (e) { if (called) return; called = true; reject(e); }
} else resolve(x);
}
坑全在 resolvePromise:if (promise2 === x) 拦循环引用;返回的对象带 then 就递归 resolve(thenable 穿透)。这两点答出来,面试官基本就点头了。还有 then 回调一定用微任务包,不然时序对不上——我当时就是栽这儿。
防抖节流(90% 人只写得出半截)
function debounce(fn, wait = 300, immediate = false) {
let timer = null, invoked = false;
const debounced = function (...args) {
if (timer) clearTimeout(timer);
if (immediate && !invoked) { fn.apply(this, args); invoked = true; }
timer = setTimeout(() => {
if (!immediate) fn.apply(this, args);
invoked = false; timer = null;
}, wait);
};
debounced.cancel = () => { clearTimeout(timer); timer = null; invoked = false; };
return debounced;
}
function throttle(fn, wait = 300) {
let last = 0, timer = null;
return function (...args) {
const now = Date.now();
const remaining = wait - (now - last);
if (remaining <= 0) {
if (timer) { clearTimeout(timer); timer = null; }
last = now; fn.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => { last = Date.now(); timer = null; fn.apply(this, args); }, remaining);
}
};
}
别只会 setTimeout 版。immediate 首触发、cancel 取消、节流里时间戳版 vs 定时器版——时间戳版首调立即执行但末尾漏一次,定时器版末尾补一刀,remaining 算窗口剩余是精髓。我第一次写节流就只写了时间戳版,被追问「末尾那次不执行合理吗」直接卡壳。
Vue3 响应式(框架原理真·重头戏,二三线厂也爱问)
let activeEffect = null;
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
dep.add(activeEffect);
}
function trigger(target, key) {
const dep = targetMap.get(target) && targetMap.get(target).get(key);
dep && [...dep].forEach((effect) => effect());
}
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key);
const res = Reflect.get(obj, key, receiver);
return typeof res === 'object' && res !== null ? reactive(res) : res;
},
set(obj, key, value, receiver) {
const ok = Reflect.set(obj, key, value, receiver);
trigger(obj, key); return ok;
}
});
}
Proxy 拦 get/set,get 时 track 收集依赖、set 时 trigger 触发更新,WeakMap 按 target -> key -> Set 三层存。自己敲一遍,你就懂 Vue3 凭啥比 Object.defineProperty 香——数组、新增属性不用挨个劫持,WeakMap 还不挡原对象回收。光会背「Vue3 用 Proxy」没用,让你手写 track/trigger 哑火照样挂。
我踩过的坑,兄弟们别重蹈
- 只背不写:看十遍 Promise 实现,不如关掉网页自己写一遍。写不出就是没真懂。
- 忽略边缘:面试官多半不考主流程,考循环引用、thenable、空参、new 调 bind 这些犄角旮旯。
- 框架只背概念:「响应式是 Proxy」谁都会,手写 track/trigger 就露馅。
- 防抖节流混为一谈:一个管「最后执行」,一个管「频率上限」,讲不清区别等于白答。
包里都有啥
30 题六类:异步与 Promise、this 与原型、函数进阶、数组与对象、框架原理、工程与算法。从 call/apply/bind、curry、compose、深拷贝、大数相加、千分位、懒加载,面试常客基本齐了。
外加两张脑图当骨架:
- Vue3 原理:响应式 / 运行时 / 编译 / 性能
- React18 原理:Fiber / 调和 / Hooks / 并发 / 事件
目录:
前端面试手写题宝典/
├─ 前端面试手写题宝典.pdf # 30 题,代码+解析+考点
├─ Vue3底层原理思维导图.svg
├─ React18底层原理思维导图.svg
└─ README.md
怎么用
PDF 当刷题册,先自己写再对答案;两张 SVG 脑图像地图,面试前扫一遍心里有底。包里带了生成脚本,想加题自己改数据重跑就完事。
怎么拿
代码都是面试向精简实现,讲思路为主,生产请以官方源码为准。欢迎转发,但别拿去包装倒卖,那样真没劲。也祝在看的同学,下次面试手写题下笔如有神,别像我一样当场裂开。见评论区