前端高频手写面试题大全
一、Promise 系列
Promise 是前端异步编程的核心,相关手写题出现频率极高。
1. 手写 Promise(完整版)
思路:实现一个符合 Promise/A+ 规范的 Promise,包含状态管理、then 链式调用、值穿透、错误捕获等 。
class MyPromise {
constructor(executor) {
this.state = 'pending'; // pending/fulfilled/rejected
this.value = undefined; // 成功的值
this.reason = undefined; // 失败的原因
this.onFulfilledCallbacks = []; // 成功回调队列
this.onRejectedCallbacks = []; // 失败回调队列
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
// 执行所有成功回调
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
// 执行所有失败回调
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
// 处理值穿透:如果 onFulfilled/onRejected 不是函数,则创建一个默认函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0);
}
if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0);
}
if (this.state === 'pending') {
// 将回调存入队列
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
}, 0);
});
}
});
return promise2;
}
resolvePromise(promise2, x, resolve, reject) {
// 防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called = false; // 防止多次调用
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then;
if (typeof then === 'function') {
// x 是 thenable 对象或 Promise
then.call(
x,
y => {
if (called) return;
called = true;
this.resolvePromise(promise2, y, resolve, reject);
},
err => {
if (called) return;
called = true;
reject(err);
}
);
} else {
// x 是普通对象
resolve(x);
}
} catch (err) {
if (called) return;
called = true;
reject(err);
}
} else {
// x 是普通值
resolve(x);
}
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
reason => MyPromise.resolve(callback()).then(() => { throw reason })
);
}
static resolve(value) {
if (value instanceof MyPromise) return value;
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
MyPromise.resolve(promises[i]).then(
value => {
results[i] = value;
count++;
if (count === promises.length) resolve(results);
},
reject
);
}
});
}
static race(promises) {
return new MyPromise((resolve, reject) => {
for (const p of promises) {
MyPromise.resolve(p).then(resolve, reject);
}
});
}
static allSettled(promises) {
return new MyPromise(resolve => {
const results = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
MyPromise.resolve(promises[i]).then(
value => {
results[i] = { status: 'fulfilled', value };
count++;
if (count === promises.length) resolve(results);
},
reason => {
results[i] = { status: 'rejected', reason };
count++;
if (count === promises.length) resolve(results);
}
);
}
});
}
}2. 手写 Promise.all
思路:接收一个 Promise 数组,全部成功时返回结果数组,任一失败则立即失败 。
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
const results = [];
let count = 0;
if (promises.length === 0) {
resolve(results);
return;
}
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
value => {
results[i] = value;
count++;
if (count === promises.length) resolve(results);
},
reject
);
}
});
};3. 手写 Promise.race
思路:接收一个 Promise 数组,返回第一个改变状态的结果 。
Promise.myRace = function(promises) {
return new Promise((resolve, reject) => {
for (const p of promises) {
Promise.resolve(p).then(resolve, reject);
}
});
};4. 手写 Promise.allSettled
思路:等待所有 Promise 完成,返回每个 Promise 的结果状态 。
Promise.myAllSettled = function(promises) {
return new Promise(resolve => {
const results = [];
let count = 0;
if (promises.length === 0) {
resolve(results);
return;
}
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
value => {
results[i] = { status: 'fulfilled', value };
count++;
if (count === promises.length) resolve(results);
},
reason => {
results[i] = { status: 'rejected', reason };
count++;
if (count === promises.length) resolve(results);
}
);
}
});
};5. 手写 Promise.any
思路:接收一个 Promise 数组,返回第一个成功的 Promise。如果全部失败,则返回 AggregateError。
Promise.myAny = function(promises) {
return new Promise((resolve, reject) => {
const errors = [];
let count = 0;
if (promises.length === 0) {
reject(new AggregateError('All promises were rejected'));
return;
}
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(
resolve,
err => {
errors[i] = err;
count++;
if (count === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
}
});
};6. 手写 Promise 并发控制(async-pool)
思路:控制同时执行的 Promise 数量,常用于并发请求限制 。
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = [];
const executing = [];
for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p);
if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}
return Promise.all(ret);
}
// 使用示例
const timeout = (i) => new Promise(resolve => setTimeout(() => resolve(i), i));
asyncPool(2, [1000, 2000, 3000, 1000], timeout).then(console.log);二、防抖与节流
7. 手写防抖(debounce)
思路:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时 。
function debounce(func, wait = 300, immediate = false) {
let timer = null;
return function(...args) {
const context = this;
if (timer) clearTimeout(timer);
if (immediate && !timer) {
// 立即执行一次
func.apply(context, args);
timer = setTimeout(() => {
timer = null;
}, wait);
} else {
timer = setTimeout(() => {
func.apply(context, args);
timer = null;
}, wait);
}
};
}8. 手写节流(throttle)
思路:规定一个单位时间,在这个单位时间内最多只能触发一次函数执行 。
// 时间戳版本(立即执行)
function throttle(func, wait) {
let previous = 0;
return function(...args) {
const now = Date.now();
if (now - previous >= wait) {
func.apply(this, args);
previous = now;
}
};
}
// 定时器版本(延迟执行)
function throttle(func, wait) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, wait);
}
};
}
// 综合版:支持首次和末次执行
function throttle(func, wait, options = { leading: true, trailing: true }) {
let timer = null;
let previous = 0;
return function(...args) {
const now = Date.now();
if (!previous && options.leading === false) previous = now;
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer);
timer = null;
}
previous = now;
func.apply(this, args);
} else if (!timer && options.trailing !== false) {
timer = setTimeout(() => {
previous = options.leading === false ? 0 : Date.now();
timer = null;
func.apply(this, args);
}, remaining);
}
};
}三、this 指向相关(call/apply/bind)
9. 手写 call 函数
思路:将函数设为对象的属性,执行该函数,删除该属性 。
Function.prototype.myCall = function(context = window, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
// 使用 Symbol 创建唯一键,避免覆盖原有属性
const fn = Symbol('fn');
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};10. 手写 apply 函数
思路:与 call 类似,参数以数组形式传入 。
Function.prototype.myApply = function(context = window, args = []) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
const fn = Symbol('fn');
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};11. 手写 bind 函数
思路:返回一个绑定 this 的新函数,支持预设参数,且需要考虑 new 调用的优先级 。
Function.prototype.myBind = function(context, ...bindArgs) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
const self = this;
return function F(...callArgs) {
const args = [...bindArgs, ...callArgs];
// 如果使用 new 调用,this 指向实例,需要忽略传入的 context
if (this instanceof F) {
return new self(...args);
}
return self.apply(context, args);
};
};四、对象与数组工具函数
12. 手写深拷贝(deepClone)
思路:处理循环引用、特殊对象(Date、RegExp)、Symbol 等 。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Error) return new Error(obj.message);
if (hash.has(obj)) return hash.get(obj);
const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
// 遍历所有属性,包括 Symbol 属性
[...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)].forEach(key => {
clone[key] = deepClone(obj[key], hash);
});
return clone;
}13. 手写 instanceof
思路:沿着原型链查找,判断右侧构造函数的 prototype 是否在左侧的原型链上 。
function myInstanceof(left, right) {
if (left === null || typeof left !== 'object') return false;
let proto = Object.getPrototypeOf(left);
while (true) {
if (proto === null) return false;
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}14. 手写 new 操作符
思路:创建新对象,链接原型,绑定 this,返回对象 。
function myNew(ctor, ...args) {
if (typeof ctor !== 'function') {
throw new TypeError('ctor must be a function');
}
// 创建新对象,原型指向构造函数的 prototype
const obj = Object.create(ctor.prototype);
// 执行构造函数,绑定 this
const result = ctor.apply(obj, args);
// 如果构造函数返回对象,则返回该对象;否则返回创建的新对象
return (result !== null && typeof result === 'object') ? result : obj;
}15. 手写 Object.create
思路:创建一个新对象,使用现有对象作为新对象的原型 。
function myCreate(proto) {
if (typeof proto !== 'object' || proto === null) {
throw new TypeError('Object prototype may only be an Object or null');
}
function F() {}
F.prototype = proto;
return new F();
}16. 手写类数组转数组
思路:将 arguments 或 NodeList 等类数组对象转换为真正的数组 。
function toArray(arrLike) {
// 方法1:Array.from
return Array.from(arrLike);
// 方法2:扩展运算符(需支持迭代器)
return [...arrLike];
// 方法3:Array.prototype.slice
return Array.prototype.slice.call(arrLike);
// 方法4:Array.prototype.concat
return Array.prototype.concat.apply([], arrLike);
}17. 手写数组去重
思路:多种方式实现数组去重 。
function unique(arr) {
// 方法1:Set
return [...new Set(arr)];
// 方法2:filter + indexOf
return arr.filter((item, index) => arr.indexOf(item) === index);
// 方法3:Map
const map = new Map();
return arr.filter(item => !map.has(item) && map.set(item, true));
// 方法4:reduce
return arr.reduce((acc, cur) => {
if (!acc.includes(cur)) acc.push(cur);
return acc;
}, []);
}18. 手写数组扁平化(flatten)
思路:将多维数组转换为一维数组 。
function flatten(arr, depth = Infinity) {
// 方法1:递归
if (depth === 0) return arr.slice();
return arr.reduce((acc, cur) => {
if (Array.isArray(cur)) {
acc.push(...flatten(cur, depth - 1));
} else {
acc.push(cur);
}
return acc;
}, []);
// 方法2:ES6 flat
return arr.flat(depth);
// 方法3:while + some
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}19. 手写数组 map 方法
思路:对数组每个元素执行回调,返回新数组 。
Array.prototype.myMap = function(callback, thisArg) {
if (this === null || this === undefined) {
throw new TypeError('Cannot read property \'map\' of null or undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
const arr = Object(this);
const len = arr.length >>> 0;
const result = new Array(len);
for (let i = 0; i < len; i++) {
if (i in arr) {
result[i] = callback.call(thisArg, arr[i], i, arr);
}
}
return result;
};20. 手写数组 filter 方法
思路:返回满足回调条件的元素组成的新数组 。
Array.prototype.myFilter = function(callback, thisArg) {
if (this === null || this === undefined) {
throw new TypeError('Cannot read property \'filter\' of null or undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
const arr = Object(this);
const len = arr.length >>> 0;
const result = [];
for (let i = 0; i < len; i++) {
if (i in arr) {
if (callback.call(thisArg, arr[i], i, arr)) {
result.push(arr[i]);
}
}
}
return result;
};21. 手写数组 reduce 方法
思路:累加器,对数组每个元素执行回调,返回累积结果 。
Array.prototype.myReduce = function(callback, initialValue) {
if (this === null || this === undefined) {
throw new TypeError('Cannot read property \'reduce\' of null or undefined');
}
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
const arr = Object(this);
const len = arr.length >>> 0;
let accumulator = initialValue;
let startIndex = 0;
if (arguments.length < 2) {
// 没有传入初始值,取数组第一个有效元素
while (startIndex < len && !(startIndex in arr)) {
startIndex++;
}
if (startIndex >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = arr[startIndex++];
}
for (let i = startIndex; i < len; i++) {
if (i in arr) {
accumulator = callback(accumulator, arr[i], i, arr);
}
}
return accumulator;
};22. 手写数组 flatMap 方法
思路:map 之后 flatten 一层。
Array.prototype.myFlatMap = function(callback, thisArg) {
const arr = this;
return arr.map(callback, thisArg).reduce((acc, cur) => acc.concat(cur), []);
};23. 手写函数柯里化(curry)
思路:将接受多个参数的函数转换为接受单一参数的函数序列 。
function curry(func, arity = func.length) {
return function curried(...args) {
if (args.length >= arity) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
// 使用示例
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 624. 手写组合函数(compose)
思路:从右到左组合多个函数 。
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
// 使用示例
const add1 = x => x + 1;
const double = x => x * 2;
const add1ThenDouble = compose(double, add1);
console.log(add1ThenDouble(3)); // (3+1)*2 = 825. 手写管道函数(pipe)
思路:从左到右组合多个函数。
function pipe(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => b(a(...args)));
}五、数据处理与格式化
26. 手写 JSON.stringify
思路:将 JavaScript 对象转换为 JSON 字符串 。
function myStringify(obj) {
const type = typeof obj;
if (type === 'string') return `"${obj}"`;
if (type === 'number' || type === 'boolean' || obj === null) return String(obj);
if (type === 'undefined' || type === 'function' || type === 'symbol') return undefined;
if (Array.isArray(obj)) {
const arr = obj.map(item => myStringify(item) || 'null');
return `[${arr.join(',')}]`;
}
if (type === 'object') {
const keys = Object.keys(obj).filter(key => myStringify(obj[key]) !== undefined);
const items = keys.map(key => `"${key}":${myStringify(obj[key])}`);
return `{${items.join(',')}}`;
}
}27. 手写 JSON.parse
思路:将 JSON 字符串解析为 JavaScript 对象 。可以使用 eval 或 Function,注意安全性。
function myParse(jsonStr) {
return (new Function('return ' + jsonStr))();
}
// 更安全的实现需要自行解析字符串,这里只做演示28. 手写 URL 参数解析(parseParams)
思路:将 URL 查询字符串解析为对象 。
function parseParams(url) {
const paramsStr = url.split('?')[1];
if (!paramsStr) return {};
return paramsStr.split('&').reduce((acc, cur) => {
const [key, value] = cur.split('=');
if (key in acc) {
acc[key] = Array.isArray(acc[key]) ? [...acc[key], value] : [acc[key], value];
} else {
acc[key] = decodeURIComponent(value || '');
}
return acc;
}, {});
}
// 处理复杂情况:数组、中文等
function parseParamsPro(url) {
const search = url.split('?')[1] || '';
return search.split('&').reduce((acc, param) => {
if (!param) return acc;
const [key, value] = param.split('=').map(decodeURIComponent);
const match = key.match(/(.+)\[\]$/);
if (match) {
// 处理数组:a[]=1&a[]=2
const arrKey = match[1];
acc[arrKey] = acc[arrKey] ? [...acc[arrKey], value] : [value];
} else {
acc[key] = value;
}
return acc;
}, {});
}29. 手写字符串模板解析(render)
思路:实现类似 ${name} 的模板字符串替换 。
function render(template, data) {
const reg = /\$\{(\w+)\}/g;
return template.replace(reg, (match, key) => {
return data[key] !== undefined ? data[key] : '';
});
}
// 支持深层属性
function renderDeep(template, data) {
return template.replace(/\$\{([^}]+)\}/g, (match, path) => {
const props = path.split('.');
let value = data;
for (const prop of props) {
value = value?.[prop];
if (value === undefined) break;
}
return value !== undefined ? value : '';
});
}
// 使用示例
const tpl = '我的名字是${name},年龄${age}岁';
console.log(render(tpl, { name: '张三', age: 18 })); // 我的名字是张三,年龄18岁30. 手写千分位分隔符(toThousands)
思路:将数字每隔三位加一个逗号 。
function toThousands(num) {
// 方法1:正则
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// 方法2:循环
const parts = num.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
// 方法3:toLocaleString
return num.toLocaleString('en-US');
}
// 支持负数和小数
function formatNumber(num) {
const [int, decimal] = num.toString().split('.');
const formattedInt = int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return decimal ? `${formattedInt}.${decimal}` : formattedInt;
}31. 手写大数相加(bigNumberAdd)
思路:处理超出 Number 安全范围的整数相加 。
function bigNumberAdd(a, b) {
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
let result = [];
while (i >= 0 || j >= 0 || carry) {
const x = i >= 0 ? parseInt(a[i--]) : 0;
const y = j >= 0 ? parseInt(b[j--]) : 0;
const sum = x + y + carry;
result.unshift(sum % 10);
carry = Math.floor(sum / 10);
}
return result.join('');
}
// 支持小数
function bigDecimalAdd(a, b) {
const [int1, dec1 = ''] = a.split('.');
const [int2, dec2 = ''] = b.split('.');
const maxDecLen = Math.max(dec1.length, dec2.length);
const dec1Padded = dec1.padEnd(maxDecLen, '0');
const dec2Padded = dec2.padEnd(maxDecLen, '0');
// 小数部分相加
let decSum = bigNumberAdd(dec1Padded, dec2Padded);
const decCarry = decSum.length > maxDecLen ? 1 : 0;
if (decCarry) {
decSum = decSum.slice(1);
}
// 整数部分相加
const intSum = bigNumberAdd(int1, int2) + (decCarry ? 1 : 0);
return `${intSum}.${decSum}`;
}32. 手写列表转树结构(listToTree)
思路:将扁平数组(有 id 和 pid)转换为树形结构 。
function listToTree(list, idKey = 'id', parentKey = 'parentId', childrenKey = 'children') {
const map = {};
const roots = [];
// 先建立 id 到节点的映射
list.forEach(item => {
map[item[idKey]] = { ...item, [childrenKey]: [] };
});
// 建立父子关系
list.forEach(item => {
const node = map[item[idKey]];
const parentId = item[parentKey];
if (parentId === null || parentId === undefined || parentId === 0) {
roots.push(node);
} else {
const parent = map[parentId];
if (parent) {
parent[childrenKey].push(node);
}
}
});
return roots;
}
// 支持动态 id 类型
function listToTreePro(list, idKey = 'id', parentKey = 'parentId') {
const map = new Map(list.map(item => [item[idKey], { ...item, children: [] }]));
const tree = [];
for (const item of map.values()) {
const parent = map.get(item[parentKey]);
if (parent) {
parent.children.push(item);
} else {
tree.push(item);
}
}
return tree;
}33. 手写树转列表结构(treeToList)
思路:将树形结构展开为扁平数组 。
function treeToList(tree, childrenKey = 'children') {
const result = [];
const stack = [...tree];
while (stack.length) {
const node = stack.pop();
const children = node[childrenKey];
if (children) {
stack.push(...children);
}
const { [childrenKey]: _, ...rest } = node;
result.push(rest);
}
return result;
}
// 递归版本(保留顺序)
function treeToListRecursive(tree, childrenKey = 'children') {
return tree.reduce((acc, node) => {
const children = node[childrenKey] || [];
const { [childrenKey]: _, ...rest } = node;
return [...acc, rest, ...treeToListRecursive(children, childrenKey)];
}, []);
}34. 手写深比较(isEqual)
思路:比较两个对象是否相等(深度比较)。
function isEqual(a, b, visited = new Set()) {
// 处理相同引用
if (a === b) return true;
// 处理基本类型
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') {
return a === b;
}
// 防止循环引用
if (visited.has(a) || visited.has(b)) return false;
visited.add(a);
visited.add(b);
// 比较构造函数
if (a.constructor !== b.constructor) return false;
// 比较数组
if (Array.isArray(a)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!isEqual(a[i], b[i], visited)) return false;
}
return true;
}
// 比较对象
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
if (!isEqual(a[key], b[key], visited)) return false;
}
return true;
}六、设计模式
35. 手写发布订阅(EventEmitter)
思路:实现事件的 on、off、emit、once 方法 。
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
return this;
}
off(event, callback) {
if (!this.events[event]) return this;
if (!callback) {
delete this.events[event];
} else {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
return this;
}
emit(event, ...args) {
if (!this.events[event]) return false;
this.events[event].forEach(callback => {
callback.apply(this, args);
});
return true;
}
once(event, callback) {
const onceWrapper = (...args) => {
callback.apply(this, args);
this.off(event, onceWrapper);
};
this.on(event, onceWrapper);
return this;
}
}
// 使用示例
const emitter = new EventEmitter();
emitter.on('test', (data) => console.log('test:', data));
emitter.emit('test', 123);36. 手写观察者模式(Observer)
思路:被观察者维护观察者列表,状态变化时通知观察者 。
// 被观察者
class Subject {
constructor() {
this.observers = [];
this.state = null;
}
getState() {
return this.state;
}
setState(state) {
this.state = state;
this.notifyAll();
}
attach(observer) {
this.observers.push(observer);
}
detach(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notifyAll() {
this.observers.forEach(observer => observer.update(this));
}
}
// 观察者
class Observer {
constructor(name) {
this.name = name;
}
update(subject) {
console.log(`${this.name} 收到通知,新状态为:${subject.getState()}`);
}
}
// 使用示例
const subject = new Subject();
const observer1 = new Observer('观察者1');
const observer2 = new Observer('观察者2');
subject.attach(observer1);
subject.attach(observer2);
subject.setState('开心');37. 手写单例模式(Singleton)
思路:确保一个类只有一个实例 。
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
this.data = [];
Singleton.instance = this;
}
}
// 使用闭包实现
const Singleton = (function() {
let instance = null;
function createInstance() {
return { data: [] };
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// 使用示例
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true38. 手写惰性单例
思路:在需要时才创建实例。
function getSingleton(fn) {
let instance = null;
return function(...args) {
return instance || (instance = fn.apply(this, args));
};
}
// 使用示例
const createModal = getSingleton(() => {
const div = document.createElement('div');
div.innerHTML = '我是一个弹窗';
div.style.display = 'none';
document.body.appendChild(div);
return div;
});
document.getElementById('btn').onclick = () => {
const modal = createModal();
modal.style.display = 'block';
};七、原理模拟
39. 手写简易 MVVM(双向绑定)
思路:基于 Object.defineProperty 实现数据劫持和发布订阅 。
class Vue {
constructor(options) {
this.$el = document.querySelector(options.el);
this.$data = options.data || {};
this._directives = [];
this.observe(this.$data);
this.compile(this.$el);
}
observe(data) {
Object.keys(data).forEach(key => {
let value = data[key];
const directives = [];
Object.defineProperty(data, key, {
get() {
return value;
},
set(newVal) {
if (newVal === value) return;
value = newVal;
directives.forEach(directive => directive.update());
}
});
this._directives.push({ key, directives });
});
}
compile(el) {
const nodes = el.children;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.children.length) {
this.compile(node);
}
if (node.hasAttribute('v-model')) {
const attr = node.getAttribute('v-model');
node.addEventListener('input', () => {
this.$data[attr] = node.value;
});
const directive = {
update: () => {
node.value = this.$data[attr];
}
};
directive.update();
this._directives.find(d => d.key === attr).directives.push(directive);
}
if (node.hasAttribute('v-bind')) {
const attr = node.getAttribute('v-bind');
const directive = {
update: () => {
node.innerText = this.$data[attr];
}
};
directive.update();
this._directives.find(d => d.key === attr).directives.push(directive);
}
}
}
}40. 手写 JSONP
思路:利用 script 标签实现跨域请求 。
function jsonp({ url, params = {}, callbackKey = 'callback', timeout = 60000 }) {
return new Promise((resolve, reject) => {
// 生成唯一的回调函数名
const callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2)}`;
// 组装参数
const query = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
// 创建 script 标签
const script = document.createElement('script');
script.src = `${url}?${query}&${callbackKey}=${callbackName}`;
// 定义回调函数
window[callbackName] = (data) => {
resolve(data);
clearTimeout(timer);
document.body.removeChild(script);
delete window[callbackName];
};
// 超时处理
const timer = setTimeout(() => {
reject(new Error('JSONP request timeout'));
document.body.removeChild(script);
delete window[callbackName];
}, timeout);
// 错误处理
script.onerror = () => {
reject(new Error('JSONP request failed'));
clearTimeout(timer);
document.body.removeChild(script);
delete window[callbackName];
};
// 插入 script 标签
document.body.appendChild(script);
});
}
// 使用示例
jsonp({
url: 'https://api.example.com/data',
params: { id: 123 },
callbackKey: 'callback'
}).then(data => console.log(data));41. 手写 Ajax
思路:使用 XMLHttpRequest 封装 AJAX 请求 。
function ajax(options) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const method = options.method?.toUpperCase() || 'GET';
const url = options.url;
const data = options.data || null;
const headers = options.headers || {};
const timeout = options.timeout || 30000;
const withCredentials = options.withCredentials || false;
xhr.open(method, url, true);
xhr.timeout = timeout;
xhr.withCredentials = withCredentials;
// 设置请求头
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
status: xhr.status,
statusText: xhr.statusText,
data: xhr.response,
headers: xhr.getAllResponseHeaders()
});
} else {
reject(new Error(`Request failed with status ${xhr.status}`));
}
};
xhr.onerror = () => {
reject(new Error('Network error'));
};
xhr.ontimeout = () => {
reject(new Error('Request timeout'));
};
xhr.send(data);
});
}
// 使用示例
ajax({
url: '/api/user',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({ name: '张三' })
}).then(res => console.log(res));42. 手写 setTimeout 模拟 setInterval
思路:使用递归 setTimeout 实现 interval 。
function mySetInterval(fn, delay, ...args) {
let timer = null;
const loop = () => {
timer = setTimeout(() => {
fn.apply(this, args);
loop();
}, delay);
};
loop();
return {
clear: () => clearTimeout(timer)
};
}
// 增强版:支持动态调整间隔
function createInterval(fn, delay, ...args) {
let timer = null;
let running = true;
const loop = () => {
if (!running) return;
timer = setTimeout(() => {
fn.apply(this, args);
loop();
}, delay);
};
loop();
return {
clear: () => {
running = false;
clearTimeout(timer);
},
setDelay: (newDelay) => {
delay = newDelay;
}
};
}43. 手写 setInterval 模拟 setTimeout
思路:使用 setInterval 并在执行后清除 。
function mySetTimeout(fn, delay, ...args) {
const timer = setInterval(() => {
clearInterval(timer);
fn.apply(this, args);
}, delay);
return timer;
}
// 或者使用 requestAnimationFrame
function rafTimeout(fn, delay) {
const start = performance.now();
let frame;
const loop = (now) => {
if (now - start >= delay) {
fn();
cancelAnimationFrame(frame);
return;
}
frame = requestAnimationFrame(loop);
};
frame = requestAnimationFrame(loop);
return () => cancelAnimationFrame(frame);
}44. 手写 sleep 函数
思路:实现延迟执行 。
// 基于 Promise
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 基于 async/await 使用
async function test() {
console.log('开始');
await sleep(2000);
console.log('2秒后');
}
// 同步方式(不推荐,会阻塞线程)
function sleepSync(ms) {
const start = Date.now();
while (Date.now() - start < ms) {}
}八、算法类手写题
45. 手写 LRU 缓存
思路:使用 Map 或 哈希表 + 双向链表实现 。
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const value = this.cache.get(key);
// 更新访问顺序:先删除再添加
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 删除最久未使用的(Map 的第一个 key)
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
}46. 手写版本号排序
思路:对版本号数组进行排序 。
function versionSort(versions) {
return versions.sort((a, b) => {
const aParts = a.split('.').map(Number);
const bParts = b.split('.').map(Number);
const maxLen = Math.max(aParts.length, bParts.length);
for (let i = 0; i < maxLen; i++) {
const aNum = aParts[i] || 0;
const bNum = bParts[i] || 0;
if (aNum !== bNum) return aNum - bNum;
}
return 0;
});
}
// 使用示例
const versions = ['1.0.2', '1.0.1', '2.0', '1.1', '1.0.1-beta'];
console.log(versionSort(versions));47. 手写数组乱序(shuffle)
思路:实现 Fisher-Yates 洗牌算法 。
function shuffle(arr) {
const result = [...arr];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
// 使用示例
console.log(shuffle([1, 2, 3, 4, 5]));48. 手写获取字符串中出现最多的字符及次数
思路:统计字符频率,找出最大值 。
function getMaxChar(str) {
const map = {};
let maxChar = '';
let maxCount = 0;
for (const char of str) {
map[char] = (map[char] || 0) + 1;
if (map[char] > maxCount) {
maxCount = map[char];
maxChar = char;
}
}
return { char: maxChar, count: maxCount };
}
// 正则版本
function getMaxCharRegex(str) {
const sorted = str.split('').sort().join('');
const matches = sorted.match(/(\w)\1*/g);
const max = matches.reduce((max, cur) => cur.length > max.length ? cur : max, '');
return { char: max[0], count: max.length };
}49. 手写二分查找
思路:在有序数组中查找目标值 。
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
// 查找第一个等于 target 的位置
function binarySearchFirst(arr, target) {
let left = 0;
let right = arr.length - 1;
let result = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
if (arr[mid] === target) result = mid;
}
return result;
}50. 手写快速排序
思路:分治思想,原地排序 。
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left >= right) return;
const pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
return arr;
}
function partition(arr, left, right) {
const pivot = arr[left];
let i = left;
let j = right;
while (i < j) {
while (i < j && arr[j] >= pivot) j--;
arr[i] = arr[j];
while (i < j && arr[i] <= pivot) i++;
arr[j] = arr[i];
}
arr[i] = pivot;
return i;
}刷题建议
- 基础必会:防抖节流、call/apply/bind、深拷贝、Promise 系列
- 重点掌握:数组方法实现、发布订阅、LRU、JSONP
- 进阶挑战:Promise A+ 完整实现、MVVM、并发控制
- 练习技巧:先理解原理,再手写实现,注意边界条件