深入理解JavaScript 闭包究竟是什么


Posted in Javascript onApril 12, 2013

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的后续调用还会创建这些闭包的新实例,同时也会创建新的封闭环境,本质上是创建了一个新对象,自由变量就是这个对象的实例变量,而闭包就是这个对象的实例方法,而且这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了了面向对象数据的专有性。

4.解惑

现在我们可以回头看看开头写的例子就很容易明白为什么第一种写法每次都会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的值。

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

Javascript 相关文章推荐
使用JQuery进行跨域请求
Jan 25 Javascript
基于jquery中children()与find()的区别介绍
Apr 26 Javascript
jQuery获得内容和属性示例代码
Jan 16 Javascript
JavaScript程序员应该知道的45个实用技巧
Mar 04 Javascript
js 弹出新页面避免被浏览器、ad拦截的一种新方法
Apr 30 Javascript
Jquery技巧(必须掌握)
Mar 16 Javascript
jQuery easyUI datagrid 增加求和统计行的实现代码
Jun 01 Javascript
jQuery用noConflict代替$的实现方法
Apr 12 jQuery
从零开始学习Node.js系列教程二:文本提交与显示方法
Apr 13 Javascript
javascript回调函数详解
Feb 06 Javascript
JS判断数组里是否有重复元素的方法小结
May 21 Javascript
vue实现二级导航栏效果
Oct 19 Javascript
关于JavaScript中string 的replace
Apr 12 #Javascript
关于JavaScript与HTML的交互事件
Apr 12 #Javascript
Js中setTimeout()和setInterval() 何时被调用执行的用法
Apr 12 #Javascript
js实现单一html页面两套css切换代码
Apr 11 #Javascript
获取内联和链接中的样式(js代码)
Apr 11 #Javascript
JavaScript在XHTML中的用法详解
Apr 11 #Javascript
JavaScript中的noscript元素属性位置及作用介绍
Apr 11 #Javascript
You might like
如何使用PHP对网站验证码进行破解
2015/09/17 PHP
PHP+AjaxForm异步带进度条上传文件实例代码
2017/08/14 PHP
js实现ASP分页函数 HTML分页函数
2006/09/22 Javascript
[原创]js获取数组任意个不重复的随机数组元素
2010/03/15 Javascript
JQUERY获取form表单值的代码
2010/07/17 Javascript
artDialog 4.1.5 Dreamweaver代码提示/补全插件 附下载
2012/07/31 Javascript
javascript对select标签的控制(option选项/select)
2013/01/31 Javascript
JS实现程序暂停与继续功能代码解读
2013/10/10 Javascript
探讨js字符串数组拼接的性能问题
2014/10/11 Javascript
浅谈JavaScript中运算符的优先级
2015/07/07 Javascript
javascript中arguments,callee,caller详解
2016/03/16 Javascript
jQuery源码分析之init的详细介绍
2017/02/13 Javascript
bootstrap fileinput 插件使用项目总结(经验)
2017/02/22 Javascript
Nodejs Express 通过log4js写日志到Logstash(ELK)
2018/08/30 NodeJs
JavaScript基于用户照片姓名生成海报
2020/05/29 Javascript
[31:33]2014 DOTA2国际邀请赛中国区预选赛 TongFu VS DT 第一场
2014/05/23 DOTA
Python中functools模块函数解析
2017/03/12 Python
Python实现对字符串的加密解密方法示例
2017/04/29 Python
Python并发:多线程与多进程的详解
2019/01/24 Python
树莓派+摄像头实现对移动物体的检测
2019/06/22 Python
python 字符串常用函数详解
2019/09/11 Python
sklearn和keras的数据切分与交叉验证的实例详解
2020/06/19 Python
Pytest如何使用skip跳过执行测试
2020/08/13 Python
python opencv pytesseract 验证码识别的实现
2020/08/28 Python
深入理解HTML5定时器requestAnimationFrame的使用
2018/12/12 HTML / CSS
Onzie官网:美国时尚瑜伽品牌
2019/08/21 全球购物
办公室主任岗位职责
2013/11/08 职场文书
保安的辞职报告怎么写
2014/01/20 职场文书
市场营销大学生职业规划书
2014/02/25 职场文书
学生党员的自我评价范文
2014/03/01 职场文书
个人银行贷款担保书
2014/04/01 职场文书
购房协议书范本
2014/10/02 职场文书
校园新闻广播稿5篇
2014/10/10 职场文书
运动会100米广播稿
2015/08/19 职场文书
浅谈:电影《孔子》观后感(范文)
2019/10/14 职场文书
Vue操作Storage本地化存储
2022/04/29 Vue.js