1.什么是闭包
在函数中嵌套函数,内部函数另一个作用域中被调用,就形成了闭包。
2.闭包的例子
我们来看一个闭包的例子
<button id="btn">按钮</button>
<script>
// 定义全局作用域变量
let a = 3
let b = 4
function outFn(){
//定义函数作用域变量
let a = 1
let b = 2
return function(){
console.log("使用a和b处理了一些逻辑",a,b)
}
}
window.onload = ()=>{
let innerFn = outFn()
let btn = document.getElementById("btn")
btn.onclick = innerFn
}
</script>
可见,每当我们点击按钮的时候,都触发了 outFn中的innerFn函数
但为什么调用函数输出的是函数作用域变量1,2,而不是全局作用域变量3,4呢?明明我们是在全局作用域中调用的?
- 因为存在闭包
闭包机制,使函数内部的函数,在被调用时,作用域任然是外部函数的作用域。
在内部函数调用时,外部函数并未被销毁,内部函数仍然存在外部函数的作用域中
3.闭包的作用
我更愿意理解为,闭包的作用是我们通过闭包的性质,来开发出的用法,而不是闭包开始就是为了这种功能而存在的
闭包的作用有两种:
- 将函数需要用到的变量在函数作用域内声明,避免全局污染
- 将函数内部的变量给外部作用域开放一个访问渠道(将变量设置为私有变量)
我们先来分析第一种作用:
- 在真实开发中,我们需要尽量少的使用全局变量,理由如下:
- 使用全局变量在整个全局作用域都能获取,以及修改,整个开发中我们可能会由不同人编写很多个js文件,可能会导致在这个js文件中不小心操作了另一个作用域中的全局变量的情况,是我们不愿意看到的。
- 由于js的垃圾回收机制,全局作用域变量是最后被销毁的,因此将会一直占用内存,如果占用的多了,会影响性能。
因此,可以通过闭包,将需要使用的全局变量,定义在函数作用域内,沿用上一个例子
<script>
let a = 3
let b = 4
//不使用闭包,操作全局变量
function fn(){
console.log("使用a和b处理了一些逻辑",a,b)
}
//使用闭包,操作函数作用域的局部变量,避免使用全局变量
function outFn(){
let a = 1
let b = 2
return function(){
console.log("使用a和b处理了一些逻辑",a,b)
console.log(this)
}
}
window.onload = ()=>{
let innerFn = outFn()
let btn = document.getElementById("btn")
btn.onclick = innerFn
}
</script>
再来分析第二种作用:
-
由于js中只能访问当前作用域的变量,因此如果我们想访问函数作用域中的变量,只能在函数作用域中访问,但我们又有一个需求,需要在全局访问,怎么办?
-
借助闭包性质
我们来看一个例子
<script>
function fn(){
let a = "函数中的变量"
}
window.onload = ()=>{
console.log("在全局作用域中获取"+a)
}
</script>
很显然作用域不同,是获取不到重要变量的
类似java中获取private变量的方式,我们想要获取到这个变量,可以这样做
<script>
function fn(){
let a = "函数中的变量"
return function getA(){
return a
}
}
window.onload = ()=>{
//获取getA(),完整写法
let getA = fn()
let a1 = getA()
//简写
let a2 = fn()()
console.log("在全局作用域中获取"+a1)
console.log("在全局作用域中获取"+a2)
}
</script>
- 利用闭包性质,由于函数作用域未被销毁,因此getA()获取到的仍然是函数中的变量,完成了功能。
4.闭包的应用场景——防抖与节流
之前,我们了解了闭包的两种作用,现在我们在讲讲闭包的真实应用场景
什么是防抖与节流
- 首先我们来简单的介绍一下防抖与节流的定义
- 防抖:当持续触发某种事件时,一定时间没有执行该事件,处理函数才会调用。
- 节流:当持续触发某种事件时,在一定时间范围内,限定执行处理函数的次数。
为什么要防抖与节流
为了提高性能
- 如果不防抖
- 如下图,例如我们要做一个实时检测用户输入,并且发送请求搜索数据的业务,我们输入每一个字符时,都会发送一次请求,但我们实际只需要携带用户输入完的那一次的数据来请求服务器就够了,这样多次无用的请求,增大了服务器的压力
- 如果不节流
- 如下图,我们要实现一个当页面大小改变的时候,执行一些业务
<script>
window.onresize = function(){
console.log("执行了一些业务")
}
</script>
发现,当稍微改变浏览器窗口大小,就会执行很多次业务,这样浏览器性能将会受影响,可能会造成卡顿
为了解决这样的问题,就需要实现防抖与节流了
如何实现防抖与节流
- 实现防抖:
//防抖函数
function debounce(callback,delay){
//定义一个计时器
timer = null
return ()=>{
//如果进来的时候有计时器
if(timer){
//清除计时器
clearTimeout(timer)
}
timer = setTimeout(callback,delay)
}
}
//测试函数
window.onload = ()=>{
let input = document.getElementById("inp")
input.addEventListener("input",debounce(sendAjax,1000))
}
function sendAjax(e){
// let content = e.target.value
// console.log("发送了请求,携带参数"+content)
console.log("发送了请求")
}
效果:只发送了一次请求
- 可以发现,我把一些的代码注释起来了
function sendAjax(e){
// let content = e.target.value
// console.log("发送了请求,携带参数"+content)
console.log("发送了请求")
}
如果放开会导致如下情况,为什么呢?
因为我们的oninput事件绑定给了防抖函数,而接受到event参数的是防抖函数,我们并没有将event传递给要执行的函数。
因此做如下改进
function debounce(callback,delay){
//定义一个计时器
timer = null
//使用剩余参数获取事件
return (...args)=>{
console.log(args)
//如果进来的时候有计时器
if(timer){
//清除计时器
clearTimeout(timer)
}
timer = setTimeout(()=>{
//传递参数,并且绑定this指针,apply传递数组
callback(args[0])
},delay)
}
}
function sendAjax(e){
let content = e.target.value
console.log("发送了请求,携带参数"+content)
}
实现了功能
我们还可以进行进一步优化,如果我们想要在调用的函数中的this指针指向触发的函数要怎么办呢?先来分析一下之前我们的代码的this指针
//我们可以看见,我们实际调用的是 return的这个函数
input.addEventListener("input",debounce(sendAjax,1000))
//debounce在window中调用
function debounce(callback,delay){
timer = null
//这里是箭头函数,其中的this指向debounce的作用域
//debounce在window中调用,因此这里的this指针指向window
return (...args)=>{、
console.log(args)
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=>{
callback(args[0])
},delay)
}
}
这里就发现我们this的指向出错了,想要return的这个函数的指针指向调用他的元素,我们这里不能写箭头函数
//普通函数,this指针指向调用它的元素,我们需要将这个this传到回调函数callback中
return function(...args){
if(timer){
clearTimeout(timer)
}
//这里必须写箭头函数,如果写普通函数则函数中的this指向为window
//我们需要函数中的this指向和return的function相同
timer = setTimeout(()=>{
//通过apply改变callback函数中指针的指向,使其指向this
callback.apply(this,args)
},delay)
}
function sendAjax(e){
console.log(this)
// console.log(this)
let content = e.target.value
// let content = e.target.value
console.log("发送了请求,携带参数"+content)
}
最终实现将this指向触发函数的元素,效果如下:
- 完整代码:
function debounce(callback,delay){
//定义一个计时器
timer = null
return function(...args){
//如果进来的时候有计时器
if(timer){
//清除计时器
clearTimeout(timer)
}
timer = setTimeout(()=>{
// callback(args[0])
callback.apply(this,args)
},delay)
}
}
上述的例子讲述了如何实现防抖,那其中的闭包体现在哪里呢?
我们可以不适用闭包,实现防抖吗?答案是可以
//这里我们不考虑event和this的问题,仅实现最简单的功能
<script>
window.onload = ()=>{
let input = document.getElementById("inp")
input.addEventListener("input",debounce.bind(this,sendAjax,1000))
}
let timer = null
function debounce(callback,delay,...args){
console.log("debounce被调用了")
if(timer){
//清除计时器
clearTimeout(timer)
}
timer = setTimeout(()=>{
callback()
},delay)
}
function sendAjax(){
console.log("发送了请求")
}
效果如下,看来也是可以成功运行的
- 那么为什么要使用闭包呢?
- 联系上文我们提到的闭包的作用,我们把time放在了函数中,避免了全局污染
- 实现节流
实现防抖和实现节流的思路差不多,节流中闭包的作用也是将变量放在函数中,避免全局污染,有了上面的经验,我们直接开写
<script>
window.onresize = throttle(fn,1000)
function fn(){
console.log("执行了一些业务")
}
function throttle(callback,delay){
//判断是否正在定时
let flag = false
return function(...args){
if(flag == true){
return
}
//如果不在定时,则开始定时
flag = true
setTimeout(()=>{
callback.apply(this,args)
//执行完成,结束定时
flag = false
},delay)
}
}
</script>
效果不容易演示,就不放图了,测试能完成功能