前端手写面试题汇总
本文整理了前端面试中常见的手写题,涵盖了 JavaScript 核心概念和常用工具函数的实现。
目录
- Promise 相关
- Promise.all
- Promise.race
- 防抖与节流
- debounce (防抖)
- throttle (节流)
- 深拷贝
- 发布订阅模式
- call、apply、bind
- instanceof 和 new
- 函数柯里化
- 异步任务调度器
- LRU 缓存
1. Promise 相关
1.1 Promise.all
题目描述: 实现 Promise.all 方法,接收一个 Promise 数组,当所有 Promise 成功时返回结果数组,任一失败则立即返回失败原因。
核心要点:
- 返回结果顺序与传入顺序一致
- 使用计数器判断是否全部完成
- 快速失败机制
- 处理非 Promise 值
代码实现:
function MyPromiseAll(promises) { return new Promise((resolve, reject) => { // 参数校验 if (!Array.isArray(promises)) { reject(new TypeError("arguments must be an array")); return; }
// 空数组直接返回 if (promises.length === 0) { resolve([]); return; }
const result = []; let count = 0;
promises.forEach((promise, index) => { // 使用 Promise.resolve 包装,处理非 Promise 值 Promise.resolve(promise) .then((value) => { result[index] = value; // 保证顺序 count++; if (count === promises.length) { resolve(result); } }) .catch(reject); // 快速失败 }); });}使用示例:
const p1 = Promise.resolve(1);const p2 = new Promise((resolve) => setTimeout(() => resolve(2), 1000));const p3 = Promise.resolve(3);
MyPromiseAll([p1, p2, p3]).then((results) => { console.log(results); // [1, 2, 3]});1.2 Promise.race
题目描述: 实现 Promise.race 方法,返回最先完成(resolve 或 reject)的 Promise 结果。
核心要点:
- 第一个完成的 Promise 决定结果
- Promise 状态只能改变一次
- 空数组返回永远 pending 的 Promise
代码实现:
function MyPromiseRace(promises) { return new Promise((resolve, reject) => { if (!Array.isArray(promises)) { reject(new TypeError("arguments must be an array")); return; }
// 空数组:返回永远 pending 的 Promise if (promises.length === 0) { return; }
promises.forEach((promise) => { Promise.resolve(promise) .then(resolve) // 第一个完成的会改变状态 .catch(reject); // 后续的会被忽略 }); });}使用示例:
const p1 = new Promise((resolve) => setTimeout(() => resolve(1), 1000));const p2 = new Promise((resolve) => setTimeout(() => resolve(2), 500));
MyPromiseRace([p1, p2]).then((result) => { console.log(result); // 2 (500ms 后输出)});2. 防抖与节流
2.1 debounce (防抖)
题目描述: 实现防抖函数,在事件触发 n 秒后执行回调,若 n 秒内再次触发则重新计时。
核心要点:
- 支持立即执行模式(immediate)
- 支持取消功能(cancel)
- 非 immediate 模式返回 Promise
- 正确绑定 this 和传递参数
代码实现:
function debounce(func, wait, immediate = false) { let timer = null;
const debounced = function (...args) { const callNow = immediate && !timer;
if (timer) clearTimeout(timer);
// immediate 模式:立即执行并返回结果 if (callNow) { const result = func.apply(this, args); timer = setTimeout(() => { timer = null; }, wait); return result; }
// 非 immediate 模式:返回 Promise return new Promise((resolve) => { timer = setTimeout(() => { timer = null; const result = func.apply(this, args); resolve(result); }, wait); }); };
// 添加 cancel 方法 debounced.cancel = function () { if (timer) { clearTimeout(timer); timer = null; } };
return debounced;}使用示例:
// 搜索框输入const searchInput = debounce((value) => { console.log('搜索:', value);}, 500);
// 用户输入searchInput('a'); // 不执行searchInput('ab'); // 不执行searchInput('abc'); // 500ms 后执行
// 取消执行const debouncedFn = debounce(() => console.log('执行'), 1000);debouncedFn();debouncedFn.cancel(); // 取消执行应用场景:
- 搜索框输入
- 窗口 resize
- 表单验证
2.2 throttle (节流)
题目描述: 实现节流函数,持续触发事件时保证一定时间内只执行一次。
核心要点:
- 时间戳 + 定时器混合实现
- 支持 leading 和 trailing 模式
- 支持取消功能
- 处理系统时间修改的边界情况
代码实现:
function throttle(func, wait, options = {}) { let timer = null; let previous = 0;
const { leading = true, trailing = true } = options;
const throttled = function (...args) { const now = Date.now();
// 如果不需要首次立即执行,设置 previous 为当前时间 if (!leading && !previous) { previous = now; }
const remaining = wait - (now - previous);
// 如果距离上次执行已超过 wait,可以执行 if (remaining <= 0 || remaining > wait) { if (timer) { clearTimeout(timer); timer = null; }
previous = now; func.apply(this, args); } // 如果需要尾部执行,且还没设置定时器 else if (trailing && !timer) { timer = setTimeout(() => { previous = leading ? Date.now() : 0; timer = null; func.apply(this, args); }, remaining); } };
// 添加 cancel 方法 throttled.cancel = function () { if (timer) { clearTimeout(timer); timer = null; } previous = 0; };
return throttled;}使用示例:
// 滚动事件const handleScroll = throttle(() => { console.log('滚动位置:', window.scrollY);}, 200);
window.addEventListener('scroll', handleScroll);
// 不同模式const fn1 = throttle(log, 1000); // 首尾都执行const fn2 = throttle(log, 1000, { trailing: false }); // 只在开始执行const fn3 = throttle(log, 1000, { leading: false }); // 只在结束执行应用场景:
- 滚动事件
- 鼠标移动
- 窗口 resize
- 按钮点击
防抖 vs 节流:
| 特性 | 防抖 | 节流 |
|---|---|---|
| 执行时机 | 停止触发后 n 秒执行 | 每 n 秒最多执行一次 |
| 形象比喻 | 电梯关门(有人进就重新计时) | 技能 CD(冷却时间内无法释放) |
| 典型场景 | 搜索框输入、窗口 resize | 滚动事件、鼠标移动 |
3. 深拷贝
题目描述: 实现深拷贝函数,能够正确处理各种数据类型和循环引用。
核心要点:
- 处理基本类型、对象、数组
- 处理特殊对象:Date、RegExp、Map、Set
- 使用 WeakMap 解决循环引用
- 处理 Symbol 作为 key
- 处理函数
代码实现:
function deepClone(obj, map = new WeakMap()) { // 基本类型直接返回 if (obj === null || typeof obj !== 'object') { return obj; }
// 函数直接返回引用 if (typeof obj === 'function') { return obj; }
// 检查循环引用 if (map.has(obj)) { return map.get(obj); }
// Date if (obj instanceof Date) { return new Date(obj); }
// RegExp if (obj instanceof RegExp) { const result = new RegExp(obj.source, obj.flags); result.lastIndex = obj.lastIndex; return result; }
// Set if (obj instanceof Set) { const newSet = new Set(); map.set(obj, newSet); // 先设置,防止循环引用 obj.forEach((item) => { newSet.add(deepClone(item, map)); }); return newSet; }
// Map if (obj instanceof Map) { const newMap = new Map(); map.set(obj, newMap); obj.forEach((value, key) => { newMap.set(deepClone(key, map), deepClone(value, map)); }); return newMap; }
// Array 和 Object const newObj = Array.isArray(obj) ? [] : {}; map.set(obj, newObj);
// 使用 Reflect.ownKeys 同时处理 String 和 Symbol 类型的 key Reflect.ownKeys(obj).forEach(key => { newObj[key] = deepClone(obj[key], map); });
return newObj;}使用示例:
// 测试循环引用const obj = { name: 'Alice' };obj.self = obj;const cloned = deepClone(obj);console.log(cloned.self === cloned); // true
// 测试特殊对象const original = { date: new Date(), reg: /test/gi, set: new Set([1, 2, 3]), map: new Map([['key', 'value']]), [Symbol('id')]: 123};const cloned2 = deepClone(original);关键知识点:
- WeakMap 不会阻止垃圾回收
Reflect.ownKeys可以获取 Symbol 类型的 key- Set/Map 需要先创建并记录映射,再遍历内容
4. 发布订阅模式
题目描述: 实现 EventEmitter 类,支持事件的发布订阅功能。
核心要点:
- on: 订阅事件
- emit: 触发事件
- off: 取消订阅
- once: 一次性订阅
- 支持链式调用
- 错误处理
代码实现:
class EventEmitter { constructor() { this.events = new Map(); }
on(event, listener) { if (!this.events.has(event)) { this.events.set(event, []); } this.events.get(event).push(listener); return this; // 支持链式调用 }
emit(event, ...args) { if (this.events.has(event)) { const listeners = this.events.get(event); // 错误处理:某个监听器出错不影响其他监听器 listeners.forEach((listener) => { try { listener(...args); } catch (error) { console.error(`Error in listener for event "${event}":`, error); } }); } return this; }
off(event, listener) { if (this.events.has(event)) { const listeners = this.events.get(event); const index = listeners.indexOf(listener); if (index !== -1) { listeners.splice(index, 1); } // 如果没有监听器了,删除该事件 if (listeners.length === 0) { this.events.delete(event); } } return this; }
once(event, listener) { const onceWrapper = (...args) => { this.off(event, onceWrapper); listener(...args); }; this.on(event, onceWrapper); return this; }
// 取消某个事件的所有监听器,或取消所有事件 allOff(event) { if (event) { this.events.delete(event); } else { this.events.clear(); } return this; }}使用示例:
const emitter = new EventEmitter();
// 订阅事件emitter.on('login', (username) => { console.log(`${username} 登录了`);});
// 链式调用emitter .on('click', () => console.log('点击1')) .on('click', () => console.log('点击2')) .once('load', () => console.log('只执行一次'));
// 触发事件emitter.emit('login', 'Alice'); // Alice 登录了emitter.emit('click'); // 点击1 点击2emitter.emit('load'); // 只执行一次emitter.emit('load'); // 不输出
// 取消订阅emitter.allOff('click'); // 删除 click 的所有监听器5. call、apply、bind
5.1 Function.prototype.myCall
题目描述: 实现 call 方法,改变函数的 this 指向并立即执行。
核心要点:
- 改变 this 指向
- 立即执行并返回结果
- 处理 context 为 null/undefined
- 使用 Symbol 避免属性冲突
代码实现:
Function.prototype.myCall = function (context, ...args) { // 1. 处理 context 为 null 或 undefined const ctx = context ?? globalThis;
// 2. 使用 Symbol 作为属性名,避免冲突和污染 const fn = Symbol('fn'); ctx[fn] = this; // this 就是要执行的函数
// 3. 执行函数 const result = ctx[fn](...args);
// 4. 删除添加的属性 delete ctx[fn];
// 5. 返回执行结果 return result;};核心原理:
// 原理:对象方法调用时,this 指向该对象const obj = { name: 'Alice' };function greet() { console.log(this.name);}
// 直接调用:this 是 undefined 或 windowgreet(); // undefined
// 作为对象方法调用:this 指向 objobj.greet = greet;obj.greet(); // 'Alice'5.2 Function.prototype.myApply
题目描述: 实现 apply 方法,与 call 类似,但参数以数组形式传入。
代码实现:
Function.prototype.myApply = function (context, args = []) { const ctx = context || globalThis; const fn = Symbol('fn'); ctx[fn] = this; const result = ctx[fn](...args); // 展开数组参数 delete ctx[fn]; return result;};call vs apply:
| 方法 | 参数形式 | 示例 |
|---|---|---|
| call | 逐个传递 | fn.call(obj, 1, 2, 3) |
| apply | 数组传递 | fn.apply(obj, [1, 2, 3]) |
5.3 Function.prototype.myBind
题目描述: 实现 bind 方法,返回一个绑定了 this 的新函数,支持柯里化和 new 操作符。
核心要点:
- 返回新函数,不立即执行
- 支持柯里化(分步传参)
- 支持 new 操作符
- 维护原型链
代码实现:
Function.prototype.myBind = function (context, ...args) { const fn = this; // 保存原函数
const boundFn = function (...newArgs) { // 判断是否用 new 调用 // 如果是,this 指向实例,忽略 context // 如果不是,this 使用传入的 context return fn.apply( this instanceof boundFn ? this : context, [...args, ...newArgs] ); };
// 维护原型链,让 instanceof 正常工作 if (fn.prototype) { boundFn.prototype = Object.create(fn.prototype); }
return boundFn;};使用示例:
function Person(name, age) { this.name = name; this.age = age;}
const obj = { name: 'Bob' };
// 普通调用 + 柯里化const boundPerson = Person.myBind(obj, 'Alice');boundPerson(25);console.log(obj.name); // 'Alice'
// new 调用const BoundPerson = Person.myBind({ ignored: true }, 'Charlie');const p = new BoundPerson(30);console.log(p.name); // 'Charlie'console.log(p instanceof Person); // true关键知识点:
this instanceof boundFn判断是否用 new 调用- new 调用时,bind 的 context 会被忽略
6. instanceof 和 new
6.1 myInstanceof
题目描述: 实现 instanceof 操作符,判断对象是否是某个构造函数的实例。
核心要点:
- 沿着原型链查找
- 处理边界情况
- 原型链的终点是 null
代码实现:
function myInstanceof(obj, construct) { // 处理基本类型�� null if (obj === null || typeof obj !== 'object') return false;
let proto = Object.getPrototypeOf(obj);
while (proto) { if (proto === construct.prototype) return true; proto = Object.getPrototypeOf(proto); }
return false;}原型链示例:
function Person() {}const p = new Person();
// 原型链:// p.__proto__ === Person.prototype// Person.prototype.__proto__ === Object.prototype// Object.prototype.__proto__ === null
myInstanceof(p, Person); // truemyInstanceof(p, Object); // truemyInstanceof(p, Array); // false6.2 myNew
题目描述: 实现 new 操作符的功能。
核心要点:
- 创建新对象并设置原型
- 执行构造函数绑定 this
- 处理构造函数的返回值
- 如果返回对象则使用该对象,否则返回新对象
代码实现:
function myNew(fn, ...args) { // 1. 创建新对象,原型指向构造函数的 prototype const obj = Object.create(fn.prototype);
// 2. 执行构造函数,this 绑定到新对象 const result = fn.apply(obj, args);
// 3&4. 判断构造函数返回值 return result instanceof Object ? result : obj;}使用示例:
function Person(name, age) { this.name = name; this.age = age;}
Person.prototype.sayHi = function() { return `Hi, I'm ${this.name}`;};
const person = myNew(Person, 'Bob', 25);console.log(person.name); // 'Bob'console.log(person.sayHi()); // "Hi, I'm Bob"console.log(person instanceof Person); // truenew 的四个步骤:
- 创建新对象
- 设置原型
- 执行构造函数
- 返回对象
7. 函数柯里化
7.1 基础版 curry
题目描述: 实现函数柯里化,将多参数函数转换为单参数的嵌套函数。
核心要点:
- 支持多次调用收集参数
- 参数够了自动执行
- 递归调用实现无限层嵌套
代码实现:
function curry(fn) { return function curried(...args) { // 参数够了,执行原函数 if (args.length >= fn.length) { return fn.apply(this, args); } // 参数不够,返回新函数继续收集 return curried.bind(this, ...args); };}使用示例:
function add(a, b, c) { return a + b + c;}
const curriedAdd = curry(add);
// 多种调用方式console.log(curriedAdd(1)(2)(3)); // 6console.log(curriedAdd(1, 2)(3)); // 6console.log(curriedAdd(1)(2, 3)); // 6console.log(curriedAdd(1, 2, 3)); // 6
// 柯里化的复用性const add1 = curriedAdd(1);const add1and2 = add1(2);console.log(add1and2(3)); // 6console.log(add1and2(5)); // 87.2 带占位符的 curry
题目描述: 实现支持占位符的柯里化,允许跳过某些参数位置。
核心要点:
- 使用 Symbol 定义占位符
- 合并参数时优先填充占位符
- 完整性判断需检查是否有占位符
代码实现:
function curry(fn) { // 定义占位符 const placeholder = Symbol('placeholder'); curry.placeholder = placeholder;
return function curried(...args) { // 检查参数是否完整 const isComplete = (args) => { return args.length >= fn.length && !args.slice(0, fn.length).includes(placeholder); };
// 合并参数:新参数填充旧参数中的占位符 const mergeArgs = (oldArgs, newArgs) => { const result = [...oldArgs]; let newArgIndex = 0;
// 遍历旧参数,用新参数替换占位符 for (let i = 0; i < result.length; i++) { if (result[i] === placeholder && newArgIndex < newArgs.length) { result[i] = newArgs[newArgIndex++]; } }
// 剩余的新参数追加到末尾 while (newArgIndex < newArgs.length) { result.push(newArgs[newArgIndex++]); }
return result; };
// 参数完整,执行原函数 if (isComplete(args)) { return fn.apply(this, args); }
// 参数不完整,返回新函数继续收集 return function (...newArgs) { const mergedArgs = mergeArgs(args, newArgs); return curried.apply(this, mergedArgs); }; };}使用示例:
function test(a, b, c, d) { return `${a}-${b}-${c}-${d}`;}
const curriedTest = curry(test);const _ = curry.placeholder;
// 占位符在开头console.log(curriedTest(_, 2, 3, 4)(1)); // "1-2-3-4"
// 占位符在中间console.log(curriedTest(1, _, 3, 4)(2)); // "1-2-3-4"
// 多个占位符console.log(curriedTest(_, 2, _, 4)(1, 3)); // "1-2-3-4"
// 分步填充console.log(curriedTest(_, _, _, 4)(1)(2, 3)); // "1-2-3-4"应用场景:
// HTTP 请求封装const request = curry((method, url, headers, data) => { return fetch(url, { method, headers, body: JSON.stringify(data) });});
const _ = curry.placeholder;const apiCall = request(_, '/api/users', { 'Content-Type': 'application/json' });
const getUsers = apiCall('GET', null);const createUser = apiCall('POST', { name: 'Alice' });8. 异步任务调度器
题目描述: 实现一个 Scheduler 类,控制异步任务的并发执行数量。
核心要点:
- 维护当前执行任务计数
- 超过限制时加入队列等待
- 任务完成后自动执行下一个
- 返回 Promise 支持异步结果
代码实现:
class Scheduler { constructor(max) { this.max = max; // 最大并发数 this.count = 0; // 当前执行任务数 this.queue = []; // 等待队列 }
async add(promiseCreator) { // 如果当前执行数量超过限制,加入队列等待 if (this.count >= this.max) { await new Promise((resolve) => { this.queue.push(resolve); }); }
// 执行任务 this.count++; try { const result = await promiseCreator(); return result; } finally { this.count--; // 执行队列中的下一个任务 if (this.queue.length > 0) { const resolve = this.queue.shift(); resolve(); } } }}使用示例:
const scheduler = new Scheduler(2); // 最多同时执行 2 个任务
const timeout = (time) => new Promise((resolve) => setTimeout(resolve, time));
const addTask = (time, order) => { scheduler.add(() => timeout(time).then(() => console.log(order)));};
addTask(1000, '1');addTask(500, '2');addTask(300, '3');addTask(400, '4');
// 输出顺序:// 500ms 后输出: 2// 800ms 后输出: 3// 1000ms 后输出: 1// 1200ms 后输出: 4执行时间线:
时间轴: 0ms 500ms 800ms 1000ms 1200ms任务1: |----------1-----------|任务2: |----2----|任务3: |--3--|任务4: |---4---| ↑ ↑ ↑ ↑ ↑并发数: 2 1 2 1 0应用场景:
- 控制 HTTP 请求并发数
- 限制文件上传/下载的并发数
- 爬虫并发控制
- 图片懒加载
9. LRU 缓存
题目描述: 实现 LRU(Least Recently Used)缓存淘汰算法。
核心要点:
- 使用 Map 保持插入顺序
- get 时更新访问顺序(删除再插入)
- put 时检查容量,满了删除最久未使用的
- 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) { // 如果 key 存在,先删除 if (this.cache.has(key)) { this.cache.delete(key); } // 如果已满,删除最久未使用的(第一个) else if (this.cache.size >= this.capacity) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } // 插入新数据 this.cache.set(key, value); }}使用示例:
const lruCache = new LRUCache(2);
lruCache.put(1, 1); // cache: {1=1}lruCache.put(2, 2); // cache: {1=1, 2=2}console.log(lruCache.get(1)); // 返回 1, cache: {2=2, 1=1}lruCache.put(3, 3); // 淘汰 key 2, cache: {1=1, 3=3}console.log(lruCache.get(2)); // 返回 -1 (未找到)lruCache.put(4, 4); // 淘汰 key 1, cache: {3=3, 4=4}console.log(lruCache.get(1)); // 返回 -1 (未找到)console.log(lruCache.get(3)); // 返回 3console.log(lruCache.get(4)); // 返回 4核心原理:
- Map 会记住键的插入顺序
- 删除后重新插入,相当于把元素移到最后
- 第一个元素永远是最久未使用的
总结
本文整理了前端面试中最常见的手写题,涵盖了:
- 异步编程:Promise.all、Promise.race、异步调度器
- 性能优化:防抖、节流
- 数据处理:深拷贝、LRU 缓存
- 设计模式:发布订阅模式
- 函数式编程:柯里化
- 原型链相关:call/apply/bind、instanceof、new
这些题目不仅考察代码实现能力,更重要的是理解背后的原理和应用场景。建议在掌握实现的同时,深入理解:
- 原理:为什么要这样实现?
- 场景:什么时候用?
- 优化:有什么改进空间?
- 扩展:还有哪些相关知识点?
希望这份总结能帮助你在面试中更有信心!