第 6 卷
57// 测试示例 58// 1. 基本深拷贝测试(嵌套对象)
59const obj1 = { 60name: '深拷贝',
61user: { age: 20 }, 62hobbies: ['篮球', '游戏'],
63birth: new Date('2000-01-01'), 64reg: /test/g
65};
66const obj2 = deepClone(obj1);
67obj2.user.age = 21;
68obj2.hobbies.push('编程');
69obj2.birth.setFullYear(2001);
| 70 | console.log(obj1.user.age); // 20(不影响原对象) |
|---|---|
| 71 | console.log(obj1.hobbies); // ['篮球', '游戏'](不影响) |
| 72 | console.log(obj1.birth.getFullYear()); // 2000(不影响) |
| 73 | console.log(obj2.reg.flags); // 'g'(RegExp拷贝成功) |
74 75// 2. 循环引用测试(obj.a = obj)
76const obj3 = { a: 1 };
| 77 | obj3.b = obj3; //循环引用:obj3.b指向自身 |
|---|---|
| 78 | const obj4 = deepClone(obj3); |
| 79 | console.log(obj4.a); // 1 |
| 80 | console.log(obj4.b === obj4); // true(拷贝后仍保持循环引用,且不报错) |
81 82// 3. Symbol属性测试
83const sym = Symbol('test');
84const obj5 = { [sym]: 'symbol值' };
85const obj6 = deepClone(obj5);
86console.log(obj6[sym]); // 'symbol值'(Symbol属性拷贝成功)
87
补充总结(新增5道面试题)
- 防抖(debounce):延迟执行,频繁触发时只执行最后一次,核心是清除定时器重新计时,支持
立即执行和取消
- 节流(throttle):控制执行频率,指定时间内只执行一次,结合时间戳和定时器实现完整效果,
支持取消
- Vue响应式(Vue2):核心是数据劫持(Object.defineProperty)+依赖收集(Dep)+观察者
(Watcher),递归劫持对象属性,数据变化时触发视图更新
十一、手写事件总线(EventBus)的代码实现
核心原理
- 事件总线是一种发布-订阅模式的实现,用于组件/模块间的解耦通信,核心是维护一个事件注册表
- 事件注册表:以事件名为键,对应的值是一个数组,存储该事件的所有订阅回调函数
- 核心方法:
- on:订阅事件,向注册表中添加回调函数
- emit:发布事件,触发对应事件的所有回调函数并传递参数
- off:取消订阅,从注册表中移除指定回调函数(若未指定则移除该事件所有回调)
- once:订阅一次性事件,回调函数执行一次后自动取消订阅
- 需要处理边界情况:重复订阅同一回调、取消不存在的事件/回调、触发未订阅的事件
实现代码
Code block
1class EventBus { 2constructor() {
| 3 | //初始化事件注册表:key-事件名,value-回调函数数组 |
|---|---|
| 4 | this.events = {}; |
| 5 | } |
6 7/**
8* 订阅事件
| 9 | * @param {string} eventName -事件名 |
|---|---|
| 10 | * @param {Function} callback -订阅回调 |
| 11 | */ |
| 12 | on(eventName, callback) { |
| 13 | //校验参数:事件名必须是字符串,回调必须是函数 |
| 14 | if (typeof eventName !== 'string' || typeof callback !== 'function') { |
| 15 | throw new Error('事件名必须为字符串,回调必须为函数'); |
| 16 | } |
| 17 | //若事件不存在,初始化回调数组 |
| 18 | if (!this.events[eventName]) { |
| 19 | this.events[eventName] = []; |
| 21 | //避免重复添加同一回调 |
| 22 | if (!this.events[eventName].includes(callback)) { |
| 29 | * @param {string} eventName -事件名 |
|---|---|
| 30 | * @param {...any} args -传递给回调的参数 |
| 31 | */ |
| 32 | emit(eventName, ...args) { |
| 33 | if (typeof eventName !== 'string') { |
| 34 | throw new Error('事件名必须为字符串'); |
| 35 | } |
| 36 | //若事件不存在,直接返回 |
| 37 | if (!this.events[eventName]) return; |
| 38 | //触发所有回调,绑定this为EventBus实例(可选,便于回调内调用其他方法) |
| 39 | this.events[eventName].forEach(callback => { |
| 40 | callback.apply(this, args); |
| 41 | }); |
| 42 | } |
43 44/**
45* 取消订阅 46* @param {string} eventName - 事件名
| 47 | * @param {Function} [callback] -可选,要取消的回调;未传递则移除该事件所有回调 |
|---|---|
| 48 | */ |
| 49 | off(eventName, callback) { |
| 50 | if (typeof eventName !== 'string') { |
| 51 | throw new Error('事件名必须为字符串'); |
| 52 | } |
| 53 | //事件不存在,直接返回 |
| 54 | if (!this.events[eventName]) return; |
| 55 | //未指定回调:移除该事件所有订阅 |
| 56 | if (!callback) { |
| 57 | this.events[eventName] = []; |
| 58 | return; |
| 59 | } |
| 60 | //指定回调:只移除该回调(过滤掉目标回调) |
| 61 | if (typeof callback === 'function') { |
| 62 | this.events[eventName] = this.events[eventName].filter(cb => cb |
!== callback);
63} 64}
65 66/**
| 67 | *订阅一次性事件(执行一次后自动取消) |
|---|---|
| 68 | * @param {string} eventName -事件名 |
| 69 | * @param {Function} callback -订阅回调 |