JS中闭包的理解剖析
写在前面:如果此篇博客中有任何错误的地方,欢迎大家的指正!让我们共同进步!
如果觉得这篇博客有用就点赞+收藏+关注三连吧!
这几日对于JS中的闭包进行了相对深入的了解,写在这里,以便于后面复习回顾的时候好看。
关于闭包,大致分为六个内容,分别为
闭包的理解、常见的闭包、闭包的作用、闭包的生命周期、闭包的应用、闭包的缺点及其解决。
一、闭包的理解
闭包是如何产生的?
闭包到底是什么?
产生闭包的条件
①函数嵌套
②内部函数引用了外部函数的数据(变量\函数)
③并且调用了外部函数
举例说明何时产生闭包
让我们在浏览器中调试一下,看看是否是这样。
注:这里是加了断点才能在fn1()调用过程中看到fn1内部的闭包,但是整个fn1()调用结束以后,闭包就消失了,因为没有对其进行引用保存。(有关这里更深入的解析请看 闭包的作用)
相信大家看到这里对闭包有了一定模糊的认识,接下来,让我们来看看常见的闭包。
二、常见的闭包
两种常见的闭包
①将函数作为了一个函数的返回值
由于满足在第一部分所说的闭包产生的三个条件,因此这个代码中是产生了闭包的。虽然闭包产生了,但是fn2()中的console.log(a)语句并没有执行。
接下来让我们看一下对 f 进行调用会有什么效果。
这里的执行结果是他分别打印了3和4。
这说明什么呢?
答:这说明变量a还是能够进行读写操作,即a没有消失。
这是什么原因呢?
答:这是由于闭包的一个特点就是函数内部引用外部的变量(如这里的变量a)会一直存在于内存中,不会被立即释放。
而在第一个f()执行完了以后,这个闭包依然存在,第二个f()执行完了以后,也依然存在,直到关闭浏览器。
在此处,外部函数执行几次,就有几个闭包的产生,而和内部函数执行多少次没有关系。因此,截图代码整个过程中只产生了一个闭包,因为fn1()只执行了一次,并将其引用保存到了变量f中,而无论f()执行了多少次,整个过程都只产生了1个闭包。
②将函数作为实参传递给另一个函数调用
注意:图中圈中的区域才是闭包范围(即只有msg,而没有time)。
区分:若如下图↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓所示,则没有闭包产生了。
但是这两种闭包虽然这样定义,但是闭包的作用到底是什么呢?
接下来就让我们一起看看。
三、闭包的作用
两个闭包的作用
①使函数内部的变量在函数执行完以后,任然可以存活在内存中(延长了局部变量的生命周期)
②让函数外部可以操作(读写)到函数内部的数据(变量/函数)
理解注意:
②中所谓的读写,是指的通过封装的内部函数进行操作,内部函数封装什么读写操作,就只能执行那些读写操作,而不能在外面重新自定义访问和修改(读写)。
为了让我们更好的理解闭包,我们在上上一个代码案例的基础上进行一些修改。
执行过程剖析:进入fn1()后,在 ‘return fn3’ 语句之前,这个时候整个环境中存在两个闭包,分别是fn2()的函数体以及fn3()的函数体。
但是在 ‘return fn3’ 语句之后,
①对于fn2()
当31行代码执行完毕之后,fn2()就不在了。
分析:
fn2()也是一个对象,没有人引用它,他就会在fn1()函数调用之后变成垃圾对象了。
换言之,由于没有人引用fn2(),使得fn2()相当于一个局部变量,这个局部变量没有被保存在其他引用中继续使用,因此会在fn1()函数调用后自动释放。
②对于fn3()
当31行代码执行完毕之后,fn3()就不在了,但是他内部的代码内容依旧还在。
分析:
由于在31行代码中,用一个变量指向了这个fn3函数对象,如下图所示,
这使得在31行代码执行完毕以后,fn3()这个对象虽然不在了,但是由于var f有对这个函数的引用,因此fn3()这个函数内部的内容依旧存在。
也就是说,是 f 对其的引用 导致闭包最终存在,一直没有消失。
四、闭包的生命周期
闭包的产生与死亡
特别注意!不是在内部函数调用的时候产生,而是内部函数定于预解析的时候就会产生闭包!
接下来我们来举一个例子来说明这一过程。
分析:此时是正在执行23行代码的时候,跳到了16行,由于有函数提升,因此这个时候内部函数对象以及创建,因此闭包已经产生。
当一个闭包我们不需要它的时候,就将其引用变量置为null,这样才能使闭包所占的内存空间得以释放。
若以变量的方式声明函数:
则是在17-20行函数全部执行完了才会产生闭包。
五、闭包的应用
定义自定义的JS模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包含n个方法的对象或函数
- 模块的使用者,只需要通过模块表露的对象调用方法来实现对应的功能
①法1:我们首先创建一个.js后缀文件。
function myModule() {
//私有数据
var msg = 'I Love SWPU';
//操作数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase());
}
function doOtherthing() {
console.log('doOtherthing() ' +msg.toLowerCase());
}
//返回的是一个对象
//向外暴露对象(给外部使用的方法)
return {
//字符串:函数名
doSomething:doSomething,
doOtherthing:doOtherthing
}
}
②法1:并在另外一个.html后缀文件中对其进行使用。
<body>
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule();
module.doSomething();
module.doOtherthing();
</script>
</body>
另外一个更加简洁的定义模块方式(不需要在对这个模块进行声明,而是直接使用)——通过匿名函数自调用,将这个模块直接添加为window的属性。
①法2:我们首先创建一个.js后缀文件。
(function(){
//私有数据
var msg = 'I Love SWPU';
//操作数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase());
}
function doOtherthing() {
console.log('doOtherthing() ' +msg.toLowerCase());
}
//把要暴露的东西添加为window的属性
window.myModule2={
doSomething:doSomething,
doOtherthing:doOtherthing
}
})()
②法2:并在另外一个.html后缀文件中对其进行使用。
<body>
<script type="text/javascript" src="myModule2.js"></script>
<script>
myModule2.doSomething();
myModule2.doOtherthing();
</script>
</body>
针对于第二种模块添加,这里分享一个题外小技巧(提高执行效率)。
//一种更普遍的写法,会有代码压缩
(function(window){
//私有数据
var msg = 'I Love SWPU';
//操作数据的函数
function doSomething() {
console.log('doSomething() ' + msg.toUpperCase());
}
function doOtherthing() {
console.log('doOtherthing() ' +msg.toLowerCase());
}
//把要暴露的东西添加为window的属性
window.myModule2={
doSomething:doSomething,
doOtherthing:doOtherthing
}
})(window)
六、闭包的缺点及其解决
闭包虽然 使得函数内部变量在函数执行完以后,任然可以存活在内存中(延长了局部变量的生命周期),也让程序员在 函数外部可以操作(读写)到函数内部的数据(变量/函数)。但是他也带来了一些缺点。
若对内存泄漏这个名字有一定疑惑,可以看一下我的另外一篇博客。
为了解决这个问题。
我们可以通过两个方式:
①能不使用闭包就不使用
②及时释放闭包
显而易见的是,第一种方式基本是无法避免的,不然也就不会有闭包的产生了,因此我们举个例子说明第二种方法——及时释放。
如果觉得这篇博客就点赞+评论+收藏吧!给予小白程序员一些鼓励!