前端核心进阶:从原理到手写Promise、防抖节流与深拷贝
“在面试和实际开发中,我多次被Promise的实现原理、防抖节流的性能优化和深拷贝的边界条件所困扰。本文通过手写实现这三个核心功能,帮助大家从根源上理解JavaScript的异步控制、性能优化和数据处理的底层逻辑。”
一、手写Promise实现
1. Promise基本概念
Promise是异步编程的一种解决方案,比传统的回调函数更合理和强大。它有三种状态:
- pending(进行中)
- fulfilled(已成功)
- rejected(已失败)
2. Promise基础实现
class MyPromise {constructor(executor) {// 初始状态为pendingthis.state = 'pending';// 存储成功的结果值this.value = undefined;// 存储失败的原因this.reason = undefined;// 成功回调队列(用于处理异步情况)this.onFulfilledCallbacks = [];// 失败回调队列(用于处理异步情况)this.onRejectedCallbacks = [];// resolve函数:将状态从pending变为fulfilledconst resolve = (value) => {// 只有pending状态可以改变if (this.state === 'pending') {this.state = 'fulfilled';this.value = value;// 执行所有成功回调this.onFulfilledCallbacks.forEach(fn => fn());}};// reject函数:将状态从pending变为rejectedconst 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执行出错,直接rejectreject(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) => {// 如果当前状态已经是fulfilledif (this.state === 'fulfilled') {// 使用setTimeout确保异步执行setTimeout(() => {try {// 执行成功回调const x = onFulfilled(this.value);// 处理返回值(可能是普通值或Promise)resolvePromise(promise2, x, resolve, reject);} catch (e) {// 如果回调执行出错,直接rejectreject(e);}}, 0);} // 如果当前状态已经是rejectedelse 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是Promiseif (typeof then === 'function') {then.call(x,// resolve回调y => {if (called) return;called = true;// 递归解析,直到返回值不是PromiseresolvePromise(promise2, y, resolve, reject);},// reject回调r => {if (called) return;called = true;reject(r);});} else {// 普通对象,直接resolveresolve(x);}} catch (e) {if (called) return;called = true;reject(e);}} else {// 普通值,直接resolveresolve(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; // 保存thisconst 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. 防抖与节流对比表格
特性 | 防抖(debounce) | 节流(throttle) |
---|---|---|
原理 | 事件触发后延迟执行,期间重复触发则重新计时 | 固定时间内只执行一次 |
适用场景 | 输入框搜索联想、窗口resize | 滚动加载、鼠标移动、频繁点击 |
执行频率 | 停止触发后才执行一次 | 固定频率执行 |
实现方式 | setTimeout | 时间戳或setTimeout |
效果 | 将多次密集触发合并为一次执行 | 稀释执行频率,保持一定节奏执行 |
三、深拷贝(deep clone)实现
1. 基础深拷贝实现(带注释)
/*** 深拷贝函数* @param {*} target 要拷贝的目标* @param {WeakMap} map 用于解决循环引用的WeakMap* @return {*} 返回深拷贝后的对象*/
function deepClone(target, map = new WeakMap()) {// 1. 处理基本数据类型和nullif (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. 处理基本数据类型和nullif (typeof target !== 'object' || target === null) {return target;}// 2. 解决循环引用问题if (map.get(target)) {return map.get(target);}// 3. 获取构造函数const constructor = target.constructor;// 4. 处理特殊对象类型// 4.1 处理Functionif (constructor === Function) {// 通过函数字符串创建新函数return new Function('return ' + target.toString())();}// 4.2 处理RegExpif (constructor === RegExp) {return new RegExp(target);}// 4.3 处理Dateif (constructor === Date) {return new Date(target);}// 4.4 处理Mapif (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 处理Setif (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. 深拷贝与浅拷贝对比
特性 | 深拷贝(deep clone) | 浅拷贝(shallow clone) |
---|---|---|
拷贝层级 | 所有层级 | 仅第一层 |
引用类型 | 创建新的引用 | 复制引用 |
修改影响 | 不影响原对象 | 修改拷贝对象会影响原对象 |
实现方式 | 递归或序列化/反序列化 | Object.assign()或扩展运算符 |
性能 | 较低(需要递归处理) | 较高 |
适用场景 | 需要完全独立的对象副本 | 只需要浅层拷贝,性能要求高 |
四、总结
-
手写Promise:
- 理解Promise的状态机制
- 掌握then方法的链式调用原理
- 学会处理异步和同步的不同情况
-
防抖与节流:
- 防抖适合高频触发但只需最后一次结果的场景
- 节流适合需要均匀执行高频触发的场景
- 两者都可以有效优化性能
-
深拷贝:
- 理解JavaScript中的值类型和引用类型
- 掌握循环引用的处理方法
- 学会处理各种特殊对象类型
这些知识点是前端进阶的重要内容,理解它们的实现原理可以帮助你写出更高效、更健壮的代码。建议在学习时:
- 先理解原理和概念
- 然后手动实现代码
- 最后思考各种边界情况和优化方案