浅谈Javascript嵌套函数及闭包


Posted in Javascript onNovember 09, 2010

【嵌套函数】

JavaScript允许嵌入的函数,允许函数用作数据,并且在函数词法作用域下面,可以产生与传统面向对象语言不同的惊人地方。

首先,JavaScript的函数是通过词法来划分作用域的,而不是动态的划分作用域的,于是,函数的是在定义它们的作用域中运行,而不是在执行它们的作用域中运行,所以,当嵌套函数和它的外围函数定义在同一个词法作用域中的时候,是很容易理解的。比如下面很平淡无奇的代码:

var x = 'global'; 
function f () { 
var x = 'local'; 
function g() { 
alert(x); 
} 
g(); 
} 
f(); // 'local'

当f()调用的时候,作用域链可以理解为由两部分组成,包含f这一调用的调用对象,然后后面是全局对象。此时查找x的值,会先从f的调用对象中查找,如果没有,再查找后面全局对象中x。同理,g因为是f的一个嵌套函数,那么,g调用的时候,作用域链应该就是由三部分组成了,g的调用对象,f的调用对象,和全局对象。函数g是要输出x的值,所以会先在g的调用对象中查找x的值,g中没有定义,接下来查找外围f调用对象中x的定义,于是找到了x='local',那么就会输出x,而不会继续往下查找全局对象了。 如果f中也没定义x的值,那么就会继续查找作用域链后面的全局对象,结果就是global了。如果全局对象中也没定义,那么自然就是undefined。

好了,我们对作用域链有了个初步的理解,同时我们知道,闭包有两个比较常用的用途,一个是可以利用它访问到局部变量,另一个是可以把它外围作用域中的变量值存储在内存中而不在函数调用完毕后就销毁。

下面接着看一个平淡无奇的例子,或许可以帮助理解为什么闭包可以把外部变量值保存在内存中了。

function makeFunc (x) { 
return function () {return x++} 
} 
var a = [makeFunc(0), makeFunc(1), makeFunc(2)]; 
alert(a[0]()); 
alert(a[1]()); 
alert(a[2]());

执行结果为0,1,2 ;也没有什么特别的地方,这也是严格的词法作用域的正常表现。每次makeFunc调用完毕后,它的调用对象会从作用域链中移除,再没有任何对它的引用,最终通过垃圾收集而完结。说的详细一点,我们可以这样理解。

makeFunc每次调用的时候,会为他创建一个调用对象放置到作用域链中。针对makeFunc这个函数而言,这个调用对象包含一个属性x(也就是函数的参数,因为函数参数可以看做调用对象的一个属性),makeFunc会返回一个匿名嵌套函数的引用,接下来这个匿名嵌套函数执行,又会创建一个调用对象,放置到作用域链中,匿名函数返回x的值,(注意:匿名函数的调用对象中是没有x的定义的,于是它会引用到它外围的函数makeFunc的调用对象,访问到x)然后x加1,至此,匿名函数执行完毕,它调用对象从作用域链中移除, 然后makeFunc也执行完毕,makeFunc调用对象也被移除。由于它的调用对象中包含x,所以x也随着它的销毁而销毁。不会保存下来。

以上就是函数的详细的执行过程,请仔细理解后看看下面改动的代码:

var x = 0; 
function makeFunc () { 
return function () {return x++} 
} 
var a = [makeFunc(), makeFunc(), makeFunc()]; 
alert(a[0]()); 
alert(a[1]()); 
alert(a[2]());

现在x是一个全局变量了,执行结果为0,1,2;但是这个结果就与上面的有些不同了。下面我们还是从作用域链的方向来理解这个结果产生的原因。

同样,makeFunc每次调用的时候会创建一个调用对象到作用域链中,由于它返回内部嵌套函数的引用,所以内部嵌套函数开始执行,又创建一个嵌套函数的调用对象到作用域链。然后返回x的值,注意,这里就不同了,嵌套函数的调用对象中没有x,它外围的makeFunc的调用对象中也没有x,只能接着往下查找到全局对象中,在全局对象中找到了x的定义,于是正常执行,返回x的值,x加1,然后嵌套函数完毕,调用对象移除,接着makeFunc完毕,调用对象也移除,可是因为他们的调用对象中都没有x,他们的调用对象销毁根本不会影响到x。于是,全局变量x值的改变就这样被保存下来了。

注意,上面说的访问外围的调用对象只是为了帮助理解而不严格的说法,JavaScript不会以任何方式直接访问调用对象,但是,它定义的属性作为调用对象中作用域链的一部分,还是“活的”。另外,如果一个外围函数包含了两个或多个嵌套函数都对全局对象有引用,那么这些嵌套函数都共享同一个全局调用对象,并且其中一个对全局对象的改变对其他的都是可见的。

好了,在JavaScript里,函数是将要执行的代码以及执行这些代码的作用域构成的一个综合体,广义的说,我们就可以把这种代码和作用域的综合体叫做闭包。

【闭包】

我们偶尔需要写一个需要通过调用来记住一个变量值的函数。于是,如果我们了解了作用域,就会知道,局部变量是很难做到的,因为函数的调用对象不能在调用后一直维持。全局变量可以做到,就如上面的例子一样,但是这样很容易造成全局变量污染。既然调用对象不能维持,那么我们不把值保存在调用对象中不就行了?!所以,下面是实现的一种方法:用函数对象自身的属性来保存。

uniqueID = function () { 
if (!arguments.callee.id) arguments.callee.id = 0; 
return arguments.callee.id ++; 
} 
alert(uniqueID()); //0 
alert(uniqueID()); //1

如上,因为函数本身就是一个对象,所以,我们用它自身的一个属性来保存是可行的,但是这样做有一个问题,就是任何人在任何时候都可以通过unqueID.id强制访问到我们原本保存到的值并作出修改。这是我们不愿看到的。

所以,通常,我们使用闭包来实现这件事。如下:

_uniqueID = (function(){ 
var id = 0; 
return function () {return id ++} 
})(); 
alert(_uniqueID()); //0 
alert(_uniqueID()); //1

同样,我们也用作用链域来解释下结果。注意到_uniqueID本身就是一个匿名函数,它内部又有个匿名嵌套函数,我们直接调用的是_uniqueID(),也就是说,我们直接调用的其实是_uniqueID内部的嵌套函数,而它本身的调用对象没有定义id,于是引用外围的调用对象中的id,并返回,id加1,执行完毕,内层嵌套函数调用对象移出作用域链。而外围的id并没有被销毁,于是就这样保存了下来。

有人可能会疑惑,不是说调用对象在函数执行完毕后就移除了作用域链吗,外围匿名函数(function(){})();也是调用完毕了的,应该调用对象也没了才对。

是的,调用对象是在当前函数执行完毕后就结束引用,但是这里不要误解了上面_uniqueID()的调用,他并不是直接调用的外围函数,而是调用的嵌套函数,嵌套函数的作用域链是包含外围函数的作用域链的。所以在它的调用对象移除作用域链的时候是能够访问到这条作用域链上其他对象的属性并改变的。

闭包本身就是个难以理解但是又非常有用的东西,希望能对有需要的人一些帮助吧。此外,资历所限,本人理解也可能有误,如发现,敬请指正。

Javascript 相关文章推荐
SeaJS入门教程系列之SeaJS介绍(一)
Mar 03 Javascript
调整小数的格式保留小数点后两位
May 14 Javascript
JavaScript 作用域链解析
Nov 13 Javascript
JavaScript页面模板库handlebars的简单用法
Mar 02 Javascript
解决Jquery向页面append新元素之后事件的绑定问题
Mar 16 Javascript
JavaScript代码实现图片循环滚动效果
Mar 19 Javascript
layui之select的option叠加问题的解决方法
Mar 08 Javascript
vuex操作state对象的实例代码
Apr 25 Javascript
node全局变量__dirname与__filename的区别
Jan 14 Javascript
微信小程序组件传值图示过程详解
Jul 31 Javascript
js实现鼠标点击页面弹出自定义文字效果
Dec 24 Javascript
vue 将多个过滤器封装到一个文件中的代码详解
Sep 05 Javascript
JavaScript高级程序设计 扩展--关于动态原型
Nov 09 #Javascript
关于JavaScript定义类和对象的几种方式
Nov 09 #Javascript
JS图片浏览组件PhotoLook的公开属性方法介绍和进阶实例代码
Nov 09 #Javascript
一个javascript图片阅览组件
Nov 09 #Javascript
js中格式化日期时间型数据函数代码
Nov 08 #Javascript
window.location.hash 使用说明
Nov 08 #Javascript
JavaScript游戏之是男人就下100层代码打包
Nov 08 #Javascript
You might like
php smarty函数扩展
2010/03/15 PHP
PHP登陆后跳转到登陆前页面实现思路及代码
2014/01/17 PHP
PHP对象相互引用的内存溢出实例分析
2014/08/28 PHP
javascript some()函数用法详解
2014/11/13 PHP
PHP文件锁函数flock()详细介绍
2014/11/18 PHP
php使用Cookie控制访问授权的方法
2015/01/21 PHP
PHP中调用C/C++制作的动态链接库的教程
2016/03/10 PHP
laravel 框架结合关联查询 when()用法分析
2019/11/22 PHP
php实现简易计算器
2020/08/28 PHP
innertext , insertadjacentelement , insertadjacenthtml , insertadjacenttext 等区别
2007/06/29 Javascript
页面中iframe相互传值传参
2009/12/13 Javascript
深入理解JSON数据源格式
2014/01/10 Javascript
js+css实现超简洁的二级下拉菜单效果代码
2015/09/07 Javascript
Adapter适配器模式在JavaScript设计模式编程中的运用分析
2016/05/18 Javascript
RequireJS多页面应用实例分析
2016/06/29 Javascript
基于jQuery实现歌词滚动版音乐播放器的代码
2016/09/17 Javascript
jQuery实现页面倒计时并刷新效果
2017/03/13 Javascript
webpack配置的最佳实践分享
2017/04/21 Javascript
基于vue实现swipe分页组件实例
2017/05/25 Javascript
angular中不同的组件间传值与通信的方法
2017/11/04 Javascript
vue拦截器实现统一token,并兼容IE9验证功能
2018/04/26 Javascript
vue+element-ui动态生成多级表头的方法
2018/08/28 Javascript
js 将线性数据转为树形的示例代码
2019/05/28 Javascript
微信小程序获取用户绑定手机号方法示例
2019/07/21 Javascript
python实现根据窗口标题调用窗口的方法
2015/03/13 Python
详解Python 数据库的Connection、Cursor两大对象
2018/06/25 Python
Python常见MongoDB数据库操作实例总结
2018/07/24 Python
对Python 窗体(tkinter)树状数据(Treeview)详解
2018/10/11 Python
PyCharm中Matplotlib绘图不能显示UI效果的问题解决
2020/03/12 Python
解决python cv2.imread 读取中文路径的图片返回为None的问题
2020/06/02 Python
英国复古服装和球衣购买网站:3Retro Football
2018/07/09 全球购物
《鸟的天堂》教学反思
2014/02/27 职场文书
2014年文明创建工作总结
2014/11/25 职场文书
全国爱眼日活动总结
2015/02/27 职场文书
生日宴会家属答谢词
2015/09/29 职场文书
python 三边测量定位的实现代码
2021/04/22 Python