学完这篇,你能拿到 20 道高频手写题的可用原生实现,每份实现都标出了面试官最常追问的边界条件。
第一步:先把手写题分成四类
按主题分组记忆,出现任何题目都能快速对应到同一套模板:
- 函数控制:防抖、节流、柯里化、compose
- this 与对象:call、apply、bind、new、instanceof、深拷贝
- 原型与继承:Object.create、寄生组合继承
- 异步:Promise.all、并发池、sleep、重试、简易 Promise、发布订阅
面试时先说出题目类型,再写代码,比直接写出代码得分更高。
第二步:防抖与节流(最常考,也是边界最多的)
function debounce(fn, wait = 300, immediate = false) {
let timer = null, result;
function debounced(...args) {
if (timer) clearTimeout(timer);
if (immediate) {
if (!timer) result = fn.apply(this, args);
timer = setTimeout(() => { timer = null; }, wait);
} else {
timer = setTimeout(() => { fn.apply(this, args); timer = null; }, wait);
}
return result;
}
debounced.cancel = () => { clearTimeout(timer); timer = null; };
return debounced;
}
function throttle(fn, wait = 300) {
let last = 0, timer = null;
return function (...args) {
const now = Date.now();
const remain = wait - (now - last);
if (remain <= 0) {
clearTimeout(timer); timer = null;
last = now;
fn.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => { last = Date.now(); timer = null; fn.apply(this, args); }, remain);
}
};
}
注意:防抖必须用 fn.apply(this, args) 保留调用时的 this 和参数;节流只写时间戳版会丢最后一次触发,必须补定时器。
第三步:call、apply、bind、new
Function.prototype.myCall = function (ctx, ...args) {
ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx);
const key = Symbol('fn');
ctx[key] = this;
const res = ctx[key](...args);
delete ctx[key];
return res;
};
// apply 只把 ...args 换成 args 数组即可
Function.prototype.myBind = function (ctx, ...preset) {
const fn = this;
function Bound(...args) {
return fn.apply(this instanceof Bound ? this : ctx, [...preset, ...args]);
}
Bound.prototype = Object.create(fn.prototype);
return Bound;
};
function myNew(Ctor, ...args) {
if (typeof Ctor !== 'function') throw new TypeError('not a constructor');
const obj = Object.create(Ctor.prototype);
const res = Ctor.apply(obj, args);
return (res !== null && typeof res === 'object') ? res : obj;
}
注意:ctx 为 null 或 undefined 时要指向 globalThis;原始值要先用 Object() 包装。bind 返回的函数要支持 new,所以要判断 this instanceof Bound。
第四步:instanceof 与 Object.create
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;
}
function myCreate(proto) {
function F() {}
F.prototype = proto;
return new F();
}
寄生组合继承最简单:Child.prototype = Object.create(Parent.prototype),再把 constructor 指回 Child,最后 Parent.call(this, ...)。
第五步:深拷贝(注意 WeakMap 处理循环引用)
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (map.has(obj)) return map.get(obj);
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
const target = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
map.set(obj, target);
Reflect.ownKeys(obj).forEach(k => { target[k] = deepClone(obj[k], map); });
return target;
}
注意:面试官一定会问循环引用和 Symbol 键,WeakMap 和 Reflect.ownKeys 就是这两处的答案。
第六步:数组与函数式四道
const flatten = (arr, depth = 1) =>
depth < 1 ? arr.slice() :
arr.reduce((acc, v) => acc.concat(Array.isArray(v) ? flatten(v, depth - 1) : v), []);
const uniq = arr => [...new Set(arr)]; // 对象去重再说 Map + JSON.stringify
function curry(fn) {
return function curried(...args) {
return args.length >= fn.length ? fn.apply(this, args) : (...rest) => curried.apply(this, [...args, ...rest]);
};
}
const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)));
注意:flat 的 depth 默认是 1,并非无限;不是传入 Infinity 才无限扁平。
第七步:异步四道
function myPromiseAll(list) {
return new Promise((resolve, reject) => {
const arr = [...list], res = new Array(arr.length);
let count = 0;
if (!arr.length) return resolve(res);
arr.forEach((p, i) => {
Promise.resolve(p).then(v => { res[i] = v; if (++count === arr.length) resolve(res); }, reject);
});
});
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function pool(tasks, limit = 3) {
const ret = []; let i = 0;
const worker = async () => { while (i < tasks.length) { const idx = i++; ret[idx] = await tasks[idx](); } };
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
return ret;
}
顺带要能说出简易 Promise 的三个要点:状态只能从 pending 变一次、then 回调进微任务(用 queueMicrotask)、then 返回新 Promise 以支持链式调用。