各位观众,各位听众,晚上好!欢迎来到今天的“闭包奇妙夜”!我是你们今晚的导游,将带领大家探索闭包这个编程世界里既神秘又实用的概念,并揭开它在函数防抖和节流这两种常见性能优化技巧中的应用。
准备好了吗?让我们系好安全带,开启这段代码之旅吧!🚀
第一幕:闭包,你是我的小呀小苹果🍎
首先,我们来聊聊闭包。闭包这玩意儿,就像一个装着秘密的小盒子,或者更像你小时候藏在床底下的零食,只有你知道,而且还能随时拿出来吃!
更专业一点儿说,闭包是指有权访问另一个函数作用域中的变量的函数。这听起来有点绕口,让我们用一个简单的例子来说明:
function outerFunction() {
let outerVar = "我是外面的变量";
function innerFunction() {
console.log(outerVar); // innerFunction 可以访问 outerVar
}
return innerFunction;
}
let myClosure = outerFunction();
myClosure(); // 输出 "我是外面的变量"
在这个例子中,innerFunction
就是一个闭包。即使 outerFunction
已经执行完毕,innerFunction
仍然可以访问 outerFunction
作用域中的 outerVar
变量。
为什么会这样呢?因为当 outerFunction
返回 innerFunction
时,innerFunction
就“记住”了 outerFunction
的作用域,并将这个作用域链保存了下来。即使 outerFunction
已经执行完毕,这个作用域链仍然存在,innerFunction
仍然可以通过这个作用域链访问到 outerVar
。
闭包就像一个“记忆大师”,它记住了创建它的环境,并将这个环境一起带走。 这就是闭包的核心所在。
第二幕:防抖,别急,让我喘口气💨
现在,我们进入正题,聊聊函数防抖(Debouncing)。想象一下,你正在做一个搜索功能,用户每输入一个字,就向服务器发送一次请求。如果用户输入速度很快,那就会产生大量的请求,给服务器带来很大的压力。
这时候,防抖就派上用场了!防抖就像一个“贤内助”,它会告诉你:“别急,等用户停止输入一段时间后再发送请求。”
防抖的原理是:在一定时间内,如果事件被触发多次,则只有最后一次会执行。
让我们用一个例子来说明:
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId); // 清除之前的定时器
timeoutId = setTimeout(() => {
func.apply(this, args); // 执行函数
}, delay);
};
}
function search(query) {
console.log(`Searching for: ${query}`);
}
const debouncedSearch = debounce(search, 300); // 300ms 的防抖时间
// 模拟用户输入
debouncedSearch("a");
debouncedSearch("ab");
debouncedSearch("abc");
setTimeout(() => debouncedSearch("abcd"), 400); // 最终只执行这一个
在这个例子中,debounce
函数返回了一个新的函数,这个新函数就是防抖后的函数。每次调用 debouncedSearch
函数时,都会清除之前的定时器,并重新设置一个新的定时器。只有当用户停止输入一段时间后,定时器才会触发,执行 search
函数。
注意这里,debounce
函数返回的匿名函数就构成了一个闭包。 它“记住”了 timeoutId
和 func
这两个变量。
函数 | 作用 |
---|---|
debounce |
创建一个防抖函数,接收一个函数和一个延迟时间作为参数。 |
匿名函数 | 返回的防抖后的函数,形成闭包,可以访问 debounce 函数作用域中的 timeoutId 和 func 变量。 |
setTimeout |
设置一个定时器,在延迟时间后执行 func 函数。 |
clearTimeout |
清除之前的定时器,防止函数被多次执行。 |
防抖适用于:
- 搜索框输入
- 调整浏览器窗口大小
- 滚动事件
第三幕:节流,雨露均沾,一个都不能少 💧
接下来,我们来聊聊函数节流(Throttling)。节流就像一个“公平的分配者”,它会告诉你:“不能让事件太频繁地执行,要雨露均沾,每个一段时间执行一次。”
节流的原理是:在一定时间内,只能执行一次。
让我们用一个例子来说明:
function throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, args);
lastTime = now;
}
};
}
function scrollHandler() {
console.log("Scrolling...");
}
const throttledScrollHandler = throttle(scrollHandler, 200); // 200ms 的节流时间
window.addEventListener("scroll", throttledScrollHandler);
在这个例子中,throttle
函数返回了一个新的函数,这个新函数就是节流后的函数。每次调用 throttledScrollHandler
函数时,都会判断当前时间距离上次执行的时间是否超过了 delay
。如果超过了,则执行 scrollHandler
函数,并更新 lastTime
。
同样, throttle
函数返回的匿名函数也构成了一个闭包。它“记住”了 lastTime
和 func
这两个变量。
函数 | 作用 |
---|---|
throttle |
创建一个节流函数,接收一个函数和一个延迟时间作为参数。 |
匿名函数 | 返回的节流后的函数,形成闭包,可以访问 throttle 函数作用域中的 lastTime 和 func 变量。 |
Date.now |
获取当前时间戳。 |
if 条件判断 |
判断当前时间距离上次执行的时间是否超过了 delay 。 |
节流适用于:
- 滚动事件
- 鼠标移动事件
- 按钮点击事件
第四幕:闭包,防抖,节流,三剑客的完美配合 🤝
现在,我们来总结一下闭包在防抖和节流中的作用。
- 闭包是实现防抖和节流的关键。 防抖和节流都需要“记住”一些状态,比如定时器 ID、上次执行的时间等。这些状态需要保存在函数的作用域中,而闭包正好可以实现这个功能。
- 闭包可以防止变量被意外修改。 防抖和节流都需要在函数内部修改一些变量,比如定时器 ID、上次执行的时间等。如果这些变量暴露在全局作用域中,可能会被意外修改,导致防抖和节流失效。而闭包可以将这些变量封装在函数的作用域中,防止被意外修改。
我们可以用一个表格来总结一下防抖和节流的区别:
特性 | 防抖 (Debouncing) | 节流 (Throttling) |
---|---|---|
触发时机 | 在一段时间内,只执行最后一次。 | 在一段时间内,最多执行一次。 |
目的 | 减少不必要的请求,优化性能。 | 控制事件的触发频率,防止页面卡顿。 |
适用场景 | 搜索框输入、调整浏览器窗口大小、滚动事件。 | 滚动事件、鼠标移动事件、按钮点击事件。 |
状态 | 需要记住定时器 ID。 | 需要记住上次执行的时间。 |
闭包作用 | 保存定时器 ID,防止被意外修改。 | 保存上次执行的时间,防止被意外修改。 |
形象比喻 | “贤内助”,告诉你:“别急,等用户停止输入一段时间后再发送请求。” | “公平的分配者”,告诉你:“不能让事件太频繁地执行,要雨露均沾,每个一段时间执行一次。” |
第五幕:代码优化,让你的代码飞起来 🚀
最后,我们来聊聊如何优化防抖和节流的代码。
- 使用
requestAnimationFrame
代替setTimeout
。requestAnimationFrame
可以更好地与浏览器的渲染周期同步,从而提高性能。 - 使用
leading
和trailing
选项来控制函数的执行时机。leading
选项表示在延迟开始前是否立即执行一次函数,trailing
选项表示在延迟结束后是否执行最后一次函数。 - 使用第三方库,例如 Lodash 和 Underscore.js。 这些库提供了现成的防抖和节流函数,可以直接使用。
一个使用 leading
和 trailing
选项的防抖函数:
function debounce(func, delay, options = { leading: false, trailing: true }) {
let timeoutId;
let lastArgs;
let lastThis;
const { leading, trailing } = options;
function invokeFunc(time) {
timeoutId = null;
if (lastArgs) {
func.apply(lastThis, lastArgs);
lastArgs = lastThis = null;
}
}
return function(...args) {
lastArgs = args;
lastThis = this;
if (!timeoutId) {
if (leading) {
func.apply(this, args);
}
timeoutId = setTimeout(invokeFunc, delay);
} else if (trailing) {
clearTimeout(timeoutId);
timeoutId = setTimeout(invokeFunc, delay);
}
};
}
总结:
今天,我们一起探索了闭包的奥秘,并了解了它在函数防抖和节流中的应用。希望通过今天的学习,你能够更好地理解闭包,并能够灵活运用防抖和节流来优化你的代码。
记住,闭包就像一个装着秘密的小盒子,防抖就像一个“贤内助”,节流就像一个“公平的分配者”。 掌握它们,你就能在编程的世界里游刃有余! 💪
谢谢大家! 晚安! 🌙