👸🏾 写在前面:我试图用执行上下文和作用域这些底层原理来对闭包这个概念进行理解,所以我先复习了,执行上下文,又搞清楚了作用域是怎么一回事,最后再回来解释一波闭包。
其实闭包这个概念,在刚开始学习前端随便翻面经的时候我就见过它,三个月前也认真地学习过,做了好些代码阅读题,知道了它是“内部函数引用外部函数的变量使常驻内存”,但其实也只是停留在概念上。后来慢慢地这个地方用到了闭包,那个原来也有闭包,对他的理解不是浮在表面,再到现在去想要理解它的底层实现原理。
好像一直在学习又好像什么新的内容都没有学习,我不知道我是把简单的问题复杂化了,还是这个问题就值得我去花费时间研究。总之,慢慢学习,慢慢探索吧,只要一直在学总不会出错。
一、基本概念
先复盘一下你只要听过闭包就一定会看到的解释
定义:当一个嵌套的内部函数引用了外部函数的变量时,就产生了闭包。
闭包是一个容器,也可以理解为一个对象。(闭包永远跟着内部函数)
产生条件:函数嵌套;内部函数引用外部函数的数据
作用:延长外部函数局部变量的生命周期;从外部访问函数内部的局部变量
缺点:常驻内存,不及时清除可造成内存溢出
如何避免缺点:能不用就不用;及时清除闭包
举个栗子🌰:
function fun(){
var num=123;
var a='aa'; //a并不在闭包里面
function fun2(){ //题外话:如果你看过前面文章作用域那道基础面试题注意区别一下,题目相似,但闭包和作用域考察侧重点不同。
function fun(){
var num=123;
var a='aa'; //a并不在闭包里面
function fun2(){
console.log(num);
}
fun2();
}
fun(); //输出:123
//理解闭包含义👆,闭包运用👇
function fun(){
var num=123;
return function fun2(){ //返回一个新的函数,使用到了外部函数的变量
console.log(num);
}
}
function fun2=fun();
fun2();//输出:123 虽然说在上一步fun()执行完已经销毁了,但是有闭包,延长了外部函数局部变量的生命周期;
fun2=null; //及时清除闭包
console.log(num);
}
fun2();
}
fun(); //输出:123
//理解闭包含义👆,闭包运用👇
function fun(){
var num=123;
return function fun2(){ //返回一个新的函数,使用到了外部函数的变量
console.log(num);
}
}
function fun2=fun();
fun2();//输出:123 虽然说在上一步fun()执行完已经销毁了,但是有闭包,延长了外部函数局部变量的生命周期;
fun2=null; //及时清除闭包
其实闭包也就这么点内容,但我这人总喜欢比来比去。把学过的东西大杂烩一样放一起,这些知识点关联性在哪里,想知道分别发生在那个环节,为什么要这么做,。然后就自己把自己给搞晕了😵,再从头梳理,感觉是给这个迷糊的脑袋瓜子分区归档。所以接下来继续 👇
二、闭包底层原理
举个栗子🌰,分析一下代码的执行过程相信一下子就通透了~(我是搬运工👷🏾♀️)
1: let top = 0;
2: function createWarp() {
3: function add(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return add
8: }
9: let sum = createWarp()
10: let result = sum(top, 8)
11: console.log('result:',result)
执行过程:(全局上下文已经创建,这里说之后的过程)
-
在全局上下文中声明变量
top
并赋值为0. -
2 - 8行。在全局执行上下文中声明了一个名为
createWarp
的变量,并为其分配了一个函数定义。其中第3-7行描述了其函数定义,并将函数定义存储到那个变量(createWarp
)中。 -
第9行。我们在全局执行上下文中声明了一个名为
sum
的新变量,暂时,值为undefined
。 -
第9行。遇到
()
,表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为createWarp
的变量。 明显,已经在步骤2中创建完毕。接着,调用它。 -
调用函数时,回到第2行。(函数在调用时创建函数执行上下文)创建一个新的
createWarp
执行上下文。我们可以在createWarp
的执行上下文中创建自有变量。js
引擎createWarp
的上下文添加到调用堆栈(call stack
)。因为这个函数没有参数,直接跳到它的主体部分. -
3 - 6 行。我们有一个新的函数声明,在
createWarp
执行上下文中创建一个变量add
。add
只存在于createWarp
执行上下文中, 其函数定义存储在名为add
的自有变量中。 -
第7行,我们返回变量
add
的内容。js引擎查找一个名为add
的变量并找到它. 第4行和第5行括号之间的内容构成该函数定义。 -
createWarp
调用完毕,createWarp
执行上下文将被销毁,add 变量也跟着被销毁。 但add
函数定义仍然存在,因为它返回并赋值给了sum
变量。 (ps: 这才是闭包产生的变量存于内存当中的真相) -
接下来就是简单的执行过程,不再赘述
-
……
-
代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。
三、应用场景
-
解决循环遍历加监听
ES6 之前因为 var 没有块级作用域的概念,所以很多的时候,我们必须借助函数function的作用域(即使用闭包)来解决应用外面的变量问题
for(var i=0;i<btns.length;i++){ var btn=btn[i]; (function (i){ console.log(i); btn.onclick=function(){ console.log(i); //∵使用了闭包∴拿到的i不是最后的3 } })(i) }
-
将内部函数返出
function fun(){ var num=123; return function fun2(){ console.log(num); } } function fun2=fun(); fun2(); //输出:123 fun2=null; //及时清除闭包
-
将函数作为实参传递给另一个函数调用
function fun(msg,time){ setTimeout(function (){ console.log(msg); },time) } fun('传入数据',2000);
-
防抖,bind 的实现原理用到闭包
👉【手写代码】call,apply,bind 的区别和实现原理 -
自定义js模块Module
功能:具体特定功能的js 文件,将所有数据和功能都封装在一个函数内部,只向外暴露包含想操作数据的内部方法。模块的使用者只需要通过模块暴露的对象调用方法来实现对应的功能。
学到这儿又到了我的提问环节了
问:为什么闭包会造成内存泄漏?闭包产生的变量存储在哪里?最后怎么回收的?
答:内部函数引用外部函数的变量,这是位于两个作用域,根据垃圾回收机制,被另一个作用域引用的变量不会被回收。
匿名函数调用后被立刻销毁,我们可以利用匿名函数,这样解决闭包问题。
四、面试题
👉【面试题集—No.10】 闭包面试题_Chailo的博客-优快云博客
🚗 参考资料