深入理解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 相关文章推荐
优化innerHTML操作(提高代码执行效率)
Aug 20 Javascript
jquery多行滚动/向左或向上滚动/响应鼠标实现思路及代码
Jan 23 Javascript
最丑的时钟效果!js canvas时钟制作方法
Aug 15 Javascript
JavaScript使用Range调色及透明度实例
Sep 25 Javascript
JS实现表单多文件上传样式美化支持选中文件后删除相关项
Sep 30 Javascript
微信小程序 vidao实现视频播放和弹幕的功能
Nov 02 Javascript
基于vue的短信验证码倒计时demo
Sep 13 Javascript
vue学习教程之带你一步步详细解析vue-cli
Dec 26 Javascript
node.js使用express框架进行文件上传详解
Mar 03 Javascript
BootStrap表单验证中的非Submit类型按钮点击时触发验证的坑
Sep 05 Javascript
koa-passport实现本地验证的方法示例
Feb 20 Javascript
深度解读vue-resize的具体用法
Jul 08 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 文件缓存函数
2011/10/08 PHP
ThinkPHP之import方法实例详解
2014/06/20 PHP
php实现的发送带附件邮件类实例
2014/09/22 PHP
PHP中把有符号整型转换为无符号整型方法
2015/05/27 PHP
phpmailer绑定邮箱的实现方法
2016/12/01 PHP
元素的内联事件处理函数的特殊作用域在各浏览器中存在差异
2011/01/12 Javascript
js 为label标签和div标签赋值的方法
2013/08/08 Javascript
jquery设置text的值示例(设置文本框 DIV 表单值)
2014/01/06 Javascript
jquery选择符快速提取web表单数据示例
2014/03/27 Javascript
如何在node的express中使用socket.io
2014/12/15 Javascript
原生JS实现仿淘宝网左侧商品分类菜单效果代码
2015/09/10 Javascript
js+canvas绘制五角星的方法
2016/01/28 Javascript
JS实现刷新父页面不弹出提示框的方法
2016/06/22 Javascript
随机生成10个不重复的0-100的数字(实例讲解)
2017/08/16 Javascript
JavaScript ES6常用基础知识总结
2019/02/09 Javascript
详解webpack 最简打包结果分析
2019/02/20 Javascript
layui对工具条进行选择性的显示方法
2019/09/19 Javascript
vue-resource:jsonp请求百度搜索的接口示例
2019/11/09 Javascript
[20:30]职业巡回赛回顾
2018/08/09 DOTA
Python正则抓取新闻标题和链接的方法示例
2017/04/24 Python
Python切片工具pillow用法示例
2018/03/30 Python
python将控制台输出保存至文件的方法
2019/01/07 Python
Python字符串对象实现原理详解
2019/07/01 Python
numpy按列连接两个维数不同的数组方式
2019/12/06 Python
matplotlib交互式数据光标实现(mplcursors)
2021/01/13 Python
python pillow库的基础使用教程
2021/01/13 Python
GAZMAN官网:澳大利亚领先的男装品牌
2019/12/19 全球购物
信息系统专业个人求职信范文
2013/12/07 职场文书
外企财务年会演讲稿
2014/01/03 职场文书
金融管理专业毕业生求职信
2014/03/12 职场文书
客户经理竞聘演讲稿
2014/05/15 职场文书
运动会的口号
2014/06/09 职场文书
镇党委书记群众路线整改措施思想汇报
2014/10/13 职场文书
2014年党务工作总结
2014/11/25 职场文书
道德模范事迹材料
2014/12/20 职场文书
2015年招商引资工作总结
2015/04/25 职场文书