JavaScript词法作用域与调用对象深入理解


Posted in Javascript onNovember 29, 2012

关于 Javascript 的函数作用域、调用对象和闭包之间的关系很微妙,关于它们的文章已经有很多,但不知道为什么很多新手都难以理解。我就尝试用比较通俗的语言来表达我自己的理解吧。
作用域 Scope
Javascript 中的函数属于词法作用域,也就是说函数在它被定义时的作用域中运行而不是在被执行时的作用域内运行。这是犀牛书上的说法。但"定义时"和"执行(被调用)时"这两个东西有些人搞不清楚。简单来说,一个函数A在"定义时"就是 function A(){} 这个语句执行的时候就是定义这个函数的时候,而A被调用的时候是 A() 这个语句执行的时候。这两个概念一定要分清楚。
那词法作用域(以下称之为"作用域",除非特别指明)到底是什么呢?它是个抽象的概念,说白了它就是一个"范围",scope 在英文里就是范围的意思。一个函数的作用域是它被定义时它所处的"范围",也就是它外层的"范围",这个"范围"包含了外层的变量属性,这个"范围"被设置成这个函数的一个内部状态。一个全局函数被定义的时候,全局(这个函数的外层)的"范围"就被设置成这个全局函数的一个内部状态。一个嵌套函数被定义的时候,被嵌套函数(外层函数)的"范围"就被设置成这个嵌套函数的一个内部状态。这个"内部状态"实际上可以理解成作用域链,见下文。
照以上说法,一个函数的作用域是它被定义的时候所处的"范围",那么 Javascript 里的函数作用域是在函数被定义的时候就确定了,所以它是静态的作用域,词法作用域又称为静态作用域。
调用对象 Call Object
一个函数的调用对象是动态的,它是在这个函数被调用时才被实例化的。我们已经知道,当一个函数被定义的时候,已经确定了它的作用域链。当 Javascript 解释器调用一个函数的时候,它会添加一个新的对象(调用对象)到这个作用域链的前面。这个调用对象的一个属性被初始化成一个名叫 arguments 的属性,它引用了这个函数的 Arguments 对象,Arguments 对象是函数的实际参数。所有用 var 语句声明的本地变量也被定义在这个调用对象里。这个时候,调用对象处在作用域链的头部,本地变量、函数形式参数和 Arguments 对象全部都在这个函数的范围里了。当然,这个时候本地变量、函数形式参数和 Arguments 对象就覆盖了作用域链里同名的属性。
作用域、作用域链和调用对象之间的关系
我的理解是,作用域是是抽象的,而调用对象是实例化的。
在函数被定义的时候,实际上也是它外层函数执行的时候,它确定的作用域链实际上是它外层函数的调用对象链;当函数被调用时,它的作用域链是根据定义的时候确定的作用域链(它外层函数的调用对象链)加上一个实例化的调用对象。所以函数的作用域链实际上是调用对象链。在一个函数被调用的时候,它的作用域链(或者称调用对象链)实际上是它在被定义的时候确定的作用域链的一个超集。
它们之间的关系可以表示成:作用域?作用域链?调用对象。
太绕口了,举例说明吧:

function f(x) { 
var g = function () { return x; } 
return g; 
} 
var g1 = f(1); 
alert(g1()); //输出 1 
假设我们把全局看成类似以下这样的一个大匿名函数: 
(function() { 
//这里是全局范围 
})(); 
那么例子就可以看成是: 
(function() { 
function f(x) { 
var g = function () { return x; } 
return g; 
} 
var g1 = f(1); 
alert(g1()); //输出 1 
})();

全局的大匿名函数被定义的时候,它没有外层,所以它的作用域链是空的。
全局的大匿名函数直接被执行,全局的作用域链里只有一个 '全局调用对象'。
函数 f 被定义,此时函数 f 的作用域链是它外层的作用域链,即 '全局调用对象'。
函数 f(1) 被执行,它的作用域链是新的 f(1) 调用对象加上函数 f 被定义的时候的作用域链,即 'f(1) 调用对象->全局调用对象'。
函数 g (它要被返回给 g1,就命名为 g1吧)在 f(1) 中被定义,它的作用域链是它外层的函数 f(1) 的作用域链,即 'f(1) 调用对象->全局调用对象'。
函数 f(1) 返回函数 g 的定义给 g1。
函数 g1 被执行,它的作用域链是新的 g(1) 调用对象加上外层 f(1) 的作用域链,即 'g1 调用对象->f(1)调用对象->全局调用对象'。
这样看就很清楚了吧。
闭包 Closuer
闭包的一个简单的说法是,当嵌套函数在被嵌套函数之外调用的时候,就形成了闭包。
之前的那个例子其实就是一个闭包。g1 是在 f(1) 内部定义的,却在 f(1) 返回后才被执行。可以看出,闭包的一个效果就是被嵌套函数 f 返回后,它内部的资源不会被释放。在外部调用 g 函数时,g 可以访问 f 的内部变量。根据这个特性,可以写出很多优雅的代码。
例如要在一个页面上作一个统一的计数器,如果用闭包的写法,可以这么写:
var counter = (function() { 
var i = 0; 
var fns = {"get": function() {return i;}, 
"inc": function() {return ++i;}}; 
return fns; 
})(); 
//do something 
counter.inc(); 
//do something else 
counter.inc(); 
var c_value = counter.get(); //now c_value is 2

这样,在内存中就维持了一个变量 i,整个程序中的其它地方都无法直接操作 i 的值,只能通过 counter 的两个操作。
在 setTimeout(fn, delay) 的时候,我们不能给 fn 这个函数句柄传参数,但可以通过闭包的方法把需要的参数绑定到 fn 内部。
for(var i=0,delay=1000; i< 5; i++, delay +=1000) { 
setTimeout(function() { 
console.log('i:' + i + " delay:" + delay); 
}, delay); 
}

这样,打印出来的值都是
i:5 delay:6000
i:5 delay:6000
i:5 delay:6000
i:5 delay:6000
i:5 delay:6000
改用闭包的方式可以很容易绑定要传进去的参数:
for(var i=0, delay=1000; i < 5; i++, delay += 1000) { 
(function(a, _delay) { 
setTimeout(function() { 
console.log('i:'+a+" delay:"+_delay); 
}, _delay); 
})(i, delay); 
}

输出:
i:0 delay:1000
i:1 delay:2000
i:2 delay:3000
i:3 delay:4000
i:4 delay:5000
闭包还有一个很常用的地方,就是在绑定事件的回调函数的时候。也是同样的道理,绑定的函数句柄不能做参数,但可以通过闭包的形式把参数绑定进去。
总结
函数的词法作用域和作用域链是不同的东西,词法作用域是抽象概念,作用域链是实例化的调用对象链。
函数在被定义的时候,同时也是它外层的函数在被执行的时候。
函数在被定义的时候它的词法作用域就已经确定了,但它仍然是抽象的概念,没有也不能被实例化。
函数在被定义的时候还确定了一个东西,就是它外层函数的作用域链,这个是实例化的东西。
函数在被多次调用的时候,它的作用域链都是不同的。
闭包很强大。犀牛书说得对,理解了这些东西,你就可以自称是高级 Javascript 程序员了。因为利用好这些概念,可以玩转 Javascript 的很多设计模式。
Javascript 相关文章推荐
基于jquery的一个浮动框(扩展性比较好 )
Aug 27 Javascript
一个JavaScript用逗号分割字符串实例
Sep 22 Javascript
js读取cookie方法总结
Oct 31 Javascript
jQuery.extend 函数及用法详细
Sep 06 Javascript
浅谈js多维数组和hash数组定义和使用
Jul 27 Javascript
jQuery动态创建元素以及追加节点的实现方法
Oct 20 Javascript
xmlplus组件设计系列之网格(DataGrid)(10)
May 05 Javascript
AngularJS创建一个上传照片的指令实例代码
Feb 24 Javascript
关于Google发布的JavaScript代码规范你要知道哪些
Apr 04 Javascript
对angularJs中自定义指令replace的属性详解
Oct 09 Javascript
详解vue-router数据加载与缓存使用总结
Oct 29 Javascript
浅谈vue项目,访问路径#号的问题
Aug 14 Javascript
浏览器加载、渲染和解析过程黑箱简析
Nov 29 #Javascript
javascript控制swfObject应用介绍
Nov 29 #Javascript
javascript 保存文件到本地实现方法
Nov 29 #Javascript
jquery连缀语法如何实现
Nov 29 #Javascript
javascript 使td内容不换行不撑开
Nov 29 #Javascript
json原理分析及实例介绍
Nov 29 #Javascript
javascript全局变量封装模块实现代码
Nov 28 #Javascript
You might like
Php图像处理类代码分享
2012/01/19 PHP
PHP取进制余数函数代码
2012/01/19 PHP
Sublime里直接运行PHP配置方法
2014/11/28 PHP
php将csv文件导入到mysql数据库的方法
2014/12/24 PHP
php实现读取手机客户端浏览器的类
2015/01/09 PHP
PHP中文乱码解决方案
2015/03/05 PHP
php json中文编码为null的解决办法
2016/12/14 PHP
PHP如何将图片文件上传到另外一台服务器上
2019/08/26 PHP
js之事件冒泡和事件捕获详细介绍
2013/10/28 Javascript
js图片处理示例代码
2014/05/12 Javascript
基于javascript实现精确到毫秒的倒计时限时抢购
2016/04/17 Javascript
js实现键盘自动打字效果
2016/12/23 Javascript
jquery.zclip轻量级复制失效问题
2017/01/08 Javascript
vue v-on监听事件详解
2017/05/17 Javascript
JS实现给json数组动态赋值的方法示例
2020/03/19 Javascript
vue-router 权限控制的示例代码
2017/09/21 Javascript
vue.js element-ui tree树形控件改iview的方法
2018/03/29 Javascript
JS加密插件CryptoJS实现的Base64加密示例
2020/08/16 Javascript
javascript读取本地文件和目录方法详解
2020/08/06 Javascript
Vue toFixed保留两位小数的3种方式
2020/10/23 Javascript
《Python之禅》中对于Python编程过程中的一些建议
2015/04/03 Python
python爬取酷狗音乐排行榜
2019/02/20 Python
Python实现连接MySql数据库及增删改查操作详解
2019/04/16 Python
详解Python odoo中嵌入html简单的分页功能
2019/05/29 Python
详解Windows下PyCharm安装Numpy包及无法安装问题解决方案
2020/06/18 Python
Pytorch 解决自定义子Module .cuda() tensor失败的问题
2020/06/23 Python
Python读取多列数据以及用matplotlib制作图表方法实例
2020/09/23 Python
python3爬虫中多线程进行解锁操作实例
2020/11/25 Python
Hudson Jeans官网:高级精制牛仔裤
2018/11/28 全球购物
The North Face北面法国官网:美国著名户外品牌
2019/11/01 全球购物
美国主要的特色咖啡和茶公司:Peet’s Coffee
2020/02/14 全球购物
高中生自我鉴定范文
2013/10/30 职场文书
大学自我鉴定
2013/12/20 职场文书
暑假家长评语大全
2014/04/17 职场文书
2014年教师业务工作总结
2014/12/19 职场文书
2015学习委员工作总结范文
2015/04/03 职场文书