本文通过实例详解JavaScript防抖函数的实现原理。从基础定时器方案入手,逐步解决事件对象传递、arguments参数处理及this指向问题。深入解析延迟防抖与前缘防抖的区别,提供完整代码实现与关键步骤说明,帮助开发者掌握高性能事件处理技巧。
本文将通过一个具体案例,循序渐进地讲解防抖的实现过程。在理解实现方式的同时,还将深入探讨 this、apply 和 arguments 等 JavaScript 核心概念在实际开发中的具体应用。
为什么需要防抖?
在用户交互中,频繁触发某些事件往往没有实际意义。这不仅会浪费系统资源,影响页面性能,还可能导致界面卡顿,影响用户体验。
为了解决上述问题,我们需要引入防抖机制来优化事件处理逻辑。
什么是防抖?
防抖debounce:简单来说,防抖的核心效果是:在指定的时间间隔内,无论事件被触发多少次,最终只执行一次回调函数。
如何应用防抖?
首先,我们通过一个具体的实例来引入话题。
假设我们要设置一个按钮,用于切换上方文本的颜色,HTML结构如下:

<h2 id="demo">hello</h2>
<button id="btn">点我</button>
<script type="text/javascript">
var btn = document.getElementById('btn');
btn.addEventListener('click', changeColor, false);
var flag = true
function changeColor() {
if (flag) {
document.getElementById('demo').style.color = 'pink'
} else {
document.getElementById('demo').style.color = 'blue'
}
flag = !flag
}如果我们希望限制用户在一秒内只能切换一次颜色,即在一秒内多次点击只生效一次。
此时可以利用定时器来实现,但直接编写一个简单的定时器是不够的,因为它只能实现多次触发的延时执行,而无法真正限制触发的频率。
我们可以引入一个变量来标记是否已经触发过定时器。如果定时器已存在且未结束,则清除该定时器并重新创建一个新的一秒定时器;如果定时器不存在,说明一秒钟内没有操作,直接创建一个新的定时器即可。
按照上述逻辑,即可实现基础的防抖效果,具体代码实现如下:
<h2 id="demo">hello</h2>
<button id="btn">点我</button>
<script type="text/javascript">
var btn = document.getElementById('btn');
btn.addEventListener('click', debounce(changeColor), false);
var flag = true
function changeColor() {
if (flag) {
document.getElementById('demo').style.color = 'pink'
} else {
document.getElementById('demo').style.color = 'blue'
}
flag = !flag
}
function debounce(fn) {
let t = null
return function () {
//如果定时器存在就清除掉
if (t) {
clearTimeout(t)
}
//不然就创建新的定时器
t = setTimeout(function() {
fn()
}, 1000)
}
}
</script>阅读上述代码后,读者可能会产生以下疑问:
1. 为什么 debounce 函数内部不直接执行逻辑,而是要返回一个函数?
解答:因为我们将 changeColor 函数作为参数传递给了 debounce 函数(参考后续代码变化)。当在按钮上绑定 debounce 事件时,必须在 debounce 内部将改造后的 changeColor 逻辑封装为回调函数返回。
// btn.addEventListener('click', changeColor, false);
btn.addEventListener('click', debounce(changeColor), false);2. let t = null; 每次执行都会将 t 重置为 null,这如何实现防抖逻辑?
解答:你可能认为按钮绑定的是 debounce,因此每次触发都会执行 t = null。
实际上,按钮绑定的是 debounce(changeColor)!注意这里的括号至关重要。每次触发 click 事件时,执行的是返回的回调函数,而 let t = null 仅在初始化 debounce 时执行一次。
虽然上述代码实现了防抖效果,但你会发现 changeColor 函数无法获取事件对象,即它拿不到本该属于它的 event。

为了让 changeColor 能够获取到事件对象,我们可以进行如下修改:
1. 既然 event 对象已经存在于 debounce 的回调中,可以将 e 作为参数传递给 debounce 返回的回调函数。
2. 再将 e 传递给回调函数中定时器内的 fn(),即调用 fn(e)。
function debounce(fn) {
let t = null
//往这里传e
return function (e) {
if (t) {
clearTimeout(t)
}
t = setTimeout(function() {
//再带给fn
fn(e)
}, 1000)
}
}经过上述修改,changeColor 函数成功获取到了事件对象。

有人可能会问,获取这个 e 对象有什么用?
获取 e 对象后,你可以对其执行操作(例如通过 e.target 修改属性)。如果不使用 e,不传递它也可以,但这样写能使防抖函数更加完善。
然而,我们不能保证 changeColor 只有一个参数。如果只传递 e,无法实现多参数的传递。为了进一步完善,我们需要继续修改代码。
提到多参数传递,自然会想到实参列表 arguments。没错,就是它!
如果不了解 arguments,建议查阅相关文档深入学习。

arguments对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,第一个参数在索引0处。
首先,我们尝试用 arguments[0] 替换之前传递的 e,结果发现:

return 语句中的 arguments[0] 无法传递到定时器内部,更不用说传递到 changeColor 中了。
这是因为每个函数内部的 arguments 都是独立的,定时器内的函数没有自己的 arguments 对象,因此值为 undefined。
要解决此问题,可以通过赋值方式保存 arguments,或者使用箭头函数。
接下来,我们采用箭头函数的方式来解决这个问题:
由于箭头函数内部没有自己的 arguments 对象,它会向外层作用域查找,从而能够获取 return 语句中的 arguments。
function debounce(fn) {
let t = null
return function () {
console.log('我是回调的arguments',arguments[0]);
if (t) {
clearTimeout(t)
}
//这里用箭头函数
t = setTimeout(() => {
fn(arguments[0])
console.log('我是定时器里的arguments', arguments[0])
// console.log(this)
}, 1000)
}
}
这样便解决了 arguments[0] 的传递问题。如果是多个参数,可以使用 apply 方法将整个 arguments 作为参数传递过去,代码如下:
function debounce(fn) {
let t = null
return function () {
if (t) {
clearTimeout(t)
}
t = setTimeout(() => {
fn.apply(this, arguments)
}, 1000)
}
}其中,apply 方法的第一个参数还可以将 changeColor 的 this 从 Window 转为 btn,这是一举两得的优化。由于箭头函数会继承外层的 this,因此可以将 return 中的 this 传递给 changeColor。
如果不了解 apply,建议查阅相关文档深入学习。
至此,一个延迟执行的 debounce 函数便诞生了!
什么是延迟防抖?
顾名思义,延迟防抖是指在延迟时间结束的那一刻才触发回调函数。
如果你觉得每次点击后都要等待才能改变颜色太麻烦,甚至可能没等到颜色变化就关闭了网页。
前缘防抖可以解决这个问题!(即在定时器开始的那一刻就立即触发事件)
对代码进行进一步修改,即可得到前缘防抖版本:
function debounce(fn) {
let t = null
return function () {
// 用firstClick来记录每一次定时器开始的第一次按下的动作
var firstClick = !t
if(t) {
clearTimeout(t)
}
if(firstClick) {
fn.apply(this, arguments)
}
//等待1秒后将 t 置为 null,在这1秒内如果再点击事件就会去到if(t)的执行
t = setTimeout(() => {
t = null
}, 1000)
}
}前缘防抖的效果是:每次连续点击后,先立即响应一次事件,然后再开始等待一秒。
总结
本文详细介绍了 JavaScript 防抖的具体实现及详细原理步骤。如需了解更多相关实现内容,请搜索本站历史文章或继续浏览后续相关文章。希望大家多多支持本站!