浅谈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 相关文章推荐
javascript打开word文档的方法
Apr 16 Javascript
JavaScript控制table某列不显示的方法
Mar 16 Javascript
jQuery延迟加载图片插件Lazy Load使用指南
Mar 25 Javascript
Bootstrap表格制作代码
Mar 17 Javascript
AngularJS 支付倒计时功能实现思路
Jun 05 Javascript
JS中的Replace()传入函数时的用法详解
Sep 11 Javascript
Scala解析Json字符串的实例详解
Oct 11 Javascript
通过函数作用域和块级作用域看javascript的作用域链
Aug 05 Javascript
angularjs请求数据的方法示例
Aug 06 Javascript
微信小程序 flexbox layout快速实现基本布局的解决方案
Mar 24 Javascript
vue中全局路由守卫中替代this操作(this.$store/this.$vux)
Jul 24 Javascript
详解JavaScript的计时器和按钮效果设置
Feb 18 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
php5.2.0内存管理改进
2007/01/22 PHP
ThinkPHP自动转义存储富文本编辑器内容导致读取出错的解决方法
2014/08/08 PHP
CI框架装载器Loader.php源码分析
2014/11/04 PHP
javascript检查表单数据是否改变的方法
2013/07/30 Javascript
javascript跨浏览器的属性判断方法
2014/03/16 Javascript
对js关键字命名的疑问介绍
2014/04/25 Javascript
Node.js 的异步 IO 性能探讨
2014/10/08 Javascript
jquery中EasyUI使用技巧小结
2015/02/10 Javascript
jQuery自动添加表单项的方法
2015/07/13 Javascript
javascript同步服务器时间和同步倒计时小技巧
2015/09/24 Javascript
原生JavaScript实现动态省市县三级联动下拉框菜单实例代码
2016/02/03 Javascript
基于Bootstrap实现的下拉菜单手机端不能选择菜单项的原因附解决办法
2016/07/22 Javascript
使用Angular.js开发的注意事项
2016/10/19 Javascript
javascript简单实现深浅拷贝过程详解
2019/10/08 Javascript
[01:07:19]DOTA2-DPC中国联赛 正赛 CDEC vs XG BO3 第一场 1月19日
2021/03/11 DOTA
Python set集合类型操作总结
2014/11/07 Python
Python 3中的yield from语法详解
2017/01/18 Python
python 类对象和实例对象动态添加方法(分享)
2017/12/31 Python
django ModelForm修改显示缩略图 imagefield类型的实例
2019/07/28 Python
python构建指数平滑预测模型示例
2019/11/21 Python
HTML5 新标签全部总汇(推荐)
2016/06/13 HTML / CSS
美国知名日用品连锁超市:Dollar General(多来店)
2017/01/14 全球购物
拉斯维加斯城市观光通行证:Las Vegas Pass
2019/05/21 全球购物
Java中compareTo和compare的区别
2016/04/12 面试题
工程项目建议书范文
2014/03/12 职场文书
省级优秀毕业生主要事迹
2014/05/29 职场文书
孩子教育的心得体会
2014/09/01 职场文书
2014年测量员工作总结
2014/12/12 职场文书
2015元旦晚会主持人开场白+结束语
2014/12/14 职场文书
《棉鞋里的阳光》教学反思
2016/02/20 职场文书
《检阅》教学反思
2016/02/22 职场文书
2016年119消防宣传日活动总结
2016/04/05 职场文书
秀!学妹看见都惊呆的Python小招数!【详细语言特性使用技巧】
2021/04/27 Python
Django显示可视化图表的实践
2021/05/10 Python
全网非常详细的pytest配置文件
2022/07/15 Python
Go语言编译原理之源码调试
2022/08/05 Golang