前端核心进阶:从原理到手写Promise、防抖节流与深拷贝
“在面试和实际开发中,我多次被Promise的实现原理、防抖节流的性能优化和深拷贝的边界条件所困扰。本文通过手写实现这三个核心功能,帮助大家从根源上理解JavaScript的异步控制、性能优化和数据处理的底层逻辑。”
一、手写Promise实现
1. Promise基本概念
Promise是异步编程的一种解决方案,比传统的回调函数更合理和强大。它有三种状态:
- pending(进行中)
- fulfilled(已成功)
- rejected(已失败)
2. Promise基础实现
class MyPromise { constructor(executor) { // 初始状态为pending this.state = \'pending\'; // 存储成功的结果值 this.value = undefined; // 存储失败的原因 this.reason = undefined; // 成功回调队列(用于处理异步情况) this.onFulfilledCallbacks = []; // 失败回调队列(用于处理异步情况) this.onRejectedCallbacks = []; // resolve函数:将状态从pending变为fulfilled const resolve = (value) => { // 只有pending状态可以改变 if (this.state === \'pending\') { this.state = \'fulfilled\'; this.value = value; // 执行所有成功回调 this.onFulfilledCallbacks.forEach(fn => fn()); } }; // reject函数:将状态从pending变为rejected const reject = (reason) => { // 只有pending状态可以改变 if (this.state === \'pending\') { this.state = \'rejected\'; this.reason = reason; // 执行所有失败回调 this.onRejectedCallbacks.forEach(fn => fn()); } }; // 立即执行executor函数 try { executor(resolve, reject); } catch (err) { // 如果executor执行出错,直接reject reject(err); } } // then方法:注册回调函数 then(onFulfilled, onRejected) { // 参数可选处理:如果不是函数,则创建一个默认函数 onFulfilled = typeof onFulfilled === \'function\' ? onFulfilled : value => value; onRejected = typeof onRejected === \'function\' ? onRejected : err => { throw err }; // 返回一个新的Promise,实现链式调用 const promise2 = new MyPromise((resolve, reject) => { // 如果当前状态已经是fulfilled if (this.state === \'fulfilled\') { // 使用setTimeout确保异步执行 setTimeout(() => { try { // 执行成功回调 const x = onFulfilled(this.value); // 处理返回值(可能是普通值或Promise) resolvePromise(promise2, x, resolve, reject); } catch (e) { // 如果回调执行出错,直接reject reject(e); } }, 0); } // 如果当前状态已经是rejected else if (this.state === \'rejected\') { setTimeout(() => { try { // 执行失败回调 const x = onRejected(this.reason); // 处理返回值 resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } // 如果当前状态还是pending(异步情况) else if (this.state === \'pending\') { // 将成功回调加入队列 this.onFulfilledCallbacks.push(() => { setTimeout(() => { try { const x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); // 将失败回调加入队列 this.onRejectedCallbacks.push(() => { setTimeout(() => { try { const x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); } }); return promise2; }}// 处理then方法返回值的函数function resolvePromise(promise2, x, resolve, reject) { // 如果返回的是同一个Promise,报错 if (promise2 === x) { return reject(new TypeError(\'Chaining cycle detected for promise\')); } // 防止重复调用 let called = false; // 如果x是对象或函数(可能是Promise) if (x !== null && (typeof x === \'object\' || typeof x === \'function\')) { try { // 获取then方法 const then = x.then; // 如果then是函数,则认为x是Promise if (typeof then === \'function\') { then.call( x, // resolve回调 y => { if (called) return; called = true; // 递归解析,直到返回值不是Promise resolvePromise(promise2, y, resolve, reject); }, // reject回调 r => { if (called) return; called = true; reject(r); } ); } else { // 普通对象,直接resolve resolve(x); } } catch (e) { if (called) return; called = true; reject(e); } } else { // 普通值,直接resolve resolve(x); }}
二、防抖(debounce)与节流(throttle)
1. 防抖(debounce)实现
/** * 防抖函数 * @param {Function} fn 要执行的函数 * @param {number} delay 延迟时间(ms) * @param {boolean} immediate 是否立即执行 * @return {Function} 返回防抖后的函数 */function debounce(fn, delay, immediate = false) { let timer = null; let isInvoke = false; // 是否已经立即执行过 return function(...args) { const context = this; // 保存this指向 // 如果设置立即执行且还未执行过 if (immediate && !isInvoke) { fn.apply(context, args); // 立即执行 isInvoke = true; } else { clearTimeout(timer); // 清除之前的定时器 } // 设置新的定时器 timer = setTimeout(() => { // 如果不是立即执行模式,则执行函数 if (!immediate) { fn.apply(context, args); } // 重置状态,允许下次立即执行 isInvoke = false; }, delay); };}// 使用示例const input = document.getElementById(\'search-input\');input.addEventListener(\'input\', debounce(function(e) { console.log(\'搜索:\', e.target.value); // 这里可以执行实际的搜索逻辑}, 500, true)); // 500ms防抖,立即执行第一次
防抖原理说明:
- 当事件触发时,不立即执行函数,而是设置一个定时器
- 如果在延迟时间内事件再次触发,则清除之前的定时器并重新设置
- 只有当事件停止触发且延迟时间到达后,才真正执行函数
immediate
参数控制是否在第一次触发时立即执行
2. 节流(throttle)实现(带注释)
/** * 节流函数 * @param {Function} fn 要执行的函数 * @param {number} interval 时间间隔(ms) * @param {Object} options 配置选项 * leading: 是否立即执行第一次 * trailing: 是否在间隔结束后执行最后一次 * @return {Function} 返回节流后的函数 */function throttle(fn, interval, options = { leading: true, trailing: true }) { let lastTime = 0; // 上次执行时间 let timer = null; // 定时器 return function(...args) { const context = this; // 保存this const nowTime = Date.now(); // 当前时间 // 如果不需要立即执行且是第一次触发 if (!lastTime && !options.leading) { lastTime = nowTime; // 将lastTime设为当前时间 } // 计算剩余时间 const remainTime = interval - (nowTime - lastTime); if (remainTime <= 0) { // 如果剩余时间<=0,应该执行函数 if (timer) { clearTimeout(timer); timer = null; } fn.apply(context, args); // 执行函数 lastTime = nowTime; // 更新上次执行时间 } else if (options.trailing && !timer) { // 如果需要执行最后一次且没有定时器 timer = setTimeout(() => { fn.apply(context, args); // 如果不需要立即执行,lastTime设为0,否则设为当前时间 lastTime = !options.leading ? 0 : Date.now(); timer = null; }, remainTime); } };}// 使用示例window.addEventListener(\'scroll\', throttle(function() { console.log(\'滚动事件处理\'); // 这里可以执行实际的滚动处理逻辑}, 1000, { leading: true, trailing: true }));
节流原理说明:
- 节流函数会按照固定的时间间隔执行函数
- 有两种实现方式:
- 时间戳方式:通过比较当前时间和上次执行时间
- 定时器方式:通过设置定时器
leading
和trailing
选项可以控制:- leading: 是否在节流开始时立即执行
- trailing: 是否在节流结束后再执行一次
3. 防抖与节流对比表格
三、深拷贝(deep clone)实现
1. 基础深拷贝实现(带注释)
/** * 深拷贝函数 * @param {*} target 要拷贝的目标 * @param {WeakMap} map 用于解决循环引用的WeakMap * @return {*} 返回深拷贝后的对象 */function deepClone(target, map = new WeakMap()) { // 1. 处理基本数据类型和null if (typeof target !== \'object\' || target === null) { return target; } // 2. 解决循环引用问题 if (map.get(target)) { return map.get(target); } // 3. 处理特殊对象类型 // 3.1 处理Date对象 if (target instanceof Date) { return new Date(target); } // 3.2 处理RegExp对象 if (target instanceof RegExp) { return new RegExp(target); } // 4. 创建新对象/数组 const cloneTarget = Array.isArray(target) ? [] : {}; // 将target和cloneTarget存入map,解决循环引用 map.set(target, cloneTarget); // 5. 处理Symbol属性 const symbolKeys = Object.getOwnPropertySymbols(target); if (symbolKeys.length) { symbolKeys.forEach(symKey => { cloneTarget[symKey] = deepClone(target[symKey], map); }); } // 6. 递归拷贝普通属性 for (const key in target) { if (target.hasOwnProperty(key)) { cloneTarget[key] = deepClone(target[key], map); } } return cloneTarget;}// 使用示例const obj = { a: 1, b: { c: 2 }, d: new Date(), e: /regexp/, [Symbol(\'key\')]: \'symbol value\'};obj.self = obj; // 循环引用const clonedObj = deepClone(obj);console.log(clonedObj);
2. 处理更多数据类型的深拷贝(带注释)
function deepClone(target, map = new WeakMap()) { // 1. 处理基本数据类型和null if (typeof target !== \'object\' || target === null) { return target; } // 2. 解决循环引用问题 if (map.get(target)) { return map.get(target); } // 3. 获取构造函数 const constructor = target.constructor; // 4. 处理特殊对象类型 // 4.1 处理Function if (constructor === Function) { // 通过函数字符串创建新函数 return new Function(\'return \' + target.toString())(); } // 4.2 处理RegExp if (constructor === RegExp) { return new RegExp(target); } // 4.3 处理Date if (constructor === Date) { return new Date(target); } // 4.4 处理Map if (constructor === Map) { const newMap = new Map(); map.set(target, newMap); // 遍历原Map,递归拷贝每一项 target.forEach((value, key) => { newMap.set(deepClone(key, map), deepClone(value, map)); }); return newMap; } // 4.5 处理Set if (constructor === Set) { const newSet = new Set(); map.set(target, newSet); // 遍历原Set,递归拷贝每一项 target.forEach(value => { newSet.add(deepClone(value, map)); }); return newSet; } // 5. 处理数组和普通对象 const cloneTarget = new constructor(); map.set(target, cloneTarget); // 6. 处理Symbol属性 const symbolKeys = Object.getOwnPropertySymbols(target); if (symbolKeys.length) { symbolKeys.forEach(symKey => { cloneTarget[symKey] = deepClone(target[symKey], map); }); } // 7. 递归拷贝普通属性 for (const key in target) { if (target.hasOwnProperty(key)) { cloneTarget[key] = deepClone(target[key], map); } } return cloneTarget;}// 使用示例const complexObj = { arr: [1, 2, { a: 3 }], date: new Date(), reg: /abc/gi, map: new Map([[\'key1\', \'value1\'], [\'key2\', { b: 2 }]]), set: new Set([1, 2, 3]), func: function(a, b) { return a + b; }, [Symbol(\'sym\')]: \'symbol value\'};complexObj.self = complexObj; // 循环引用const clonedComplexObj = deepClone(complexObj);console.log(clonedComplexObj);
3. 深拷贝关键点解释
-
基本数据类型处理:
- 直接返回,因为它们是不可变的
-
循环引用处理:
- 使用WeakMap存储已拷贝对象
- 遇到相同引用直接返回存储的拷贝
-
特殊对象处理:
- Date: 创建新的Date对象
- RegExp: 创建新的RegExp对象
- Map/Set: 递归拷贝每一项
- Function: 通过函数字符串重新创建
-
Symbol属性处理:
- 使用Object.getOwnPropertySymbols()获取Symbol属性
- 递归拷贝每个Symbol属性
-
普通对象和数组处理:
- 创建新的对象或数组
- 递归拷贝每个属性
-
性能考虑:
- 对于大型对象,深拷贝可能消耗较多内存和CPU
- 实际项目中可以考虑使用immutable.js等库
4. 深拷贝与浅拷贝对比
四、总结
-
手写Promise:
- 理解Promise的状态机制
- 掌握then方法的链式调用原理
- 学会处理异步和同步的不同情况
-
防抖与节流:
- 防抖适合高频触发但只需最后一次结果的场景
- 节流适合需要均匀执行高频触发的场景
- 两者都可以有效优化性能
-
深拷贝:
- 理解JavaScript中的值类型和引用类型
- 掌握循环引用的处理方法
- 学会处理各种特殊对象类型
这些知识点是前端进阶的重要内容,理解它们的实现原理可以帮助你写出更高效、更健壮的代码。建议在学习时:
- 先理解原理和概念
- 然后手动实现代码
- 最后思考各种边界情况和优化方案