分享

!!JavaScript 闭包究竟是什么

 quasiceo 2016-11-21

!!JavaScript 闭包究竟是什么

用JavaScript一年多了,闭包总是让人二丈和尚摸不着头脑。陆陆续续接触了一些闭包的知识,也犯过几次因为不理解闭包导致的错误,一年多了资料也看了一些,但还是不是非常明白,最近偶然看了一下 jQuery基础教程 的附录,发现附录A对JavaScript的闭包的介绍简单易懂,于是借花献佛总结一下。

 

1.简单的例子

首先从一个经典错误谈起,页面上有若干个div, 我们想给它们绑定一个onclick方法,于是有了下面的代码

复制代码
<div id="divTest">
        <span>0</span> <span>1</span> <span>2</span> <span>3</span>
    </div>
    <div id="divTest2">
        <span>0</span> <span>1</span> <span>2</span> <span>3</span>
    </div>
复制代码
复制代码
$(document).ready(function() {
            var spans = $("#divTest span");
            for (var i = 0; i < spans.length; i++) {
                spans[i].onclick = function() {
                    alert(i);
                }
            }
        });
复制代码

很简单的功能可是却偏偏出错了,每次alert出的值都是4,简单的修改就好使了

复制代码
var spans2 = $("#divTest2 span");
        $(document).ready(function() {
            for (var i = 0; i < spans2.length; i++) {
                (function(num) {
                    spans2[i].onclick = function() {
                        alert(num);
                    }
                })(i);
            }
        });
复制代码

2.内部函数

让我们从一些基础的知识谈起,首先了解一下内部函数。内部函数就是定义在另一个函数中的函数。例如:

function outerFn () {
    functioninnerFn () {}
}

innerFn就是一个被包在outerFn作用域中的内部函数。这意味着,在outerFn内部调用innerFn是有效的,而在outerFn外部调用innerFn则是无效的。下面代码会导致一个JavaScript错误:

 

复制代码
function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                document.write("Inner function<br/>");
            }
        }
        innerFn();
复制代码

不过在outerFn内部调用innerFn,则可以成功运行:

复制代码
function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                document.write("Inner function<br/>");
            }
            innerFn();
        }
        outerFn();
复制代码

 

 

2.1伟大的逃脱

JavaScript允许开发人员像传递任何类型的数据一样传递函数,也就是说,JavaScript中的内部函数能够逃脱定义他们的外部函数。

逃脱的方式有很多种,例如可以将内部函数指定给一个全局变量:

复制代码
var globalVar;
        function outerFn() {
            document.write("Outer function<br/>");          
            function innerFn() {
                document.write("Inner function<br/>");
            }
            globalVar = innerFn;
        }
        outerFn();
        globalVar();
复制代码

 

调用outerFn时会修改全局变量globalVar,这时候它的引用变为innerFn,此后调用globalVar和调用innerFn一样。这时在outerFn外部直接调用innerFn仍然会导致错误,这是因为内部函数虽然通过把引用保存在全局变量中实现了逃脱,但这个函数的名字依然只存在于outerFn的作用域中。

也可以通过在父函数的返回值来获得内部函数引用

复制代码
function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                document.write("Inner function<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();
复制代码

这里并没有在outerFn内部修改全局变量,而是从outerFn中返回了一个对innerFn的引用。通过调用outerFn能够获得这个引用,而且这个引用可以可以保存在变量中。

 

这种即使离开函数作用域的情况下仍然能够通过引用调用内部函数的事实,意味着只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数。而且JavaScript运行时需要跟踪引用这个内部函数的所有变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间(红色部分是理解闭包的关键)。

 

说了半天总算和闭包有关系了,闭包是指有权限访问另一个函数作用域的变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数,就是我们上面说的内部函数,所以刚才说的不是废话,也是闭包相关的 ^_^

 

1.2变量的作用域

内部函数也可以有自己的变量,这些变量都被限制在内部函数的作用域中:

复制代码
function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                var innerVar = 0;
                innerVar++;
                document.write("Inner function\t");
                document.write("innerVar = "+innerVar+"<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();
        fnRef();
        var fnRef2 = outerFn();
        fnRef2();
        fnRef2();
复制代码

 

每当通过引用或其它方式调用这个内部函数时,就会创建一个新的innerVar变量,然后加1,最后显示

复制代码
Outer function
Inner function    innerVar = 1
Inner function    innerVar = 1
Outer function
Inner function    innerVar = 1
Inner function    innerVar = 1
复制代码

 

内部函数也可以像其他函数一样引用全局变量:

复制代码
var globalVar = 0;
        function outerFn() {
            document.write("Outer function<br/>");
            function innerFn() {
                globalVar++;
                document.write("Inner function\t");
                document.write("globalVar = " + globalVar + "<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();
        fnRef();
        var fnRef2 = outerFn();
        fnRef2();
        fnRef2();
复制代码

 

现在每次调用内部函数都会持续地递增这个全局变量的值:

复制代码
Outer function
Inner function    globalVar = 1
Inner function    globalVar = 2
Outer function
Inner function    globalVar = 3
Inner function    globalVar = 4
复制代码

 

 

但是如果这个变量是父函数的局部变量又会怎样呢?因为内部函数会引用到父函数的作用域(有兴趣可以了解一下作用域链和活动对象的知识),内部函数也可以引用到这些变量

复制代码
function outerFn() {
            var outerVar = 0;
            document.write("Outer function<br/>");
            function innerFn() {
                outerVar++;
                document.write("Inner function\t");
                document.write("outerVar = " + outerVar + "<br/>");
            }
            return innerFn;
        }
        var fnRef = outerFn();
        fnRef();
        fnRef();
        var fnRef2 = outerFn();
        fnRef2();
        fnRef2();
复制代码

 

这一次结果非常有意思,也许或出乎我们的意料

复制代码
Outer function
Inner function    outerVar = 1
Inner function    outerVar = 2
Outer function
Inner function    outerVar = 1
Inner function    outerVar = 2
复制代码

我们看到的是前面两种情况合成的效果,通过每个引用调用innerFn都会独立的递增outerVar。也就是说第二次调用outerFn没有继续沿用outerVar的值,而是在第二次函数调用的作用域创建并绑定了一个一个新的outerVar实例,两个计数器完全无关。

当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的一个闭包。这种情况下我们称既不是内部函数局部变量,也不是其参数的变量为自由变量,称外部函数的调用环境为封闭闭包的环境。从本质上讲,如果内部函数引用了位于外部函数中的变量,相当于授权该变量能够被延迟使用。因此,当外部函数调用完成后,这些变量的内存不会被释放(最后的值会保存),闭包仍然需要使用它们。

 

3.闭包之间的交互

当存在多个内部函数时,很可能出现意料之外的闭包。我们定义一个递增函数,这个函数的增量为2

复制代码
function outerFn() {
            var outerVar = 0;
            document.write("Outer function<br/>");
            function innerFn1() {
                outerVar++;
                document.write("Inner function 1\t");
                document.write("outerVar = " + outerVar + "<br/>");
            }

            function innerFn2() {
                outerVar += 2;
                document.write("Inner function 2\t");
                document.write("outerVar = " + outerVar + "<br/>");
            }
            return { "fn1": innerFn1, "fn2": innerFn2 };
        }
        var fnRef = outerFn();
        fnRef.fn1();
        fnRef.fn2();
        fnRef.fn1();
        var fnRef2 = outerFn();
        fnRef2.fn1();
        fnRef2.fn2();
        fnRef2.fn1();
复制代码

我们映射返回两个内部函数的引用,可以通过返回的引用调用任一个内部函数,结果:

复制代码
Outer function
Inner function 1    outerVar = 1
Inner function 2    outerVar = 3
Inner function 1    outerVar = 4
Outer function
Inner function 1    outerVar = 1
Inner function 2    outerVar = 3
Inner function 1    outerVar = 4
复制代码

 

innerFn1和innerFn2引用了同一个局部变量,因此他们共享一个封闭环境。当innerFn1为outerVar递增一时,久违innerFn2设置了outerVar的新的起点值,反之亦然。我们也看到对outerFn的后续调用还会创建这些闭包的新实例,同时也会创建新的封闭环境,本质上是创建了一个新对象,自由变量就是这个对象的实例变量,而闭包就是这个对象的实例方法,而且这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了了面向对象数据的专有性。

 

3.解惑

现在我们可以回头看看开头写的例子就很容易明白为什么第一种写法每次都会alert 4了。

for (var i = 0; i < spans.length; i++) {
           spans[i].onclick = function() {
               alert(i);
           }
       }

 

上面代码在页面加载后就会执行,当i的值为4的时候,判断条件不成立,for循环执行完毕,但是因为每个span的onclick方法这时候为内部函数,所以i被闭包引用,内存不能被销毁,i的值会一直保持4,直到程序改变它或者所有的onclick函数销毁(主动把函数赋为null或者页面卸载)时才会被回收。这样每次我们点击span的时候,onclick函数会查找i的值(作用域链是引用方式),一查等于4,然后就alert给我们了。而第二种方式是使用了一个立即执行的函数又创建了一层闭包,函数声明放在括号内就变成了表达式,后面再加上括号括号就是调用了,这时候把i当参数传入,函数立即执行,num保存每次i的值。

这一通下来想必大家也和我一样,对闭包有所了解了吧,当然完全了解的话需要把函数的执行环境和作用域链搞清楚 ^_^

posted @ 2012-09-29 16:55 Samaritans 阅读(28536) 评论(28) 编辑 收藏

评论列表
  
#1楼 2012-09-29 17:21 以便以谢  
这个确实是个难点,值得研究研究。。
  
#2楼 2012-09-29 17:56 卤鸽  
清楚了作用域,看闭包的点相对比较容易点。
  
#3楼 2012-09-29 19:41 一路转圈的雪人  
建议楼主去看看周爱民写的《JAVASCRIPT语言精髓与编程实践》对于函数和闭包的描述,能更加深刻的理解。

当然,仅理解到这篇文章的这个层次一般也够用了。
  
#4楼 2012-09-29 23:00 清風揚諰  
作用域链和活动对象的知识

这个才是闭包的关键吧
  
#5楼 2012-09-30 17:28 happydaily  
第一段红字似乎有笔误?知道=直到?
  
#6楼 2013-03-13 22:39 拉拉叟  
今天刚想了解闭包的知识 看了好几个帖子都一头雾水 以为自己太笨了
刚才才花10分钟一路看下来 一下就看懂了 还是验证那句话啊 没有笨学生 只有不会教的老师 谢谢
  
#7楼[楼主] 2013-08-07 13:34 Samaritans  
@ happydaily
确实是,已纠正
  
#8楼[楼主] 2013-08-07 13:36 Samaritans  
@ 清風揚諰
说的没错,本文只是想已一种简单方式让人理解为什么会有这种现象,至于深入理解原理,以后会总结
  
#9楼 2013-10-29 19:31 Alex_Monkey  
顶,写得真不错!
  
#10楼[楼主] 2013-10-30 21:34 Samaritans  
@ Alex_Monkey
引用顶,写得真不错!

谢谢支持
  
#11楼 2013-12-27 22:56 ncsb  
看不明白,睡觉,明天继续看
  
#12楼[楼主] 2013-12-28 08:57 Samaritans  
@ ncsb
引用看不明白,睡觉,明天继续看

其实自己动动手一下就明白了
  
#13楼 2014-09-24 13:22 红豆依旧在  
看过最好的关于闭包的解释,赞
  
#14楼 2014-12-01 20:42 KMSFan  
闭包归根结底个人认为只是一个作用域链的不同吧,其实理解了作用域链个人感觉不是太难。
  
#15楼 2014-12-13 14:15 快乐人生,积极进取  
调用outerFn时会修改全局变量globalVar,这时候它的引用变为innerFn,此后调用globalVar和调用innerFn一样。这时在outerFn外部直接调用innerFn仍然会导致错误,这是因为内部函数虽然通过把引用保存在全局变量中实现了逃脱,但这个函数的名字依然只存在于outerFn的作用域中。

这里好像说得不是太对哦!
在外部调用globalVar()也是对的,不会报错哦!
  
#16楼 2015-01-09 13:48 一枚蜗牛  
写得挺好,说明解释的部分挺好理解
  
#17楼 2015-03-24 20:27 暗夜娜姐  
写的非常清楚,感谢楼主
  
#18楼 2015-03-27 23:02 苏本东  
说下我的拙见:理解了面向对象,这些就很容易理解了。
还有最好在Chrome浏览器下一步一步的调试楼主提供的代码,这样印象更加深刻。至于怎么调试怎么打断点百度搜索吧。
  
#19楼 2015-05-15 20:57 D&L  
请问下开头的那个问题,为什么第一次点击的时候就是4,而不是先正常的alert出0 1 2 3 然后无论点击哪一个都是4, 我是新手,不太懂。
  
#20楼 2015-05-25 11:38 41Ex  
每个函数都仔细看了一遍,非常棒的博文,感谢!
  
#21楼 2015-07-14 10:05 wangchao  
楼主讲的好详细,例子也很精辟,看了好几遍,值得深思。
  
#22楼 2015-07-31 14:33 lele88lala  
我试了一下代码
var spans2 = $("#divTest2 span");
$(document).ready(function() {
for (var i = 0; i < spans2.length; i++) {
(function(num) {
spans2[i].onclick = function() {
alert(num);
}
})(i);
}
});

好像有点问题。
  
#23楼 2015-09-01 21:17 冇乜吔  
真是一语道醒梦中人呐! 终于明白什么意思了, 感谢楼主, 看了一次就明白原理了. 估计JQuery的$("xx")也是这个原理吧!
  
#24楼 2015-10-29 11:32 晨间新闻  
挺好的呢,每一个例子都自己运行了一遍,以前还不知道可以函数返回多个键值对呢
  
#25楼 2015-12-16 10:31 ||゛古城白衣少年殇  
一直在困惑,多谢
  
#26楼 2016-03-02 17:52 路警石  
请楼主或各位大神解惑:外部函数执行到for循环时的具体流程是什么?span[i].onclick=function(){} 不是给每个span绑定了点击事件了吗?页面加载完成(此时还没点击span),是否for循环的i已经自增到4时跳出了循环,如果是这样span的点击事件在循环了面为什么还能响应呢?或者我的理解是这样的:
页面加载时每个span都被绑定了onclick事件,分别是spans[0]、spans[1]、spans[2]、spans[3],加载完页面for循环也结束跳出,此时i=4,点击的实际上是spans[0]、spans[1]、spans[2]、spans[3]对应的绑定事件,传入内部函数的却是4。
请赐教佐证,谢谢各位!
  
#27楼 2016-09-22 11:17 电路分析  
先坦白,自己一直对闭包的实战应用不够清楚。。不过,楼主讲的还是不够接地气,特别是最后对例子的解惑,就没讲明白。divTest下的span点击会一直弹出4,是因为js里没有块级作用域,而i是全局作用域下的变量,onclick事件里,并没有对i进行保存,执行完for循环,i的值肯定是4了。divText2下,是因为将i作为实参传给了行参num,而行参就相当于在函数里定义了变量,即:var num,for循环的时候,每次在自执行函数里,都var num 一次,然后通过num将i的值传进来保存起来了。就是这样。。
  
#28楼 2016-10-21 19:49 以死向生  
我刚开始虽然不懂,但楼主第一个测试的时候,应该是for执行完后,点击时,只会出现4,第二个能获取,也只是实参i传给形参num的值,i值等于1时,点击第一个span并没用,所以只是弹出的只是实参i的值,但当i=0时,还是可以和获取标签的span,可以实现其功能。。。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多