前言
早在之前我对闭包还不是很理解的时候,我把它抽象的认为是函数里面写函数,有点滑稽。其实闭包的正确定义是,可以访问另外函数作用域内部变量的函数。这样它就可以重复使用变量,且不会造成变量污染。
一
什么是变量污染?先看一段经典代码,在那个没有三大框架的年代,要给列表添加点击事件时:
const list = document.querySelector('li');
for (var i = 0; i < list.length - 1; i++) {
list[i].onclick = function(i) {
console.log(i);
}
}
// 为了方便执行,用setTimeout函数代替给dom添加点击事件
for (var i = 0; i < 5; i++) {
console.log('直接打印', i);
setTimeout(() => { console.log('延迟打印', i) });
}
// 直接打印 0
// 直接打印 1
// 直接打印 2
// 直接打印 3
// 直接打印 4
// 延迟打印 5
// 延迟打印 5
// 延迟打印 5
// 延迟打印 5
// 延迟打印 5
这样写的问题就是,无论你点击哪个<li>
标签,总是打印5
,这是因为使用var
声明变量时,会创建全局变量;上面的代码类似于这样:
var i = 0;
for (i < 5; i++) {
console.log('直接打印', i);
setTimeout(() => { console.log('延迟打印', i) });
}
其实每次循环都是在操作同一个变量,这就导致了变量污染,等到触发点击事件时,这个i
就已经等于5
了。我还记得我当时是这样处理的,使用一个立即执行函数包裹一下,这样就通过立即执行函数创造了一个局部作用域,保存住了每次循环的变量i
,每个作用域块内的变量不会互相污染:
for (var i = 0; i < 5; i++) {
console.log('直接打印', i);
(function(i) {
setTimeout(() => { console.log('延迟打印', i) });
})(i);
}
// 直接打印 0
// 直接打印 1
// 直接打印 2
// 直接打印 3
// 直接打印 4
// 延迟打印 0
// 延迟打印 1
// 延迟打印 2
// 延迟打印 3
// 延迟打印 4
但是现在有了let
,这个问题就简单得多了,因为使用let
声明变量时自带作用域,其实for循环表达式部分是一块单独的作用域,其内部的i
是继承了父作用域的i
值:
for (let i = 0; i < 5; i++) {
console.log('直接打印', i);
setTimeout(() => { console.log('延迟打印', i) });
}
// 直接打印 0
// 直接打印 1
// 直接打印 2
// 直接打印 3
// 直接打印 4
// 延迟打印 0
// 延迟打印 1
// 延迟打印 2
// 延迟打印 3
// 延迟打印 4
而且Vue
有了v-for
指令,想给列表加点击事件,直接在标签上添加就可以了。
其实在我们使用立即执行函数保存变量时,就是在使用闭包了。
二
而如今,闭包应用很经典的例子就是去抖和节流。
1.去抖
去抖函数是应用于短时间内连续多次调用同一个函数场景。我们用一个延迟函数去阻止它立即执行,当短时间内再次调用,我们清除上一个延迟函数,重新创建一个延迟函数,直到最后不再调用目标函数,则延迟时间到达执行一次目标函数。应用场景就是列表筛选,需求为输入内容改变就调用接口筛选列表,当连续输入内容时,我们不能一直调用接口,只在用户输入完调用一次接口即可,这样可以大大降低网络开销。
const debounce = (fn, ms = 500) => {
let timer = null;
return (...rest) => {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
fn(...rest);
clearTimeout(timer);
timer=null;
}, ms);
};
};
2.节流
节流函数也是应用于短时间内连续多次调用同一个函数场景。常见用于页面滚动时,不断拉取数据,也是不能每次触发滚动都去请求接口,我们同样做一个延迟函数,每次触发只要前一个延迟函数还在就不做任何操作,等到前一个延迟函数执行完,我们就再创建一个延迟函数,达到的效果就是每隔一定间隔去拉取数据,也是可以大大降低网络开销。
const throttle = (fn, ms = 500) => {
let timer = null;
return (...rest) => {
if (!timer) {
timer = setTimeout(() => {
fn(...rest);
clearTimeout(timer);
timer = null;
}, ms);
}
};
}