今天呢咱们来聊聊这个js闭包,我们基本上在面试中,必然会问到的问题:什么是闭包?说说你对闭包的理解.闭包的作用是什么?
闭包也是一个很不好理解的概念,往往我们遇到的机会很多很多,很多朋友呢都说了对闭包的理解,问题表达的方式不一样,但是呢,最后都对闭包没有很清晰的理解.所以呢我这边就帮助大家理解什么是闭包.其实说起来,可以深,也可以浅.先由浅着说.之前呢,在网上也是找了不少的资料,看见人家理解的闭包,我提取出了说法有问题 的4点:
1.闭包是指有权访问另一个函数作用域中变量(参数)的函数(不可取)
2.闭包就是能读取其他函数内部变量的函数(不可取)
3.闭包可以理解成定义一个函数内部的函数(不可取)
4.函数就是闭包(不可取)
这4点呢,其实呢,怎么说呢,不能否认它是错的,只能说不严谨,第一点,可以得到一个结论,闭包是一个函数,第二点也差不多的意思,第三点有意思了,定义一个函数内部的函数,的确有这个特征,而第四点,其实也是对的,因为MDN上的解释是:闭包是一个特殊的函数对象.那上面的几种说法都是不严谨的,其实最终我查询资料,都归纳了一句话:
当一个函数能够记住并访问到其所在的词法作用域及作用域链,特别强调是在其定义的作用域外进行的访问,此时该函数和其上层执行上下文共同构成闭包
怎么理解这句话呢,其中包含两个新的名词,词法作用域及作用域链,这个我们扩展再说,这里我们直接当做作用域就行了,所以这里就可以知道,闭包肯定是跟作用域有关的,而且作用域还是很大的.然后下一句是在其定义的作用域外进行的访问.这是上面4点都没有讲到的.然后再就是该函数,也就是内部函数和其上层执行的上下文,共同构成闭包,所以闭包是一个整体.说了这么多还是很抽象,那需要明确下列几点:
1.闭包一定是函数对象
2.函数内保持对上层作用域的引用
3.闭包和词法作用域,作用域链,垃圾回收机制等息息相关
4.当函数在其定义的作用域外进行访问时,才产生闭包
5.闭包是由该函数和其上层执行上下文共同构成
//闭包函数对象
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
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函数内部的变量
这两段代码对比一下,第一段代码因为作用域和函数生命周期的关系,定义在fn函数中的变量i在调用完毕之后,就被垃圾回收机制给回收了,所以每次调用都是等于1,而第二段代码,咱们可以在函数外包操作outerFn函数内部的变量,所以每次调用执行res(),都相当于改变了i的值,并没有被垃圾回收机制给回收.这2个示例体现了什么呢,就是我们要讲的闭包的2点应用
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上,会有很大的问题,因为闭包可以让变量常驻内存,会造成内存泄露,现在高版本浏览器会有它自己的释放方法.如果让咱们自己释放怎么释放呢
释放内存:
//闭包会造成内存泄露的问题,所以页面在解构的时候,直接清除
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;
}
这也是一个闭包.所以呢,我们是经常在写闭包的,只不过是自己没有注意罢了,以后面试别说自己没有写过闭包,因为这是不可能的.通过上面我们对闭包的探究,那可以给大家总结一下,闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。闭包会发生内存泄漏,每次外部函数执行的时 候,外部函数的引用地址不同,都会重新创建一个新的地址。但凡是当前活动对象中有被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象。下面几个闭包的例子,大家可以看一下,仔细琢磨代码:
注意,此示例和结论引用 https://blog.csdn.net/weixin_43586120/article/details/89456183 的示例,里面示例更多,有兴趣可以去琢磨,看完下面示例之后,可以带入这个结论,说的很正确.结论闭包找到的是同一地址中父级函数中对应变量最终的值
function outerFn(){
var i = 0;
function innerFn(){
i++;
console.log(i);
}
return innerFn;
}
var inner = outerFn(); //每次外部函数执行的时候,外部函数的地址不同,都会重新创建一个新的地址
inner();
inner();
inner();
var inner2 = outerFn();//重新创建了一个新的引用地址
inner2();
inner2();
inner2()
//结果是 1 2 3 1 2 3
var i = 0;
function outerFn(){
function innnerFn(){
i++;
console.log(i);
}
return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2();
//结果是 1 2 3 4 i是全局的变量,改的是全局变量
(function() {
var m = 0;
function getM() { return m; }
function seta(val) { m = val; }
window.g = getM;
window.f = seta;
})();
f(100);
console.info(g()); //100 闭包找到的是同一地址中父级函数中对应变量最终的值
function love1(){
var num = 223;
var me1 = function() {
console.log(num);
}
num++;
return me1;
}
var loveme1 = love1();
loveme1(); //输出224 这里作用域内var变量进行了声明提升,在累加之前,函数未调用
扩展:
上面呢,咱们说了两个概念,词法作用域和作用域链,作用域链呢咱们这里不讲,挖的深了就很多了,不易理解,我这里简单的扩展下词法作用域,词法作用域也叫静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变,咱们的JavaScript就是使用的词法作用域.看个例子:
//词法作用域
var abc=1;//全局
function fn(){
console.log(abc);//这里注定是调用的全局的变量abc
}
function fx(){
var abc=2;//fn函数无法访问到这里的abc
fn();
}
fx();
看上面,很普通的一段代码,咱们上面讲了闭包,肯定很多人以为这个答案会输出2,但其实我告诉你,这个答案是输出1,为什么呢,这个就跟词法作用域有关.词法作用域(静态作用域)有很重要的一条,词法作用域关注函数在何处声明.好好想想,第一个abc是全局的,下面函数fn是直接打印的abc,那你认为fn中的abc会去能访问到fx函数中的变量a吗,肯定不是.当然了,如果你给fn函数把abc当做一个形参传递,那肯定是可以访问到abc的值的,会打印2.,这就是词法作用域,关注函数在何处声明.
那好,既然有静态作用域,那肯定有动态的作用域,
动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时静态确定的.那说到这里,咱们js中有那块非常类似这个动态作用域呢?没错,就是this,上代码,看着更直观点
function fx(){
console.log(this);//根据调用fx情况来指向哪里
}
fx();//此时this 是指向window的
//页面上有个按钮,获取btn
oBtn.onclick=function(){
console.log(this);//这里的this就是指向按钮oBtn了
fx();
}
//这个就是类似的动态作用域
动态作用域关注的是函数从何处调用,词法作用域和动态作用域的主要区别就是:词法作用域是在写代码或者定时确定的,而动态作用域是在运行时确定的.
好了,闭包的基本知识就讲到这里了,这是从浅处去挖,还没有到深处,各位大神有兴趣,可以评论区指教下,后面的词法作用域和动态作用域,当做是扩展的了,希望大家能明白.