JavaScript防抖与节流函数完整实现指南
本文详细解析JavaScript中防抖与节流函数的实现原理。通过从基础到完整的代码演进,涵盖this指向、参数传递、立即执行模式及取消功能。对比时间戳与定时器两种节流方案,并展示结合两者的最终实现,提供可直接复用的代码示例。
最近阅读《红楼梦》时,诗词鉴赏增多,偶尔也会产生一些随想……
近日研读underscore.js库中关于防抖和节流的章节,借此机会复习相关概念,重新梳理并整理以下知识点。
前端开发中常遇到鼠标移动、窗口滚动、键盘输入等高频操作。防抖与节流技术旨在优化此类事件,通过降低触发频率来提升页面性能。
如图所示,短短几秒内事件触发的次数非常惊人。

防抖机制
防抖的核心逻辑是:无论事件触发多少次,仅在最后一次触发后的 n 秒才执行回调。若在等待期间再次触发,则重置计时器,以新触发的时间为准重新计算。
接下来探讨如何构建一个基础的防抖函数。
基础版本实现
依据防抖原理,可设置一个定时器。每次触发事件但未到达指定时间时,清除上一个定时器并重新设置新的定时器。
const debounce = function(func, wait) {
let timeout
return function() {
// 再次触发事件则删除上一个定时器,重新设置
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}上述代码实现了最基础的防抖功能,触发频率已显著降低。

上下文(this)与参数(arguments)处理
基础版本存在缺陷,例如在setTimeout中,this的指向无法正确获取,导致setTimeout中的this指向了全局Window对象而非预期的调用者。
为解决此问题,可在执行定时器前保存并重置this:
const debounce = function(func, wait) {
let timeout
return function() {
// 保存this
let context = this // 新增
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(context) // 新增
}, wait);
}
}此外,若需在自定义函数func中传递event对象,基础实现无法正确获取参数。需进一步修改代码以支持参数传递:
const debounce = function(func, wait) {
let timeout
return function() {
let context = this // 新增
// 保存参数
let args = arguments // 新增
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(context, args) // 修改
}, wait);
}
}
至此,一个功能相对完善的防抖函数已实现。
立即执行模式
若希望事件触发时立即执行函数,随后在停止触发 n 秒后再重新允许执行,需增加此功能。
可通过传入参数immediate来控制是否立即执行。若传递immediate为真,则立即执行一次函数,并设置定时器;时间截止后将定时器状态置为null。下次进入函数时,先判断定时器状态是否为null,再决定是否执行。
const debounce = function(func, wait, immediate) {
let res, timeout, context, args;
const debounced = function() {
context = this
args = arguments
// 如果已经设置了setTimeout,则重新进行设置
if(timeout) clearTimeout(timeout)
// 判断是否为立即执行
if(immediate) {
let runNow = !timeout
// 设置定时器,指定时间后设置为null
timeout = setTimeout(function() {
timeout = null
}, wait)
// 如果timeout已经为null(已到期),则执行函数
// 保存执行结果,用于函数返回
if(runNow) res = func.apply(context, args)
} else {
// 如果没有设置立即执行,则设置定时器
timeout = setTimeout(function() {
func.apply(context, args)
}, wait)
}
return res
}
return debounced
}上述逻辑包含两种不同的触发方式,具体流程如下:
图中黑色箭头代表触发动作,红色箭头代表执行动作。

非立即执行模式

立即执行模式
当immediate为true时,执行流程如下:
首次执行:若timeout为null,则runNow设为true,随后设置定时器;指定时间后将timeout置为null,代表执行间隔时间;最后判断runNow以决定是否执行函数。
二次执行:
- 情况一:超过设置时间。若第二次触发时已超时,此时
timeout已被定时器置为null,进入debounced函数后,runNow为true,重新设置定时器并执行函数。 - 情况二:未超过设置时间。因未超时,
timeout未被置为null,故runNow为false。由于timeout的定时器已被清除,此时重置定时器,不执行函数。
当immediate为false时:
此情况与之前设置一致,未超时则重置定时器,定时器到达指定时间后自动执行一次函数。
两者最大区别在于:立即执行模式在首次触发时即执行一次,若下次触发时已到达设置时间,则直接执行;非立即执行模式首次触发仅设置定时器,时间到达后自动执行,若在设置时间内再次触发则仅重置定时器,永不立即执行。
取消功能
若需取消debounce函数,例如debounce的时间间隔为10秒,且immediate为true,则需等待10秒后才能重新触发。若具备取消功能,点击取消后再次触发即可立即执行。
debounced.cancel = function() {
// 删除定时器
clearTimeout(timeout);
// 设置timeout为null
timeout = null;
};只需清除定时器,将timeout置为null即可。因为若immediate为true会直接执行一次函数,随后重新设置定时器。
完整防抖实现
完整的防抖函数代码如下:
function debounce(func, wait, immediate) {
let res, timeout, context, args;
const debounced = function () {
context = this;
args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
var runNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (runNow) res = func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return res;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}节流机制
节流同样是减少触发执行的手段,但其思路与防抖完全不同。
持续触发事件时,每隔一段时间仅执行一次。即仅按设置的时间作为时间段,到达指定时间后触发函数即执行;未到达指定时间,无论触发多少次均不执行。
即“没到点,无论怎么触发,均不执行”。
目前主要有两种实现方式:使用时间戳和设置定时器。
时间戳实现
触发函数时,使用当前时间戳与上一次保存的时间戳相减,对比设置的时间,决定是否执行函数。
const throttle = function(func, wait) {
let previous = 0, context, args;
return function() {
context = this
args = arguments
// 获取当前时间戳
let now = +new Date()
// 判断当前时间戳与上一次触发的时间差值是否大于等于指定时间
if((now - previous) >= wait) {
func.apply(context, args)
// 更新时间戳
previous = now
}
}
}注意:在JS中,元素前使用'+'号可将其转换为Number类型;若转换失败,则得到NaN。
+new Date()将调用Date.prototype上的valueOf()方法。根据MDN文档,Date.prototype.value方法等同于Date.prototype.getTime()。
console.log(+new Date('2022-08-17'));
console.log(new Date('2022-08-17').getTime());
console.log(new Date('2022-08-17').valueOf());
console.log(new Date('2022-08-17') * 1);
// 结果都是相同的
定时器实现
定时器思路为:首次触发时设置定时器,指定时间后将变量置为null;下次触发时判断变量是否为null,以此决定执行。
const throttle = function(func, wait) {
let timeout, context, args;
return function() {
context = this
args = arguments
// 允许执行
if(!timeout) {
// 设置定时器,到达时间后设置timeout为null
timeout = setTimeout(function() {
timeout = null
func.apply(context, args)
}, wait)
}
}
}上述两种写法均可实现基本节流函数,但存在差异:
- 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行。
- 第一种事件停止触发后不再执行,第二种事件停止触发后仍会再执行一次。
既然执行行为不同,能否将两者结合?
结合实现
结合两者的目标是实现一个既能开始执行一次,又能结束时再执行一次的函数。
思路如下:若触发时未到指定时间,则设置定时器;若已到指定时间,则直接执行。
function throttle(func, wait) {
let timeout, context, args, previous = 0;
const later = function() {
// 定时器执行时更新时间戳
previous = +new Date();
timeout = null;
// 执行函数
func.apply(context, args)
};
const throttled = function() {
let now = +new Date();
//下次触发 func 剩余的时间
let remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者更改了系统时间
if (remaining <= 0 || remaining > wait) {
// 清空定时器及timeout
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
// 更新时间戳变量
previous = now;
func.apply(context, args);
} else if (!timeout) {
// 处理还没有到达指定时间的触发行为
// 此处设置定时器时间要设置剩余的时间,与上文中防抖函数中有区别
timeout = setTimeout(later, remaining);
}
};
return throttled;
}梳理逻辑如下:
首次触发throttled时,因previous为0,remaining <= 0条件成立,执行func函数,并重置定时器及变量,最后将previous更新为当前时间。
二次触发:
- 未到达指定时间:
remaining为正数,不进入remaining <= 0执行语句,而是设置定时器,不执行函数。 - 到达指定时间:
remaining为负数,执行函数,同首次触发。
定时器执行时,也会更新previous和timeout的值。
核心在于remaining变量的运算。
控制执行时机
若需控制首次和末次是否执行,可传递第三个参数。
具体参数定义如下:
leading:false表示禁用第一次执行。trailing: false表示禁用停止触发的回调。
function throttle(func, wait, options = {}) { //修改
let timeout, context, args, previous = 0;
const later = function() {
previous = options.leading === false ? 0 : +new Date(); //修改
timeout = null;
func.apply(context, args);
// 清空作用域及参数变量
if (!timeout) context = args = null; //修改
};
const throttled = function() {
let now = +new Date();
// 如果是首次触发,并且设置首次不执行函数。那么将previous与now进行同步
// now 与 previous 相减不小于0,则不会执行函数
if (!previous && options.leading === false) previous = now; // 新增
let remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
// 清空作用域及参数变量
if (!timeout) context = args = null; //修改
} else if (!timeout && options.trailing !== false) { // 修改
timeout = setTimeout(later, remaining);
}
};
return throttled;
}需注意实现中的一个问题:
leading:false和trailing: false不能同时设置。若同时设置,即既不开始也不结束时触发,函数将无法正常执行。
核心仍关于时间戳的加减法,仅根据功能需求设置时间戳。
取消功能
与防抖函数的取消功能基本相同,需重置各个作用变量:
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}完整节流实现
function throttle(func, wait, options = {}) {
let timeout, context, args, previous = 0;
const later = function() {
previous = options.leading === false ? 0 : +new Date();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};
const throttled = function() {
let now = +new Date();
if (!previous && options.leading === false) previous = now;
let remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
};
return throttled;
}这也是underscore.js中节流的实现方式。
以上为利用JavaScript实现防抖节流函数的详细内容,更多关于JavaScript防抖节流函数的资料请关注本站其它相关文章!
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
