4175 字
21 分钟
常见手写面试题

前端手写面试题汇总#

本文整理了前端面试中常见的手写题,涵盖了 JavaScript 核心概念和常用工具函数的实现。

目录#

  1. Promise 相关
    • Promise.all
    • Promise.race
  2. 防抖与节流
    • debounce (防抖)
    • throttle (节流)
  3. 深拷贝
  4. 发布订阅模式
  5. call、apply、bind
  6. instanceof 和 new
  7. 函数柯里化
  8. 异步任务调度器
  9. 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 点击2
emitter.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 或 window
greet(); // undefined
// 作为对象方法调用:this 指向 obj
obj.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); // true
myInstanceof(p, Object); // true
myInstanceof(p, Array); // false

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

new 的四个步骤:

  1. 创建新对象
  2. 设置原型
  3. 执行构造函数
  4. 返回对象

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)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6
// 柯里化的复用性
const add1 = curriedAdd(1);
const add1and2 = add1(2);
console.log(add1and2(3)); // 6
console.log(add1and2(5)); // 8

7.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)); // 返回 3
console.log(lruCache.get(4)); // 返回 4

核心原理:

  • Map 会记住键的插入顺序
  • 删除后重新插入,相当于把元素移到最后
  • 第一个元素永远是最久未使用的

总结#

本文整理了前端面试中最常见的手写题,涵盖了:

  1. 异步编程:Promise.all、Promise.race、异步调度器
  2. 性能优化:防抖、节流
  3. 数据处理:深拷贝、LRU 缓存
  4. 设计模式:发布订阅模式
  5. 函数式编程:柯里化
  6. 原型链相关:call/apply/bind、instanceof、new

这些题目不仅考察代码实现能力,更重要的是理解背后的原理和应用场景。建议在掌握实现的同时,深入理解:

  • 原理:为什么要这样实现?
  • 场景:什么时候用?
  • 优化:有什么改进空间?
  • 扩展:还有哪些相关知识点?

希望这份总结能帮助你在面试中更有信心!

常见手写面试题
https://fuwari.vercel.app/posts/handwritten-question/
作者
Lorem Ipsum
发布于
2025-10-29
许可协议
CC BY-NC-SA 4.0