浅谈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与flash交互通信基础教程
Aug 07 Javascript
一个简单的js渐显(fadeIn)渐隐(fadeOut)类
Jun 19 Javascript
一个基于jquery的文本框记数器
Sep 19 Javascript
node.js中的buffer.length方法使用说明
Dec 14 Javascript
JS数组排序技巧汇总(冒泡、sort、快速、希尔等排序)
Nov 24 Javascript
Angular2表单自定义验证器的实现
Oct 19 Javascript
bootstrap模态框垂直居中效果
Dec 03 Javascript
JS常见创建类的方法小结【工厂方式,构造器方式,原型方式,联合方式等】
Apr 01 Javascript
vuejs前后端数据交互之从后端请求数据的实例
Aug 11 Javascript
layui table设置前台过滤转义等方法
Aug 17 Javascript
js实现经典贪吃蛇小游戏
Mar 19 Javascript
VUE中的v-if与v-show区别介绍
Mar 13 Vue.js
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读取RSS feed的代码
2008/08/01 PHP
php获取某个目录大小的代码
2008/09/10 PHP
php中拷贝构造函数、赋值运算符重载
2012/07/25 PHP
PHP实现服务器状态监控的方法
2014/12/09 PHP
PHP的Yii框架中移除组件所绑定的行为的方法
2016/03/18 PHP
PHP预定义超全局数组变量小结
2018/08/20 PHP
thinkPHP和onethink微信支付插件分享
2019/08/11 PHP
JavaScript Chart 插件整理
2010/06/18 Javascript
jQuery效果 slideToggle() 方法(在隐藏和显示之间切换)
2011/06/28 Javascript
jQuery侧边栏随窗口滚动实现方法
2013/03/04 Javascript
js 获取和设置css3 属性值的实现方法
2013/05/06 Javascript
js为数字添加逗号并格式化数字的代码
2013/08/23 Javascript
jquery attr方法获取input的checked属性问题
2014/05/26 Javascript
Javascript随机标签云代码实例
2016/06/21 Javascript
原生js实现对Ajax的封装(仿jquery)
2017/01/22 Javascript
Javascript中数组去重与拍平的方法示例
2017/02/03 Javascript
node Buffer缓存区常见操作示例
2019/05/04 Javascript
基于小程序请求接口wx.request封装的类axios请求
2020/07/02 Javascript
vue3.0 加载json的方法(非ajax)
2020/10/26 Javascript
[02:45]DOTA2英雄基础教程 伐木机
2013/12/23 DOTA
Python二叉树的遍历操作示例【前序遍历,中序遍历,后序遍历,层序遍历】
2018/12/24 Python
Python使用OpenPyXL处理Excel表格
2020/07/02 Python
python自动生成证件号的方法示例
2021/01/14 Python
CSS3中:nth-child和:nth-of-type的区别深入理解
2014/03/10 HTML / CSS
Sofft鞋官网:世界知名鞋类品牌
2017/03/28 全球购物
美国知名眼镜网站:Target Optical
2020/04/04 全球购物
总裁办公室主任职责
2014/01/02 职场文书
安全资金保障制度
2014/01/23 职场文书
秋季运动会广播稿大全
2014/02/17 职场文书
毕业生求职信范文
2014/06/29 职场文书
离婚协议书范本及离婚须知
2014/10/15 职场文书
民政局离婚协议书范本
2014/10/20 职场文书
督导岗位职责
2015/02/04 职场文书
2015年小学数学教研组工作总结
2015/05/21 职场文书
科级干部培训心得体会
2016/01/06 职场文书
MySQL中VARCHAR与CHAR格式数据的区别
2021/05/26 MySQL