对于闭包,可以说是老生常谈的话题,但是真正理解闭包的,又有哪些核心点呢?
静态作用域
先理解一下静态作用域的定义:函数作用域在定义时就已确定。
可以看出闭包就是一个静态作用域。内部函数在定义的地方向上查找所需变量。
例1:
let a = 1 function fn() { let a = 0 function fn2() { console.log(a); } fn2() } fn() // 0
例2:
let a = 1 function fn() { let a = 0 fn2() } function fn2() { console.log(a); } fn() // 1
根据上面两个案例,可以很好的展示静态作用域的特点。
例1:
在代码定义时,确定其作用域范围。在运行时,根据作用域创建相应的执行环境。
在fn2函数中需要输出a时,会先查看当前fn2作用域是否存在,有就会使用;没有则会通过作用域链向上一级fn函数作用域中查找,找到第一次与之匹配的变量停止。
所以,a输出的时fn中的值0。
闭包定义
闭包:首先闭包是一个对象,是内部嵌套函数引用了外部函数变量(函数)的对象,是内部函数与周围状态的引用捆绑在一起的一个组合。
查看闭包
chrome浏览器控制台进行debug,执行外部函数,可以发现内部函数需要的变量存在于内部函数的[[scopes]]
内。
如上图,闭包就是存在于内部嵌套函数中,包含被引用的a变量。
常见的闭包
常见的闭包有两种:将子函数作为父函数的返回值、将父函数中实参传递给子函数。
子函数作为父函数的返回值
let a = 1 function fn() { let a = 0 function fn2() { a++ console.log(a); } return fn2 } let f = fn() f() // 1 f() // 2
执行let f = fn();f()
调用的是fn函数内部的fn2函数。
执行fn2:fn2通过作用域链找到要fn中引用的a变量,执行a++
,输出1。
再次执行f()
语句,再次调用fn2。fn函数内部的变量依旧存在,不过经过上一次自加,a变量值为1。此时fn2再次a++
,输出值为2。
注意: 闭包调用次数,取决于调用几次外部函数。
父函数中实参传递给子函数
function fn(a) { function fn2() { a++ console.log(a); } fn2() fn2() } fn(0) // 1 2
fn2()内部函数引用fn外部函数参数。第一次执行fn2()
,a自加后输出1;第二次执行fn2()
,此时a值已经为1,a自加后输出2。
闭包作用
- 使用父函数内部的变量在函数执行完成后,仍然存活在内存中(延长局部变量的生命周期)
- 外部可以调用子函数,操作到父函数内部数据(变量/函数)
function fn() { let a = 0 function fn2() { a++ console.log(a); } return fn2 } let f = fn() f()
1、fn()
执行完成后,变量a仍然存活在内存中。
解释:在执行let f = fn()
时,变量f指向了fn2指向的函数对象。该行语句执行完成后,fn2函数变量会释放。但是f指向的函数对象(闭包)仍然存在,闭包内部a变量仍然存活在内存中。再执行f()
,就相当于调用的上图的函数对象。
如果未定义变量f指向闭包,即只执行fn()
。那么,内部函数在fn执行完成后释放回收。外部也无法调用fn内部函数。
所以,父函数执行完成后,父函数内部声明的局部变量一般不存在,在闭包中的变量才可能存在。
2、在外部可以通过操作变量f
,来对fn函数内部变量a进行自加。
闭包的生命周期
产生:在子函数定义时就产生了(不是调用)
死亡:在子函数的内部对象成为垃圾对象时
function fn() { let a = 0 function fn2() { a++ console.log(a); } return fn2 } let f = fn() f() f()
在fn内部,执行到39行时,fn2定义时就存在了闭包。
代码执行完成,闭包仍然存在!
此时,我们可以将f变量置为空,将fn2指向的函数对象变为垃圾对象。添加f = null
。
闭包应用
闭包最常见的应用应该就是在定义js模块了。定义一个js模块,对外暴露多个方法或变量。如果对外暴露的方法中,使用到模块内部的变量就属于闭包的应用。
可以这样定义简单的js模块:
;(function (window) { let a = 0 function f1() { console.log('f1', a) } function f2() { console.log('f2', a) } window.myModule = { f1, f2 } })(window)
直接在所需使用的地方引入文件,并调用myModule的可用方法。如:myModule.f1()
就可以执行模块中f1的方法。
js模块中使用匿名函数,对外暴露的是myModule全局对象。在使用时不需要执行myModule,直接调用myModule的方法即可。传入window参数,通常是为了在代码压缩时,进行简写window参数。
闭包缺点及解决方法
缺点:容易引起逻辑问题;函数执行完,函数内部变量未被释放占用内存时间变长,容易造成内存泄漏。
循环与闭包
闭包容易引起逻辑问题。先来看个典型的案例:
for (var i = 0; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, 1000) } // 6 6 6 6 6 6
for语句在js中不属于块作用域,定义的i属于的全局变量。6次循环都被封闭在共享的全局作用域中,因此i只有一个。
再者,setTimeout属于异步回调。6次循环结束后,才执行setTimeout。
上段代码等价于将setTimeout函数回调重复定义6次,完全不使用循环。
如果想要输出0、1、2、3、4、5怎么办呢?
解决:
核心:使用封闭循环的各自作用域。
- 使用IIFE(立即执行函数表达式)在每次迭代中创建的作用域封闭起来。
- 使用let声明,可以用来劫持块作用域,并且在块作用域中声明这个变量。
内存泄漏
内存泄漏:一个被分配的内存既不能使用,也不能回收到浏览器,直到进程结束。 关于内存泄漏,可以参考[闭包的生命周期]中,闭包手动释放回收。
解决:
- 能不用闭包就不用闭包
- 让内部函数称为垃圾对象,回收闭包(及时释放)。
补充:内存溢出与内存泄漏
内存溢出与内存泄漏关系:内存泄漏积累到一定程度,会造成内存溢出。
内存泄漏
内存泄漏的情况:
- 闭包
- 意外的全局变量
function fn() { a = 0 console.log(a); } fn()
变量a未正常定义,变成全局变量。在代码执行完成之后,全局变量中依旧存在a变量。
- setInterval
setInterval(() => { console.log('--'); }, 1000);
启动循环定时器时,未及时清理。需要使用clearInterval手动清理。
let time = setInterval(() => { console.log('--'); }, 1000); clearInterval(time)
内存溢出
当程序运行所需内存超过剩余内存时,抛出内存溢出的错误。
执行上述代码,浏览器一直卡在运行状态,内存不能满足程序所需内存。