浅谈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 相关文章推荐
支持ie与FireFox的剪切板操作代码
Sep 28 Javascript
Jquery选择器 $实现原理
Dec 02 Javascript
JQuery从头学起第二讲
Jul 04 Javascript
JS中的二叉树遍历详解
Mar 18 Javascript
vue.js表格组件开发的实例详解
Oct 12 Javascript
js中创建对象的几种方式
Feb 05 Javascript
Angularjs为ng-click事件传递参数
Jun 15 Javascript
利用JS hash制作单页Web应用的方法详解
Oct 10 Javascript
JavaScript学习笔记之DOM基础操作实例小结
Jan 09 Javascript
微信小程序自定义纯净模态框(弹出框)的实例代码
Mar 09 Javascript
jQuery带控制按钮轮播图插件
Jul 31 jQuery
详解基于element的区间选择组件校验(交易金额)
Jan 07 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小偷相关截取函数备忘
2010/11/28 PHP
php $_SESSION会员登录实例分享
2021/01/19 PHP
PHP基于GD库实现的生成图片缩略图函数示例
2017/07/05 PHP
php判断文件上传图片格式的实例详解
2017/09/30 PHP
jQuery function的正确书写方法
2013/08/02 Javascript
手机平板等移动端适配跳转URL的js代码
2014/01/25 Javascript
JavaScript程序员应该知道的45个实用技巧
2014/03/04 Javascript
jQuery判断元素上是否绑定了指定事件的方法
2015/03/17 Javascript
js获取数组的最后一个元素
2015/04/14 Javascript
JavaScript中的lastIndexOf()方法使用详解
2015/06/06 Javascript
jQuery无刷新上传之uploadify3.1简单使用
2016/06/18 Javascript
JavaScript实现微信红包算法及问题解决方法
2018/04/26 Javascript
webpack file-loader和url-loader的区别
2019/01/15 Javascript
在Vue中用canvas实现二维码和图片合成海报的方法
2019/06/10 Javascript
JS前端基于canvas给图片添加水印
2020/11/11 Javascript
[01:57]2018DOTA2亚洲邀请赛赛前采访-iG
2018/04/03 DOTA
Python SVM(支持向量机)实现方法完整示例
2018/06/19 Python
Python基于百度云文字识别API
2018/12/13 Python
对python借助百度云API对评论进行观点抽取的方法详解
2019/02/21 Python
详解python函数的闭包问题(内部函数与外部函数详述)
2019/05/17 Python
详解用Python实现自动化监控远程服务器
2019/05/18 Python
在Python中COM口的调用方法
2019/07/03 Python
Python3内置模块之base64编解码方法详解
2019/07/13 Python
Python assert关键字原理及实例解析
2019/12/13 Python
OpenCV+python实现实时目标检测功能
2020/06/24 Python
深入分析python 排序
2020/08/24 Python
浅析NumPy 切片和索引
2020/09/02 Python
python -v 报错问题的解决方法
2020/09/15 Python
HTML5的结构和语义(2):结构
2008/10/17 HTML / CSS
实习生个人的自我评价
2013/12/08 职场文书
感谢信格式范文
2015/01/22 职场文书
起诉意见书范文
2015/05/19 职场文书
如何让2019年上半年的工作总结更出色!
2019/07/01 职场文书
MySQL性能压力基准测试工具sysbench的使用简介
2021/04/21 MySQL
python代码实现备忘录案例讲解
2021/07/26 Python
Python中非常使用的6种基本变量的操作与技巧
2022/03/22 Python