第一章:JavaScript手写题终极清单(含高频考题+参考实现+评分标准)
JavaScript 手写题是前端面试中的核心考察环节,重点评估候选人对语言机制的理解深度与编码规范。掌握常见题目及其高质量实现,有助于在技术评审中脱颖而出。
实现一个防抖函数 debounce
防抖广泛应用于搜索框、窗口 resize 等高频触发场景,确保函数在连续调用中仅执行最后一次。
function debounce(fn, delay) {
let timer = null; // 闭包保存定时器
return function (...args) {
const context = this; // 保持 this 指向
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args); // 延迟执行原函数
}, delay);
};
}
使用方式:
const debouncedFn = debounce(myFunction, 300);,每次调用将重置计时。
实现一个节流函数 throttle
节流控制函数在指定时间间隔内最多执行一次,适用于滚动事件等性能敏感场景。
function throttle(fn, delay) {
let lastExecTime = 0;
return function (...args) {
const context = this;
const now = Date.now();
if (now - lastExecTime > delay) {
fn.apply(context, args);
lastExecTime = now;
}
};
}
常见手写题分类与评分标准
- 代码正确性:逻辑无误,边界处理完整(如 null、异步)
- this 指向处理:正确使用 apply/call 绑定上下文
- 闭包与内存:合理利用闭包,避免内存泄漏
- 扩展能力:支持立即执行、取消功能等加分项
| 题目类型 | 出现频率 | 建议掌握度 |
|---|
| 防抖/节流 | 高频 | 熟练手写 + 场景说明 |
| 深拷贝 | 高频 | 处理循环引用、函数、Symbol |
| Promisify | 中频 | 理解回调转 Promise 原理 |
第二章:基础语法与核心概念手写实现
2.1 数据类型判断与深浅拷贝实现
在JavaScript中,准确判断数据类型是实现拷贝操作的前提。常用方法包括
typeof、
instanceof 和
Object.prototype.toString.call(),其中后者能精确识别内置对象类型。
常见数据类型判断对比
| 值 | typeof | toString结果 |
|---|
| [] | "object" | "[object Array]" |
| {} | "object" | "[object Object]" |
| null | "object" | "[object Null]" |
浅拷贝与深拷贝实现
浅拷贝仅复制对象第一层属性,深层仍引用原值:
const shallowCopy = obj => Object.assign({}, obj);
// 或使用扩展运算符
const copy = { ...obj };
该方法适用于无嵌套引用的简单对象。
深拷贝需递归复制所有层级,避免引用共享:
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj); // 防止循环引用
const clone = Array.isArray(obj) ? [] : {};
seen.set(obj, clone);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], seen);
}
}
return clone;
}
该实现通过
WeakMap 跟踪已访问对象,确保复杂结构(如循环引用)也能安全复制。
2.2 函数柯里化与参数重载编码实践
函数柯里化的基本实现
柯里化是将多参数函数转换为一系列单参数函数的技术,提升函数的复用性与组合能力。
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
上述代码中,
curry 函数通过判断已传参数数量与目标函数期望参数数量(
fn.length)的关系,决定是否继续返回新函数。当参数足够时执行原函数。
参数重载的模拟策略
JavaScript 不支持传统重载,可通过参数类型判断实现逻辑分支:
- 检查
arguments 长度或参数类型 - 使用默认值与解构赋值增强灵活性
- 结合柯里化实现“伪重载”效果
2.3 手写call、apply、bind方法并分析差异
手写call方法
Function.prototype.myCall = function(context, ...args) {
context = context || window;
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
const result = context[fnSymbol](...args);
delete context[fnSymbol];
return result;
};
通过将函数作为上下文对象的临时方法调用,实现this绑定。使用Symbol避免属性冲突,执行后立即删除。
手写apply方法
Function.prototype.myApply = function(context, argsArray) {
context = context || window;
const fnSymbol = Symbol('fn');
context[fnSymbol] = this;
let result;
if (argsArray) {
result = context[fnSymbol](...argsArray);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
与call类似,区别在于第二个参数为数组形式传参。
手写bind方法
Function.prototype.myBind = function(context, ...bindArgs) {
const fn = this;
const boundFn = function(...callArgs) {
return fn.apply(
this instanceof boundFn ? this : context,
bindArgs.concat(callArgs)
);
};
boundFn.prototype = Object.create(this.prototype);
return boundFn;
};
bind返回一个绑定this和预置参数的新函数,并支持new调用时的原型继承。
三者核心差异对比
| 方法 | 立即执行 | 参数形式 | 可否延迟调用 |
|---|
| call | 是 | 逐个参数 | 否 |
| apply | 是 | 参数数组 | 否 |
| bind | 否 | 可预设参数 | 是 |
2.4 实现new操作符与instanceof原理
模拟实现 new 操作符
new 操作符用于创建一个用户自定义类型的实例。其核心逻辑可分解为四步:
- 创建一个新对象;
- 将构造函数的原型赋给新对象的隐式原型;
- 执行构造函数,this 指向新对象;
- 若构造函数返回对象,则返回该对象,否则返回新对象。
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}
上述代码中,Object.create 绑定原型链,apply 执行构造函数并绑定 this,最后判断返回值类型。
instanceof 的底层机制
instanceof 通过原型链向上查找,判断构造函数的 prototype 是否出现在对象的原型链中。
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left);
while (proto) {
if (proto === right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
该实现模拟了原型链遍历过程,确保类型判断准确可靠。
2.5 模拟实现JSON.stringify与JSON.parse
简易版JSON.stringify实现
function jsonStringify(obj) {
if (obj === null) return "null";
if (typeof obj === "string") return `"${obj}"`;
if (typeof obj !== "object") return String(obj);
if (Array.isArray(obj)) {
return `[${obj.map(jsonStringify).join(",")}]`;
}
const keys = Object.keys(obj);
const pairs = keys.map(key => `"${key}":${jsonStringify(obj[key])}`);
return `{${pairs.join(",")}}`;
}
该函数递归处理基本类型、数组和对象,字符串添加引号,对象键名强制转为字符串。
简易版JSON.parse实现
- 利用JavaScript引擎的原生能力:通过
new Function构造函数解析字符串 - 安全性较低,仅用于理解原理
function jsonParse(str) {
return new Function(`return ${str}`)();
}
该实现将JSON字符串包裹在return语句中,通过动态函数执行返回对应JS值。
第三章:异步编程与事件循环高频题解析
3.1 手写Promise核心功能(then、catch、finally)
实现一个简易Promise,需定义三种状态:
PENDING、
FULFILLED、
REJECTED,并通过回调队列处理异步逻辑。
核心结构设计
class MyPromise {
constructor(executor) {
this.status = 'PENDING';
this.value = null;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === 'PENDING') {
this.status = 'FULFILLED';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.status === 'PENDING') {
this.status = 'REJECTED';
this.value = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
}
上述代码中,
executor 立即执行,
resolve 和
reject 控制状态迁移,确保状态不可逆。
链式调用支持
then 方法返回新Promise,实现链式调用。支持异步回调注册与值穿透,是Promise核心机制之一。
3.2 实现Promise.all与Promise.race容错机制
在并发控制中,
Promise.all 和
Promise.race 是常用工具,但默认行为不具备容错性。为增强稳定性,需手动实现错误处理机制。
Promise.all 容错封装
Promise.allSettled = function(promises) {
return Promise.all(promises.map(p =>
p.then(value => ({ status: 'fulfilled', value }))
.catch(reason => ({ status: 'rejected', reason }))
));
};
该实现将所有 Promise 的结果统一包装为 {status, value/reason} 格式,确保即使部分失败也不会中断整体执行。
Promise.race 超时控制
- 通过包装超时逻辑,防止长期挂起
- 利用
Promise.race 实现优先返回最快结果
const withTimeout = (promise, ms) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
};
此模式广泛应用于网络请求超时控制,提升系统响应确定性。
3.3 基于发布订阅模式实现EventEmitter
在事件驱动架构中,发布订阅模式是核心通信机制。通过构建一个轻量级的 EventEmitter 类,可以实现对象间解耦的事件通知。
核心API设计
主要包含三个方法:`on` 订阅事件、`emit` 发布事件、`off` 取消订阅。
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(cb => cb(...args));
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
}
上述代码中,`events` 对象以事件名为键存储回调数组;`on` 添加监听器,`emit` 触发对应事件的所有回调,`off` 移除指定监听函数,确保内存可回收。
应用场景
- 组件间通信(如UI组件与状态管理)
- 异步任务结果通知
- 日志系统事件广播
第四章:设计模式与DOM操作类手写题
4.1 单例模式与观察者模式的JavaScript实现
在前端架构设计中,单例模式确保一个类仅有一个实例,并提供全局访问点。适用于状态管理、配置中心等场景。
单例模式实现
class Singleton {
constructor() {
if (Singleton.instance) return Singleton.instance;
Singleton.instance = this;
this.data = 'shared state';
}
}
// 使用:const instance1 = new Singleton(); const instance2 = new Singleton();
上述代码通过静态属性跟踪实例状态,若已存在则返回原实例,保证唯一性。
观察者模式实现
该模式建立一对多依赖关系,当主体状态变化时,所有观察者自动更新。
- Subject(主题):维护观察者列表,提供订阅与通知接口
- Observer(观察者):实现更新方法,响应主题通知
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
调用 notify 时遍历执行每个观察者的 update 方法,实现松耦合通信机制。
4.2 防抖与节流函数的高级写法与应用场景
防抖函数的高级实现
防抖(Debounce)确保在事件频繁触发时,只执行最后一次操作。适用于搜索框输入、窗口调整等场景。
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
该实现利用闭包保存定时器,每次调用重置延迟。参数 `func` 为原函数,`wait` 为延迟毫秒数。
节流函数的时间戳控制
节流(Throttle)保证函数在指定时间间隔内最多执行一次,常用于滚动监听。
- 使用时间戳方式判断是否达到执行周期
- 相比定时器方案,响应更及时
4.3 手写模板引擎与虚拟DOM生成逻辑
实现一个轻量级模板引擎,核心是将字符串模板解析为抽象语法树(AST),再转换为虚拟DOM节点。首先通过正则匹配插值表达式
{{ }} 和指令标签,构建树形结构。
模板解析流程
- 词法分析:将模板字符串拆分为标记流(tokens)
- 语法分析:根据嵌套关系生成AST节点
- 代码生成:遍历AST输出渲染函数
function parse(template) {
const tokens = template.split(/({{.*?}})/); // 分割文本与表达式
return tokens.map(token => {
if (token.startsWith('{{') && token.endsWith('}}')) {
return { type: 'expression', value: token.slice(2, -2).trim() };
}
return { type: 'text', value: token };
});
}
上述代码实现基础词法解析,将模板分解为文本和表达式节点。每种节点类型后续可映射到对应的虚拟DOM结构,为动态更新提供依据。
虚拟DOM构造
通过JavaScript对象描述真实DOM结构,提升更新性能。
| 属性 | 说明 |
|---|
| tag | 元素标签名 |
| props | 属性键值对 |
| children | 子虚拟节点数组 |
4.4 实现一个简易版MVVM双向绑定
核心原理概述
MVVM的双向绑定依赖于数据劫持与观察者模式。通过
Object.defineProperty监听数据变化,并在视图层输入时同步更新模型。
代码实现
function observe(data) {
if (typeof data !== 'object') return;
Object.keys(data).forEach(key => {
let value = data[key];
const dep = [];
observe(value); // 深度监听
Object.defineProperty(data, key, {
get() { return value; },
set(newVal) {
value = newVal;
dep.forEach(fn => fn(newVal));
}
});
});
}
上述代码递归劫持对象属性,每个属性维护一个依赖收集数组
dep,当数据变更时通知所有订阅者。
视图绑定示例
- 初始化时将数据渲染到input元素
- 绑定input事件,实时更新JS对象
- 触发setter,通知DOM重新渲染
第五章:总结与面试应对策略
掌握核心知识点的串联能力
在准备分布式系统相关面试时,候选人需具备将CAP理论、一致性算法与实际架构设计结合的能力。例如,在设计一个高可用订单系统时,可基于最终一致性模型选择Kafka进行异步数据同步。
高频问题实战解析
- 如何在Paxos和Raft之间做技术选型?关键在于运维复杂度与日志可读性。
- 面对网络分区,AP系统如何保障数据不丢失?可通过本地队列+重试机制实现。
- ZooKeeper是否适合用作服务配置中心?以下是典型使用场景示例:
// 使用etcd监听配置变更
cli, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, _ := cli.Get(ctx, "service/config")
fmt.Println("Current config:", string(resp.Kvs[0].Value))
// 监听后续变更
watchCh := cli.Watch(context.Background(), "service/config")
for wr := range watchCh {
for _, ev := range wr.Events {
fmt.Println("Config updated:", string(ev.Kv.Value))
}
}
系统设计题应答框架
| 步骤 | 操作要点 |
|---|
| 需求澄清 | 明确QPS、数据规模、一致性要求 |
| 接口设计 | 定义核心API与数据结构 |
| 架构选型 | 选择注册中心、通信协议、存储引擎 |
| 容错设计 | 加入熔断、限流、选举机制 |