闭包
一.背景
我们基本上在面试中,必然会问到的问题:什么是闭包?说说你对闭包的理解.闭包的作用是什么?
闭包也是一个很不好理解的概念,往往我们遇到的机会很多很多,很多朋友呢都说了对闭包的理解,问题表达的方式不一样,但是呢,最后都对闭包没有很清晰的理解.所以呢我这边就帮助大家理解什么是闭包.其实说起来,可以深,也可以浅.先由浅着说.之前呢,在网上也是找了不少的资料,看见人家理解的闭包,我提取出了说法有问题 的4点:
1.闭包是指有权访问另一个函数作用域中变量(参数)的函数(不可取)
2.闭包就是能读取其他函数内部变量的函数(不可取)
3.闭包可以理解成定义一个函数内部的函数(不可取)
4.函数就是闭包(不可取)
这4点呢,其实呢,怎么说呢,不能否认它是错的,只能说不严谨,第一点,可以得到一个结论,闭包是一个函数,第二点也差不多的意思,第三点有意思了,定义一个函数内部的函数,的确有这个特征,而第四点,其实也是对的,因为MDN上的解释是:闭包是一个特殊的函数对象.那上面的几种说法都是不严谨的,其实最终我查询资料,都归纳了一句话:
二.概念
当一个函数能够记住并访问到其所在的词法作用域及作用域链,特别强调是在其定义的作用域外进行的访问,此时该函数和其上层执行上下文共同构成闭包
怎么理解这句话呢,其中包含两个新的名词,词法作用域及作用域链,这个我们扩展再说,这里我们直接当做作用域就行了,所以这里就可以知道,闭包肯定是跟作用域有关的,而且作用域还是很大的.然后下一句是在其定义的作用域外进行的访问.这是上面4点都没有讲到的.然后再就是该函数,也就是内部函数和其上层执行的上下文,共同构成闭包,所以闭包是一个整体.说了这么多还是很抽象,那需要明确下列几点:
1.闭包一定是函数对象
2.内部函数保持对上层作用域的引用
3.闭包和词法作用域,作用域链,垃圾回收机制等息息相关
4.内部函数在其定义的作用域外进行访问时,才产生闭包
5.闭包是由该内部函数和其上层执行上下文共同构成
1. 常见闭包形式函数嵌套函数
2. 内部函数保持对上层作用域的引用
3. 内部函数在声明的作用域之外访问
4. 此时,内部函数和其上层执行上下文共同构成闭包
//闭包函数对象
function fn() {
var a = 3;
return function () {
return ++a;//引用了fn下的a 保持对变量a的引用(上层作用域)的引用
}
}
//fn和其中的匿名函数共同构成
var res=fn();
res();//在外部进行访问,形成闭包
三.示例
上述标重点的才是闭包的必要条件.所以呢,日后面试官问起,什么闭包,我可以这样说:我们通常所用的闭包是函数嵌套函数的形式,内部的那个函数通过return出来,然后内部的函数又保持对上层作用域的引用,而且内部函数还必须要在外部调用,这个时候整个结构,内部外部结构,整个就形成了闭包 这里仅仅只是说的闭包的概念和理解,那具体有什么作用呢,上代码先
function Fn(){
var i=0;
i++;
alert(i);
}
Fn();//1
Fn();//1
Fn();//1
123456789
var i=10;//并不会污染函数内部的变量
function outerFn() {
var i = 0;
return function () {
i++;
console.log(i);
}
}
var res = outerFn();//res指向返回的函数
res();//1
res();//2
res();//3
//浏览器运行结果可以得出结论:可以操作outerFn函数内部的变量
1234567891011121314
这两段代码对比一下,第一段代码因为作用域和函数生命周期的关系,定义在fn函数中的变量i在调用完毕之后,就被垃圾回收机制给回收了,所以每次调用都是等于1,而第二段代码,咱们可以在函数外包操作outerFn函数内部的变量,所以每次调用执行res(),都相当于改变了i的值,并没有被垃圾回收机制给回收.这2个示例体现了什么呢,就是我们要讲的闭包的3点应用
四.闭包应用
1.优点
1.在函数外读取函数内部的变量(避免全局变量的污染)
2.让局部变量的值能够被保存下来(可以让变量常驻内存)
3.将模块的公有属性和方法暴露出来
第三点应用是下面要讲的,为的是把闭包的作用总结在一起,方便大家理解,那咱们来看第三点,体现在什么地方呢
//模块化写法
var moduleB = (function () {
var num = 100;//私有属性
var rem = 200;//公有属性
function add() {//公有方法
num++;
console.log(num);
}
function divide() {//公有方法
num = num / 10;
console.log(num);
}
function show(){ //私有方法
console.log(num);
}
//将需要暴露出去的属性和方法return出去
return {
add: add,
divide: divide,
rem: rem
}
})();
moduleB.add();//101
moduleB.divide();//10.1
moduleB.show();//moduleB.show is not a function
console.log(moduleB.rem);//200
那如果问你,咱们平时闭包的应用还有哪些呢,很多人说用的很少,乃至没有,其实闭包离我们很近,只是我们没有去发现它是闭包而已.比如很多事实咱们习惯将js代码写在头部,那就肯定会写一个页面文档加载的事件,来,看代码说话:
window.onload=function(){
var oDiv=document.querySelector("div");
oDiv.onclick=function(){
alert(123);
}
}
这段代码咱们是不是很熟悉,是不是很普通,这代码是基本每天都写得,我可以肯定的告诉大家,这里咱们就写了个闭包!说是闭包,那符合咱们上面说的闭包的3点条件吗,3点条件大家再回头看一下:
1.函数内保持对上层作用域的引用
2.当函数在其定义的作用域外进行访问时,才产生闭包
3.闭包是由该函数和其上层执行上下文共同构成
那问题来了有人说第一点就不符合,那里来的对上层作用域的引用呢?其实大家忘了,这个onclick事件函数内部有一个隐式的引用,不管用不用,它就在那里,没错,就是this,这个this的指向是oDiv,而oDiv的确是上层作用域的变量.那第二点,当函数在其定义的作用域外进行访问,产生闭包,咱们的onclick事件是不是全局事件啊,触发这个事件也是相当于在全局调用了.第三点,onload事件和onclick的事件,这一个整体,是不是也符合了咱们的内部函数和其上层执行上下文共同构成的条件.所有呢,闭包是不是离咱们很近,其实这个函数会在以前的低版本IE上,会有很大的问题,因为闭包可以让变量常驻内存,会造成内存泄露,现在高版本浏览器会有它自己的释放方法.如果让咱们自己释放怎么释放呢
2.缺点
闭包可以让变量常驻内存,会造成内存泄露
闭包的缺点就是会让变量常驻内存,解决方法是 释放内存:
//闭包会造成内存泄露的问题,所以页面在解构的时候,直接清除
window.onunload = function () {
oDiv = null;
oDiv.onclick=null;
}
还有一个应用,比如页面上的ul标签,里面有很多个li,咱们在获取当前点击的li的下标的时候,运用闭包会遇到,有人说用什么闭包啊,事件委托啊,但是事件委托真的可以获取下标吗,答案是不可以的.看代码:
var oLis = document.querySelectorAll("ul li");
for (var i = 0; i < oLis.length; i++) {
//自执行函数
(function (i) { // i形参
oLis[i].onclick = function () {
console.log(i);//点击li,输出当前li的下标
}
})(i);//实参
}
//闭包会造成内存泄露的问题,所以页面在解构的时候,直接清除
window.onunload = function () {
oLis = null;
}
```
这也是一个闭包.所以呢,我们是经常在写闭包的,只不过是自己没有注意罢了,以后面试别说自己没有写过闭包,因为这是不可能的.通过上面我们对闭包的探究,那可以给大家总结一下,闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。闭包会发生内存泄漏,每次外部函数执行的时 候,外部函数的引用地址不同,都会重新创建一个新的地址。但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。下面几个闭包的例子,大家可以看一下,仔细琢磨代码: