前言
函数和作用域啥的我们前面已经了解了,现在就要学习闭包了,这是一个挺晦涩的知识点,初学者可能会感觉不好理解,但是高手都不不以为然了,高手就给我提点意见吧,我和新手一起来学习什么是闭包。
例子
先不说定义,先看一个题,看看大家能得出正确的结果不,
function test(){ var arr = []; for(var i = 0;i<10;i++){ arr[i] = function(){ return i; } } return arr; } var fns = test(); console.log(fns[9]()); // 值是多少? console.log(fns[0]());//值是多少?
结果就是
10 10
View Code
你做对了吗?
什么是闭包
我们知道,javascript中的变量作用域分为全局变量和局部变量,全局的变量我们在什么地方都可以使用,但是局部变量就不是这样的了,我们只能在该变量的作用域中得到,换句话说就是我们在函数的内部可以使用函数外部的变量,但是我们在函数的外部却不能使用函数内部定义的局部变量,但是在实际中我们就是想要在函数的外部使用函数内部定义的变量那该怎么办呢?例子来了
function test(){ var inner = 10; } alert(inner);//error?咋办
咋办呢?我们知道,在内部我们可以访问到这个变量,我们还知道有一个操作符return可以返回想要的值,那我就在内部定义一个函数来访问这个变量,然后在返回这个函数不就行了,实践一下
function test(){ var inner = 10; function inFun(){ alert(inner);// }; return inFun; } var outter = test(); outter();//10;
我们做到了,为自己鼓鼓掌,有时候我们就该不断鼓励自己一下,不要给自己太大的压力,我们不是富二代,在不鼓励一下自己怎么能成为富二代他爹呢。
这就是闭包了,官方没有给出闭包一个完整的准确的定义,民间流传的是在一个函数内定义一个函数,并且这个内部函数可以在外面访问,这时候就形成了闭包。看看上面函数的结构,一个函数返回了一个内部函数,我们知道在正常情况下,一个函数执行结束之后,里面的变量会被释放,也就是说,在test()这句执行之后,里面的inner应该被释放了才对,但是我们发现,outter()时我们拿到了inner的值,这就是闭包的特性:如果闭包中使用了局部的变量,那么这个变量会一直贮存在内存中,闭包会一直保持这个值,一直到外部的函数没有被引用为止,看例子
function closure(){ var num = 0; function add(){ console.log(++num); } return add; } var test1 = closure();//形成一个闭包,保持着自己的一个num变量 test1 ();//1 test1 ();//2 var test2 = closure();//又一个闭包,保持了一个自己的num变量 test2 ();//1 test2 ();//2
好玩不?这就是闭包的神奇的地方,也是让身为初学者的我们感到彷徨的地方,相信我,我会让你们理解明白的。要想释放num占用的内存,就该这样
test1 = null; test2 = null;
简单解析下这个例子:在执行 var test1 = closure()时,由于closure()返回到是一个函数,这里就相当于test1变量指向了一个函数add,但是这个add函数有自己的作用域和活动对象,都存在了test1中,执行test1()时,会寻找num变量,由于闭包存储了该变量就可以直接取到,并且自加1,再一次执行test1()时会继续在test1执行的add函数的执行环境和作用域中查找,发现num为1了,就找到了这个num;在执行var test2 = closure()时,会重新创建一个闭包,重新存储执行环境和活动对象,所以这是和第一次完全没有关系的。
闭包的机制
- 函数也是对象,有[[scope]]属性(只能通过JavaScript引擎访问),指向函数定义时的执行环境上下文。
- 假如A是全局的函数,B是A的内部函数。执行A函数时,当前执行环境的上下文指向一个作用域链。作用域链的第一个对象是当前函数的活动对象(this、参数、局部变量),第二个对象是全局window。
- 当执行代码运行到B定义地方, 设置函数B的[[scope]]属性指向执行环境的上下文作用域链。
- 执行A函数完毕后,若内部函数B的引用没外暴,A函数活动对象将被Js垃圾回收处理;反之,则维持,形成闭包。
- 调用函数B时,JavaScript引擎将当前执行环境入栈,生成新的执行环境,新的执行环境的上下文指向一个作用域链,由当前活动对象+函数B的[[scope]]组成,链的第一个对象是当前函数的活动对象(this、参数、局部变量组成),第二个活动对象是A函数产生的,第三个window。
- B函数里面访问一个变量,要进行标志符解析(JavaScript原型也有标识符解析),它从当前上下文指向的作用域链的第一个对象开始查找,找不到就查找第二个对象,直到找到相关值就立即返回,如果还没找到,报undefined错误。
- 当有关A函数的外暴的内部引用全部被消除时,A的活动对象才被销毁。
这段是其他的地方的,就是说了执行环境和作用域的理解闭包怎么维持变量的。
闭包的应用
一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,这既是函数也是弊端。我们可以利用闭包封装一些私有的属性,例如
var factorial = (function () { var cache = []; return function (num) { if (!cache[num]) { if (num == 0) { cache[num] = 1; } cache[num] = num * factorial(num - 1); } return cache[num]; } })();
封装了一个内部私有的属性来缓存结果。
下面流行的模块模式,它允许你模拟公共,私有以及特权成员
var Module = (function(){ var privateProperty = 'foo'; function privateMethod(args){ //do something } return { publicProperty: "", publicMethod: function(args){ //do something }, privilegedMethod: function(args){ privateMethod(args); } } })();
另一个类型的闭包叫做立即执行函数表达式,是一个在window上下文中自我调用的匿名函数:
(function(window){ var a = 'foo'; function private(){ // do something } window.Module = { public: function(){ // do something } }; })(this);
闭包的弊端
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
解释例子
回到开始的例子,这是闭包的经典的例子,这个和其他的例子和有些不一样,我们分析一下,这里用了一个数组,其实这里我们执行一次var fns = test(),形成了10个闭包,数组的每一个项存了一个闭包,这与其他的例子是不一样的,其他的例子是函数执行一次形成了一个闭包,所以这个10个闭包的初始的执行环境是一样的,每一个闭包使用了i这个变量,这个变量在函数var fns = test()执行之后变为了退出循环的那个i的值10,JavaScript是解释型的语言,所以在执行数组中的闭包的时,会找到此时i的值10;看看arr的结果
现在想怎样解决这个问题呢?我们想想,这10个闭包形成时的执行环境和活动对象是一样的,现在考虑的就是要在初始时就不一样,我们知道函数的作用域是一层一层的,那我们就需要在这之间家一层作用域,这层作用域要有不同的i的值,我们想到了自执行匿名函数,(funciton(){})(),我们把i的值穿进去,按值传参就是相当于复制了一份变量嘛,在(funciton(){})()外部的作用域中的i的值的改变不会改变内部的i的值,试一下
function test(){ var arr = []; for(var i = 0;i<10;i++){ (function(i){ arr[i] = function(){return i;}})(i); } return arr; } var fns = test(); console.log(fns[9]()); // 值是9 console.log(fns[0]());//值是0
当然也可以这样
function test(){ var arr = []; for(var i = 0;i<10;i++){ arr[i] = (function(i){return function(){return i}})(i); } return arr; } var fns = test(); console.log(fns[9]()); // 值是9 console.log(fns[0]());//值是0
这两个的实质都是在闭包形成之前,给每一个闭包包上一层作用域,在这个作用域中传一个参数,是每一个闭包上一级的作用域中都有不同的i。当然还有其他的办法这里不说了。
小结
闭包的应用场景挺多的,在模块化编程中很重要的,有些地方说函数也是闭包,还是那就话,概念不重要,理解会用才是最现实的。