浅谈JS中的闭包
在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域.
变量的作用域
变量共有两种,一种为全局变量,一种为局部变量.那么全局变量的作用域为: 局部变量只能在定义此变量的函数体内使用,则局部变量的作用域为定义此变量的函数体.而全局变量可以全局使用.局部变量只有在调用函数的时候存在,函数调用完成之后立马就销毁了,而全局变量会一直存在.
首先我在全局变量中定义一个变量a,var a=0;那我们了解到变量a在内存中的存储是window的指针指向栈中的a,栈中的a的指针再指向堆中的0,如下图所示,所以变量a会一直存在.GC是没有办法回收的.
接下来,我定义一个函数,在函数中定义一个变量b,如下代码所示:
function fun(){ var b=5; }
而这时候,变量b在内存中的表现形式如下图所示:
在我们不调用这个函数的时候,并没有初始指针指向b,所有JS的GC会立马把局部变量给清理掉.而在调用函数时,调用函数的指针指向b,b右指向5,所以在调用函数的过程中b是在内存中存在的.而调用函数执行完之后,调用函数指针不再指向b,所以GC有会把变量给清除掉.
接下来让我们用一个例子来理解一下变量的作用域.
var a =0; function fun(){ var b=5; console.log(a,b); } function fun1(){ console.log(a,b); } fun(); fun1();
这段代码的输出结果如下图:我们发现在fun1的时候提示我们没有定义b,所以fun1不能访问fun中的局部变量b.
基本的闭包
在上面例子中,我们可以非常确定的知道fun1并不能访问fun中的局部变量b,但是在我们工作中我们往往需要一个函数访问另外一个函数的局部变量,那这时候我们怎么做呢?创建闭包!创建闭包!创建闭包!重要的事情说三遍.
闭包的作用:现在我们可以理解为闭包有两个作用:1.读取函数内部的局部变量 2. 让这个变量的值始终保持在内存中.
听起来有点抽象,我们可以通过一个例子来充分的理解它,代码如下:
function fun() { var b = 5; function fun1 () { alert(b); } return fun1; } var result = fun(); result();
那我们现在来分析一下这个例子,如下图所示:
首先我们先声明一个函数fun,然后在函数里面创建一个局部变量b,接着在fun里面定义一个fun1,然后把fun1当做fun函数的返回值返回出去.在函数的外部我们调用fun,把返回的结果赋值给result,那么此时result和fun1是相等的,我调用result就相当于调用了fun1.然后就相当于在fun函数外部,访问了fun函数的内部局部变量.
封闭的作用域
封闭作用域: 又称值为封闭空间,还有一个昵称叫小闭包,以及匿名函数自调。
最大目的:全局变量私有化
作用:
- 不污染全局空间.
- 内部所有的临时变量执行完毕都会释放不占内存。
- 可以保存全局数据
- 更新复杂变量.
我们通过实际案例来学习了解这些抽象的概念,代码如下:定义全局变量a,var a =0;这时候我们通过作用域章节会非常清楚的之后变量a会一直存在在内存中,但是如果我们不想让他一直存在,就可以使用闭包实现.如下代码:
(function (){ var a =0; })();
这时候,就相当于我们把a放在闭包函数中,a相当于局部变量,从而实现全局变量私有化.这种闭包形式的写法多种,比较常用的有如下几种:
//普通写法 (function () { })(); //文艺写法 ;(function () { })(); //其它 二逼写法 +(function () { })(); -(function () { })()
文艺写法在普通写法的基础上增加了一个; 可以理解为文艺青年怕上句代码没写分号,然后担心编辑器解析的时候当做一句话解析了,所以给前面加个分号.
作用域链条
在一个作用域中,如果要使用属性的话,那么会做如下两件事儿:
1、先递归遍历寻找当前作用域中有没有这个属性, 如果有,则用, 如果没有,则会顺着链条继续往上寻找
2、递归: 多层遍历;直到找到属性为止
注意点:在作用域链条中,遍历只能从下而上, 不能由上而下
这个问题很好理解.比如有方法 a中有变量strA,又有方法b,方法b中又有变量strB,如果我们在b使用a,那么系统会在方法b中遍历所有的属性,然后看有没有这个,如果没有这个属性,则顺着作用域链条往上寻找,找方法a中有没有这个属性.就这样依次类推,直到找到属性为止.
那么在理解了作用域链条以及属性的使用之后,下面有两个建议给大家:
1、尽量不要使用全局变量
2、作用域链不要太多
因为如果使用全局变量,系统沿着作用域链条会找一层一层向上找,直到找到window作用域,而window作用域下的属性非常的多,遍历的时候要执行很多次,所以不建议大家使用全局变量,并且在使用全局变量的时候,一定要注意,它的值什么时候应该改变,什么时候不应该改变,如果这个问题处理不好的话,程序很容混乱,最后你自己都不知道输出的结果时什么东西了,所不建议大家使用太多的全局变量。
我们来看一个例子,在body里面放三个button标签,然后给它绑定Id btn1 btn2 btn3,接下来要求我们拿到这三个标签.很简单直接通过document.getElementById即可.很快就可以写完,代码如下:
var btn1 = document.getElementById('btn'); var btn2 = document.getElementById('btn'); var btn3 = document.getElementById('btn');
那么我们在理解了作用域链条之后,有没有觉得这个代码可以优化?先在的情况时我每次使用到document的时候都会遍历一遍window,是不是很浪费资源?当然三个button还不明显,如果我有100个标签,是不是我就要遍历100次window?这样很浪费,那我们来想一下,我们的代码可不可以这样改一下:如下所示
(function () { var a = document; var btn1 = d.getElementById('btn'); var btn2 = d.getElementById('btn'); var btn3 = d.getElementById('btn'); })();
这样修改完之后,我们是不是只需要遍历依次window就可以了?只需要在创建a,给a赋值的时候遍历window,其它情况下只需要遍历闭包里面的属性是不是就可以了?这样效率有没有提高?这也是使用闭包的一个明显的作用,全局变量私有化!!!
闭包经典案例
其实写到这里,闭包已经基本介绍完了,但是昨天我在睡觉之前,看了一篇微博,博主@Damonare也发了一篇闭包的文章,文章的最后,它放了几道闭包的经典案例,我觉得涵盖面很广几乎涉及到了闭包里面的所有知识,但是他没给解释,那么今天我就分析一下这些案例,帮助一些闭包掌握不好的同学理解理解。
案例1
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()());
首先,我们要分析代码,如下图所示:
我们来分析一下,输出语句中的objuct.getNameFunc 的值为json中第二对键值对的value,是一个大的方法体function(){
return function(){
return this.name;
};这个没什么疑问.那么我们在继续看objuct.getNameFunc()就是调用第二对键值对的value方法,那么返回值为function(){
return this.name;
};这个方法,也没什么疑问.接下来关键点来了,我们再调用这个返回函数,返回的是this.name,对不对,我们输出的为this.name,那么this.name会是什么呢?我们都知道this所在的函数在哪个对象中, this就指向对象,还可以这样理解,谁调用this所在的那个函数,this就指向谁,那么在这个案例中我们可以这样来分解一下: object.getNameFunc()(),接下来我们来分析一下这句话,是不是可以理解为调用object.getNameFunc()方法,那么谁调用呢?明显是window,那么this就指向window,所以this.name=The Window。
案例2
我们在案例1的基础上修改一下代码,代码如下:
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ var that = this; return function(){ return that.name; }; } }; alert(object.getNameFunc()());
同样,我们也来分析一下代码,如下图所示:
上面我们已经分析到了,输出的结果的执行代码为that.name,那么我们看一下that为function(){
var that = this;
return function(){
return that.name;
};的this,那么我们来调用一下这个函数,object.getNameFunc(),这样调没什么问题吧,那么我们就知道了这里的this指向object,那么object是一个json 它的那么为My Object.所以输出的结果为My Object.
案例3
function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } var a = fun(0); a.fun(1); a.fun(2); a.fun(3); var b = fun(0).fun(1).fun(2).fun(3); var c = fun(0).fun(1); c.fun(2); c.fun(3);
拿到代码之后肯定是要分析,如下图所示:
首先我们来看a ,定义一个变量a=fun(0),那么肯定会执行fun中输出第二个参数的代码,而我们并没有传第二个参数,那么输出的结果肯定是undefined.那么这时候
a={
fun:function(m){
return fun(m,n);
}
所以,a.fun(1);就是fun(1,n),由于n是我们定义a的时候传入的参数,所以a.fun(1)===fun(1,0);所以输出0,同理a.fun(2); a.fun(3);输出的结果也都是0.故第一段输出代码输出的结果为undefined 0 0 0。
接下来我们在看第二句输出代码fun(0).fun(1).fun(2).fun(3);我们先看第一个fun(0),和上面一样输出undefined,那么fun(0).fun(1)也是和上面一样,输出0,不再解释.接下来看fun(0).fun(1).fun(2),因为我们刚才讲了fun(0).fun(1)=== fun(1,0),所以fun(0).fun(1).fun(2)=== fun(1,0).fun(2),就等于fun(2,1),所以输出1,那么fun(2,1).fun(3) 就等于fun(3,2),所以输出结果为2 ,故第二段输出代码输出结果为undefined 0 1 2。
最后我们来看第三段代码,和第二段一样fun(0)输出undefined,fun(0).fun(1)输出0,fun(0).fun(1).fun(2)===fun(2,1) 输出1 fun(0).fun(1).fun(3)===fun(3,1) 输出1 故第三段输出代码输出的结果为undefined 0 1 1
所有的输出结果如下图所示:
总结:
闭包在js中使用的比较常见,一定要掌握.但是如果最后的案例如果你都可以轻松的说出答案的话,那么恭喜你,闭包你已经理解的比较透彻了.