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 相关文章推荐
让ie6也支持websocket采用flash封装实现
Feb 18 Javascript
js window.open弹出新的网页窗口
Jan 16 Javascript
一个获取第n个元素节点的js函数
Sep 02 Javascript
深入浅析JavaScript面向对象和原型函数
Feb 06 Javascript
JS产生随机数的用法小结
Dec 10 Javascript
Vue不能检测到Object/Array更新的情况的解决
Jun 26 Javascript
JavaScript函数式编程(Functional Programming)箭头函数(Arrow functions)用法分析
May 22 Javascript
js实现计时器秒表功能
Dec 16 Javascript
js实现弹幕飞机效果
Aug 27 Javascript
Angular短信模板校验代码
Sep 23 Javascript
JavaScript实现随机点名小程序
Oct 29 Javascript
微信小程序实现滚动Tab选项卡
Nov 16 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
德劲1107的电路分析与打磨
2021/03/02 无线电
PHP+XML 制作简单的留言本 图文教程
2009/11/02 PHP
PHP处理Oracle的CLOB实例
2014/11/03 PHP
thinkphp3.x中display方法及show方法的用法实例
2016/05/19 PHP
PHP自动生成缩略图函数的源码示例
2019/03/18 PHP
laravel框架的安装与路由实例分析
2019/10/11 PHP
CSS3画一个阴阳八卦图
2021/03/09 HTML / CSS
添加到收藏夹代码(兼容几乎所有的浏览器)
2007/01/09 Javascript
jquery tools系列 expose 学习
2009/09/06 Javascript
超级有用的13个基于jQuery的内容滚动插件和教程
2011/07/31 Javascript
JS实现点击按钮控制Div变宽、增高及调整背景色的方法
2015/08/05 Javascript
JS实现滑动门效果的方法详解
2016/12/19 Javascript
用纯Node.JS弹出Windows系统消息提示框实例(MessageBox)
2017/05/17 Javascript
Vue0.1的过滤代码如何添加到Vue2.0直接使用
2017/08/23 Javascript
nodejs实现的简单web服务器功能示例
2018/03/15 NodeJs
js中获取URL参数的共用方法getRequest()方法实例详解
2018/10/24 Javascript
vue中轮训器的使用
2019/01/27 Javascript
vuex管理状态 刷新页面保持不被清空的解决方案
2019/11/11 Javascript
Vue微信公众号网页分享的示例代码
2020/05/28 Javascript
vue插件--仿微信小程序showModel实现模态提示窗功能
2020/08/19 Javascript
python实现贪吃蛇小游戏
2020/03/21 Python
python获取txt文件词向量过程详解
2019/07/05 Python
python 监测内存和cpu的使用率实例
2019/11/28 Python
jupyter 导入csv文件方式
2020/04/21 Python
Julep官网:美容产品和指甲油
2017/02/25 全球购物
TIME时代杂志台湾总代理:台时亚洲
2018/10/22 全球购物
C/C++有关内存的思考题
2015/12/04 面试题
汽车队司机先进事迹材料
2014/02/01 职场文书
四风存在的原因分析
2014/02/11 职场文书
吃空饷专项治理工作实施方案
2014/03/04 职场文书
接待员岗位职责
2015/02/13 职场文书
2015年办公室文秘工作总结
2015/04/30 职场文书
超市员工辞职信范文
2015/05/12 职场文书
Vue.js中v-bind指令的用法介绍
2022/03/13 Vue.js
「月刊Comic Alive」2022年5月号封面公开
2022/03/21 日漫
python计算列表元素与乘积详情
2022/08/05 Python