目录
一、为什么要使用闭包
在编程语言中有一条不成文的铁律——尽可能少定义全局变量
原因一:难以控制。
全局变量在任何地方都可以进行读写,意味着可能会被不相干的程序改写。
原因二:占用内存。
全局变量占用内存的生命周期长,一般局部变量(定义在函数中的变量),在函数调用完后与之对应的执行环境会被推出执行栈,回收机制会每隔一段时间进行一次回收操作,释放不需要被占用的内存,执行环境出栈就是在告诉回收机制“这些变量我不需要了,可以回收了”;而全局变量因为随时可以被任何程序在任何地方读写,所以回收机制很难统计何时需要释放全局变量所占用的内存,也就导致全局变量一般在全局执行环境被销毁时才会释放。
二、什么是闭包
-
父函数里面嵌套子函数,子函数访问父函数的局部变量。
-
通过 return 将子函数暴露到全局作用域,子函数就形成了闭包。
-
通过闭包,父函数的局部变量没有被销毁,可通过闭包去调用,但同时,这个局部变量也不会被全局变量污染。
三、示例:定时器
问题:每隔一秒钟累加并打印一下,到达十秒钟停止累加。
// 累加器
function counter() {
let second = 0
function doCounter() {
if(second === 10) {
clearInterval(recordSecond)
return
}
second += 1
console.log(`${second}秒`)
}
return doCounter
}
// 得到累加器
let doCounterFn = counter()
const recordSecond = setInterval(function() {
// 调用累加器
doCounterFn()
}, 1000)
分析
1)let doCounterFn = counter() 中的 counter() 创建了相应的 函数执行上下文,在该上下文中声明了一个局部变量 second 并且声明了一个函数 doCounter ,最后返回了 doCounter 函数的引用指针。
2)let doCounterFn = counter() 执行结束后,相应的 函数执行上下文就从栈中弹出,一般情况下,其变量也会随之销毁,但是定时器函数中 doCounterFn() 调用了 doCounterFn,及执行了 counter 函数中的 doCounter 函数,里面的 second 引用着 counter 函数里的变 second,所以其 doCounterFn 变量不会随着销毁,相当于封装了一个私有变量。
执行上下文
全局执行上下文:
1)任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象,并且设置 this 的值等于这个全局对象。
2)一个程序中只会有一个全局执行上下文。
函数执行上下文:
1)每当一个函数被调用时,都会为该函数创建一个新的执行上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。
2)函数上下文可以有任意多个。
四、作用与优缺点
作用
1)保护:保护函数的私有变量不受外部的干扰。
2)保存:把一些函数内的值保存下来,闭包可以实现方法和属性的私有化。
优点
1)延长局部变量的生命周期。
2)函数外部能够访问到内部变量。
3)可以避免全局污染。
缺点
1)闭包会携带包含其他的函数作用域,因此会比其他函数占用更多的内存。
2)不正当地使用可能会造成内存泄露(不再用到的内存,没有及时释放,就叫做内存泄露)
// 函数一
function fn2() {
let test = new Array(1000).fill('rocky')
return function() {
console.log(11111)
}
}
// 函数二
function fn2() {
let test = new Array(1000).fill('rocky')
return function() {
console.log(test)
return test
}
}
// fn2Child是fn2函数返回的匿名函数的引用变量
let fn2Child = fn2()
fn2Child()
// 如果使用函数二,则会产生内存泄露,需要及时断开对变量的引用才可以阻止内存泄露(使用函数一不会,因为返回的匿名函数中没有使用外部的函数的变量,但依旧属于闭包)
fn2Child = null // 解决方法:函数调用后,把外部的引用关系置空
- js 引擎为了解决内存泄露问题,才有了 垃圾回收机制,能够让 js 自动的管理内存,将内存中不再使用的变量回收掉,然后释放出内存空间。实现原理就是通过 判断当前的变量是否被引用(以下代码块中函数一与函数二的区别就是返回的匿名函数中是否用到了父函数内部的引用变量 test,如果用到了就存在内存泄漏,如果没有用到则 test 变量完全可以被回收)。
- 闭包产生内存泄漏的根本原因就是 没有及时的断开对变量的引用,而不是闭包产生的内存泄露。如果我们对该引用可以进行控制,就可以解决内存泄露的问题。
五、垃圾回收与内存泄露
- 由于栈内存小,由系统自动分配空间并回收,所以我们经常说的垃圾回收一般指堆内存的回收。
- 没有引用关系的对象就是垃圾(以下 {name: '鱼小包'} 即为垃圾。
let a = { name: '鱼小包' } // 重新赋值 a = [1]
- 栈内存与堆内存回收时机:
1)栈内存:基本上用完就回收了。
2)堆内存:不会随方法的结束而销毁,就算方法结束了,这个对象也可能会被其他引用变量所引用(参数传递),只有当一个对象没有任何引用变量引用它时,系统的回收机制才会在核实的时候回收它。