1.闭包的形成条件:
1.函数嵌套
2.内部函数引用了外部函数的局部变量
PS:正常情况下,当一个函数调用完之后,内存中关于函数的东西会全部释放掉,局部变量也会消失,而闭包是一种特殊的存在。由于外部函数返回的是内部函数的引用,相当于你返回了一个函数,这个函数还未被真正调用。但是内函数又使用了外函数的变量,导致内存不能释放它们,需要等到内函数使用完成之后才能释放它们----由此形成了闭包
3.闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量
2.闭包的用途
2.1
简单例子1
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下面的代码。
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量。
闭包简单例子2
概念: 闭包只能取得外部函数中任何变量的最后一个值.
function create() {
let res = []
for (var i = 0; i < 5; i++) {
res[i] = function() {
return i
}
}
return res
}
console.log(create()) // 结果: [4,4,4,4,4]
分析:
由于每个函数的作用域中都保存着同一个 函数的活动对象(create 函数), 所以他们引用的都是同一个变量 i.
如何解决这个问题?
我们可以使用匿名函数来创建一个闭包, 让闭包的行为来符合预期
function create() {
let res = []
for (var i = 0; i < 5; i++) {
res[i] = (function(num) {
return function() { return num }
})(i)
}
return res
}
console.log(create()) // 结果: [0,1,2,3,4]
上面将 create 函数进行了一个重写.
1.现在没有直接把闭包赋值给数组,而是再定义一个匿名函数, 将变量 i 当参数传进去由num来接收并立即执行.
2.在每次执行这个匿名函数时都会创建一个属于自己的 活动对象/作用域/变量对象.
3.然后再里面返回一个 只访问 num 的闭包函数, 这时取到的 num 都是独立的值
2.2 闭包的概念和特性
首先看个闭包的例子:
function makeFab () {
let last = 1, current = 1
return function inner() {
[current, last] = [current + last, current]
return last
}
}
let fab = makeFab()
console.log(fab()) // 1
console.log(fab()) // 2
console.log(fab()) // 3
console.log(fab()) // 5
分析:
makeFab的返回值就是一个闭包,makeFab像一个工厂函数,每次调用都会创建一个闭包函数,如例子中的fab。
注意:fab每次调用都不需要传参数,都会返回不同的值,因为在闭包生成的时候,它记住了变量last和current,以至于在后续的调用中能够返回不同的值。
PS:能够记住函数本身所在作用域的变量,这就是闭包和普通函数的区别所在
MDN中给出闭包的定义:函数与其状态即词法环境的引用共同构成闭包
这里的“词法环境的引用”,可以简单理解为“引用了函数外部的一些变量”,例如上述例子中每次调用makeFab都会创建并返回inner函数,引用了last和current两个变量。
2.2 闭包的用途
例子1
以实现一个可复用的确认框为例,比如在用户进行一些删除或者重要操作时,为了防止失误操作,我们可能会通过弹窗让用户再次确认操作。
比如在触发弹窗中的确认/取消事件时异步操作,这时候我们就需要使用两个回调函数完成操作,弹窗函数confirm接受三个参数,一个时提示语,一个是确认回调函数,一个是取消回调函数:
function confirm (confirmText, confirmCallback, cancelCallback) {
// 插入提示框DOM,包含提示语句、确认按钮、取消按钮
// 添加确认按钮点击事件,事件函数中做dom清理工作并调用confirmCallback
// 添加取消按钮点击事件,事件函数中做dom清理工作并调用cancelCallback
}
之前我还一直想不通,原来是闭包在作祟!!!我服了!!!
因为使用闭包,confirm中则包含两个回调函数,那么我们就可以通过confirm传递回调函数,并且根据不同的结果完成不同的动作,比如我们根据id删除一条数据可以这样这:
function removeItem(id){
confirm('确认删除吗?',()=>{
//用户点击确认,发送远程ajax请求
api.removeItem(id).then(xxx)
},
()=>{
//用户点击取消
console.log('取消删除‘)
}
}
解析:在这个例子中,confirm 的回调函数正式利用了闭包,创建了一个引用上下文id变量的函数!
例子2使用闭包实现防抖、节流函数
前端很常见的一个需求就是远程搜索,根据用户输入框的内容自动发送ajax请求,然后从后端把搜索结果请求回来。
为了简化用户的操作,有时候我们不会专门放置一个按钮来点击触发搜索事件,而是直接监听内容的变化来搜索(比如微信小程序中根据关键字搜索商品)
这时候为了避免请求过于频繁,我们可以利用”防抖“技术,即当用户停止输入一段事件时(比如500ms)后才执行
- 可以使用如下代码实现
<body>
<input type="text">
<script>
function debounce(func, time) {
let timer = 0;
return function () {
//若使用timer && clearTimeout(timer)会更加确保timer被清除记录,或者timer=0
//注释这一行和注释下面的timer=0也可以(只是相对没那么严谨)
// timer && clearTimeout(timer)
clearTimeout(timer)
timer = setTimeout(() => {
// timer = 0
func.apply(this)//反之this指向错乱
}, time)
}
}
const input = document.querySelector('input')
input.onkeypress = debounce(function () {
console.log(input.value)//事件处理逻辑
}, 500)
</script>
</body>
分析:dobounce函数每次调用时,都会创建一个新的闭包函数,该函数保留了对事件逻辑处理函数func以及防抖时间间隔time以及定时器标志timer的引用
例子3—使用闭包实现节流函数
function throttle(func,time){
var lastTime=0;
return function(){
var nowTime=new Date();
if(nowTime-lastTime>time){
func.call(this);
lastTime=nowTime;//由于闭包的作用原理,这里调用外层函数的局部变量,每次调用这个lastTime变量都会发生改变
}
}
}
例子4–使用闭包解决按钮多次连续点击问题
问题:
用户点击一个表单提交按钮时,前端会向后端发送异步请求,请求还没有返回,用户焦急又多点了即此按钮,这样带来的后果就是:①消耗服务器资源②修改了后台的数据
解决:通常办法是定义一个标记变量,即在响应函数所在的作用域声明一个布尔变量lock,响应函数被调用时,先判断lock的值,为true则表示上一次请求还没有返回来,此时点击无效;为false则将lock设置为true,然后发送请求,请求结束后将lock改为fasle
很显然,lock变量会污染函数所在的作用域,而生成闭包伴随着新的函数作用域的创建,利用这一点,刚好可以解决这个问题,下面是简单的例子:
let clickButton = (function () {
let lock = false;
//postParams为发送请求时需要的参数
return function (postParams) {
if (lock) return
lock = true
//使用axios发送请求
axios.post('urlxxxxx', postParams).then(
).catch(error => {
//请求失败
console.log(err)
}).finnally(() => {
//不管失败还是成功,都解锁
lock = false
})
}
})()
说明:这样lock变量就会在一个单独的作用域里,一次点击请求发出以后,必须等请求回来,才会开始下一次请求。
当然,为了避免各个地方都声明lock,修改lock,我们可以把上述逻辑抽象以下,实现一个装饰器,就像节流/防抖函数一样。
function singleClick(func, manuDone = false) {
let lock = false
return function (...args) {
if (lock) return
lock = true
let done = () => lock = false
if (manuDone) return func.call(this, ...args, done)
let promise = func.call(this, ...args)
promise ? promise.finally(done) : done()
return promise
}
}
默认情况下,需要原函数返回一个promise以达到promise决议后将lock重置为false,而如果没有返回值,lock将会被立即重置(比如表单验证不通过,响应函数直接返回),调用示例:
let clickButton = singleClick(function (postParams) {
if (!checkForm()) return
return axios.post('urlxxx', postParams).then(
// 表单提交成功
).catch(error => {
// 表单提交出错
console.log(error)
})
})
button.addEventListener('click', clickButton)
3、使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。