【每日五题系列】前端面试高频题目
6. 括号匹配算法
要求:编写函数判断字符串中的括号是否有效(如"({[]})"
有效,"([)]"
无效),使用栈结构实现。
7. 函数柯里化(Currying)
要求:实现一个通用柯里化函数,支持curry(add)(1)(2)(3)
调用形式,其中add
接收多个参数求和。
8. 数组扁平化(Flatten)
要求:将多维数组展开为一维数组(如[1, [2, [3]]]
转为[1,2,3]
),用递归、reduce
或ES6的flat
方法实现。
9. React/Vue的虚拟DOM对比
要求:手写一个简化版虚拟DOM的Diff算法,比较两棵树的不同并输出差异(考察节点复用与Key的作用)。
10. 实现一个简易的EventEmitter
要求:实现事件订阅(on
)、触发(emit
)、取消订阅(off
)功能,支持异步事件队列。
发布订阅
class EventEmitter {
constructor() {
this.events = {}; // 存储事件名与回调数组的映射,如 { event1: [fn1, fn2] } [[3,17]]
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback); // 将回调函数加入数组 [[3,17]]
}
// 触发事件(支持异步队列)
emit(eventName, ...args) {
const callbacks = this.events[eventName] || [];
// 异步执行所有回调 [[2,18]]
callbacks.forEach(callback => {
setTimeout(() => {
callback.apply(this, args); // 传递参数并绑定 this
}, 0);
});
}
// 取消订阅
off(eventName, callback) {
const callbacks = this.events[eventName] || [];
// 过滤掉与指定回调相同的项 [[8,9]]
this.events[eventName] = callbacks.filter(fn => fn !== callback);
}
}
// 创建一个简单的发布订阅者管理类
class EventEmitter {
constructor() {
this.events = {}; // 用于存储事件的字典,键为事件名,值为订阅者数组
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = []; // 如果该事件尚未被订阅,则初始化一个空数组
}
this.events[eventName].push(callback); // 将回调函数添加到订阅者数组中
return this; // 支持链式调用
}
// 取消订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
// 移除指定的回调函数
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
// 如果订阅者数组为空,则删除该事件
if (this.events[eventName].length === 0) {
delete this.events[eventName];
}
}
return this; // 支持链式调用
}
// 发布事件
emit(eventName, ...args) {
if (this.events[eventName]) {
// 遍历订阅者数组,并执行每个回调函数
this.events[eventName].forEach(callback => {
callback.apply(this, args);
});
}
return this; // 支持链式调用
}
// 可选:监听一次后自动取消订阅
once(eventName, callback) {
const onceCallback = (...args) => {
callback.apply(this, args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
return this; // 支持链式调用
}
}
// 使用示例
const eventBus = new EventEmitter();
// 订阅者1
function subscriber1(price) {
console.log('订阅者1收到消息:当前价格已降至' + price + '元');
}
// 订阅者2
function subscriber2(price) {
console.log('订阅者2也收到消息:价格更新为' + price + '元');
}
// 订阅事件
eventBus.on('priceUpdate', subscriber1);
eventBus.on('priceUpdate', subscriber2);
// 使用once方法监听一次
eventBus.once('specialOffer', (offer) => {
console.log('只接收一次的特别优惠:' + offer);
});
// 发布事件
eventBus.emit('priceUpdate', 99); // 订阅者1和订阅者2都会收到消息
eventBus.emit('specialOffer', '买一赠一'); // 只有一个订阅者会收到这个特别优惠的消息
// 取消订阅
eventBus.off('priceUpdate', subscriber1);
eventBus.emit('priceUpdate', 88); // 只有订阅者2会收到消息
观察者
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => {
observer.update(data);
});
}
}
class Observer {
update(data) {
console.log(`Observer received: ${data}`);
}
}
// 使用...
考察重点
• 代码健壮性:处理边界条件(如空值、异常输入);
• 性能优化:避免不必要的计算或内存泄漏;
• 原理理解:解释实现思路与底层机制(如闭包、原型链、事件循环)。
提示:以上题目可根据候选人经验调整难度(如增加异步流程、复杂数据结构处理)。建议要求候选人现场编码,并逐步追问优化方案(如防抖节流的时间参数动态化、深拷贝的循环引用检测等)。
第六个括号匹配
,用栈结构。遇到左括号入栈,右括号时检查栈顶是否匹配。最后栈应该为空。需要处理各种括号类型,确保顺序正确。
第七个函数柯里化
,支持多参数调用。柯里化函数需要递归或闭包保存参数,直到参数数量足够执行原函数。比如curry(add)(1)(2)(3)需要累积参数,当参数数目满足时调用add求和。
第八个数组扁平化
,可以用递归、reduce或flat方法。递归的话遍历数组,将元素展开;reduce方法累加结果;flat方法最简单但可能有兼容性问题。需要比较不同方法的实现。
第九个虚拟DOM的Diff算法
。简化版的话,可以比较节点的类型和属性,处理子节点的差异。Key的作用是复用节点,减少DOM操作。需要手写一个简单的Diff函数,输出差异。
第十个EventEmitter
,实现on、emit、off。使用对象存储事件和回调数组,emit时触发所有回调,支持异步可能需要用Promise或setTimeout。要注意取消订阅时从数组中移除回调,避免内存泄漏。
6. 括号匹配算法(栈结构)
实现
function isValidBrackets(str) {
const stack = [];
const map = { '(': ')', '[': ']', '{': '}' };
for (const char of str) {
if (map[char]) {
stack.push(char);
} else if (Object.values(map).includes(char)) {
if (stack.length === 0 || map[stack.pop()] !== char) return false;
}
}
return stack.length === 0;
}
原理
• 左括号入栈,右括号匹配栈顶元素
栈与队列
7. 函数柯里化
通用柯里化函数
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return (...args2) => curried.apply(this, args.concat(args2));
}
};
}
// 示例:const add = (...nums) => nums.reduce((a, b) => a + b, 0);
调用形式
curry(add)(1)(2)(3)()
输出 6
柯里化函数
8. 数组扁平化
方法 1:递归
function flatten(arr) {
return arr.reduce((acc, cur) =>
acc.concat(Array.isArray(cur) ? flatten(cur) : cur), []);
}
方法 2:ES6 flat
const flatten = arr => arr.flat(Infinity);
方法 3:扩展运算符
function flatten(arr) {
while (arr.some(Array.isArray)) {
arr = [].concat(...arr);
}
return arr;
}
9. 虚拟 DOM Diff 算法(简化版)
实现差异对比
function diff(oldNode, newNode) {
const patches = [];
if (oldNode.type !== newNode.type) {
patches.push({ type: 'REPLACE', newNode });
} else if (oldNode.props !== newNode.props) {
patches.push({ type: 'UPDATE_PROPS', props: newNode.props });
}
// 递归对比子节点(需处理 Key 优化复用)
return patches;
}
Key 的作用
• 识别节点身份,减少不必要的重新渲染
10. 简易 EventEmitter
实现
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
(this.events[event] ||= []).push(callback);
}
emit(event, ...args) {
(this.events[event] || []).forEach(cb => cb(...args));
}
off(event, callback) {
this.events[event] = this.events[event]?.filter(cb => cb !== callback);
}
}
异步队列支持
• 在 emit
中使用 setTimeout
或 Promise.resolve()
包裹回调