目录
目录
最近关于js闭包的文章看了好多,主要有两点认识:(1)闭包很重要:前端面试,必问闭包;(2)彻底把闭包搞清楚很不容易。但是我还是想写一下我对闭包的理解,不久之前,我写过一篇函数预编译的文章链接是:https://blog.csdn.net/weixin_44164982/article/details/107314659 主要介绍了函数执行期上下文的建立过程,如果有需要了解之前知识的朋友,可以先去看一下。
今天这篇文章主要是实现对闭包的初步理解:
1、闭包的简介
课本上对闭包的描述:
- 闭包,就是有权访问其外部作用域中的变量和参数的函数。
- 闭包是能够读取其他函数内部变量的函数。只有函数内部的子函数才能读取局部变量,在本质上,闭包是函数内部和函数外部连接起来的桥梁。
- 当函数可以记住并访问所在词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
4.在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
2、闭包的特点
- 内部函数被返回到外部并保存时,会生成闭包(重点)
- 可以读取自身函数外部的变量(沿着作用域链寻找)先从自身开始查找,如果自身没有才会继续往上级查找,自身如果拥有将直接调用。(哪个离的最近就用哪一个)
- 可以延长内部变量的生命周期(产生在作用域上,不能被销毁)
- 函数fn2嵌套在函数fn1内部
- 函数fn1返回函数fn2
3、闭包的形成
如下方代码所示:
function fn1() { //外部函数fn1
var a=123;
function fn2(){ //内部函数fn2
var b=234;
console.log(a)
}
return fn2; //外部函数fn1返回内部函数fn2
}
var glob=100;
var demo=fn1();
demo();
执行结果如下:
在上面代码中,fn2函数就是闭包。
在上面的代码中,函数fn2就被包括在函数fn1内部,这时fn1内部的所有局部变量,对fn2都是可见的。但是反过来就不行,fn2内部的局部变量,对fn1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然fn2可以读取fn1中的局部变量,那么只要把fn2作为返回值,我们就可以在f1外部读取它的内部变量。
接下来我们在讨论具体执行过程之前先了解一下什么是作用域链?
作用域链:[[scope]]中存储的执行期上下文对象的集合,这个链呈链式链接,把这种链式链接叫做作用域链。作用域链的查找顺序是从作用域链的顶端依次向下查找。
运行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。
上面函数的具体执行过程为
(1)fn1函数定义,[[scope]]全局执行期上下文GO,如下图所示
(2)fn1函数执行,[[scope]]又存入fn1函数执行期新建的上下文AO,第0位是AO,在作用域链顶端;第1位是GO。这里强调:作用域链的查找顺序是从作用域链的顶端依次向下查找。
(3)fn1函数执行过程中触发fn2函数的定义, fn2刚被定义时,是直接引用的是fn1函数的作用域链,直接存入fn2的[[scope]]中。
这里我们要理解具体发生了什么:
(4)fn2函数执行,生成fn2函数自己的执行期上下文AO,并存入fn2[[scope]]的最顶端。
(5) fn2函数执行完之后,销毁fn2自己生成的执行期上下文AO。
此时看代码,发现fn1其实也执行完了,fn1自己的执行期上下文此时就要销毁,要回归到被定义状态。但是:,fn2函数在fn1执行完之前被保存到全局,fn2的[[scope]]中还有之前引用的fn1的AO。因此,fn1的执行期上下文实际上是无法销毁的。
然后我们发现,两个函数类似这种结构的,都能形成闭包,比如:
function test(){
var temp=1000;
function fn1(){
console.log(temp);
}
return fn1;
}
var demo=test();
console.log(demo);
4、闭包的作用
一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
在本文例子中,在函数fn1执行完并返回后,闭包使得JavaScript的垃圾回收机制不会收回fn1所占用的资源,因为fn1的内部函数fn2的执行需要依赖fn1中的变量,闭包需要循序渐进的过程。
常用场景:
- 实现公有变量 eg:函数累加器
- 可以做缓存(存储结构) eg;eater
- 可以实现封装,属性私有化 eg: Person);
- 模块化开发,防止污染全局变量
5、闭包的构成
闭包由两部分构成:
- 函数
- 以及创建该函数的环境
6、闭包的缺点
- 闭包会导致多个执行函数共用一个公有变量,如果不是特殊需要,应尽量防止这种情况发生。
- 滥用闭包会造成内存泄露。因为闭包中引用到的包裹函数中定义的变量都永远不会被释放,所以我们应该在必要的时候,及时释放这个闭包函数。
什么是内存泄露?
内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。
7、使用闭包的注意事项
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
例子面包店销售
<script>
function breadMgr(num){
var breadNum=arguments[0]||10;//实参没给值默认是10个
function supply(){
breadNum+=10;
console.log(breadNum);
}
function sell(){
breadNum--;//卖出一个,减少一个
console.log(breadNum);
}
return[supply,sell];//内部函数被返回外部,形成闭包。埋了个钩子,抓住内部函数,也是数据缓存
}
var breadMgr=breadMgr(50);
breadMgr[0]();//60
breadMgr[1]();//59
breadMgr[1]();//58
breadMgr[1]();//57
breadMgr[1]();//56
</script>
例子:
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见的方式,就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量。
在for循环里面的匿名函数执行 return i 语句的时候,由于匿名函数里面没有i这个变量,所以这个i他要从父级函数中寻找i,而父级函数中的i在for循环中,当找到这个i的时候,是for循环完毕的i,也就是5,所以这个box得到的是一个数组里面是5个5。
function box(){
var arr = [];
for(var i=0;i<5;i++){
arr[i] = function(){
return i; //由于这个闭包的关系,他是循环完毕之后才返回,最终结果是4++是5
} //这个匿名函数里面根本没有i这个变量,所以匿名函数会从父级函数中去找i,
} //当找到这个i的时候,for循环已经循环完毕了,所以最终会返回5
return arr;
}
console.log(box()[0]());
同理 ,下面是另外一个例子。
function test(){
var arr=[];
for(var i=0;i<10;i++){
arr[i]=function(){//funcition并没有执行,返回到全局,被return arr;保存了,
console.log(i);
}
}
return arr;//返回数组,此时i=10
}
var myArr=test();
console.log(myArr);//形成闭包,把里面的arr传到了外面 [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
for(var j=0;j<myArr.length;j++){
myArr[j]();//循环执行,打印10个10
}
常见:解决闭包问题
// 解决 用立即执行函数解决。在外面加立即执行函数,保存i
for(var i=0;i<10;i++){
(function(j){
arr[j]=function(){ //funcition并没有执行,只是i++
console.log(j);//0 1 2 3....9
}
})(i)//保存到循环中每一次i的值
}
看一道面试题
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
<li>d</li>
</ul>
<script>
var list=document.getElementsByTagName("li");
for(var i=0;i<list.length;i++){
list[i].addEventListener("click",function(){ //形成闭包,函数写在循环里要考虑是否出现闭包,改为立即执行函数
console.log(i);//只输出结果4
},false);
}
//改为:
for(var i=0;i<list.length;i++){
(function(i){
list[i].addEventListener("click",function(){ //闭包,函数写在循环里要考虑是否出现闭包,改为立即执行函数
console.log(i);//正确输出次序
},false);
}(i))
}