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 相关文章推荐
前端开发部分总结[兼容性、DOM操作、跨域等](持续更新)
Mar 04 Javascript
使用jQuery+HttpHandler+xml模拟一个三级联动的例子
Aug 09 Javascript
详解JavaScript基于面向对象之继承
Dec 13 Javascript
jQuery插件 Jqplot图表实例
Jun 18 Javascript
JavaScript面向对象分层思维全面解析
Nov 22 Javascript
vue.js todolist实现代码
Oct 29 Javascript
vue 组件中添加样式不生效的解决方法
Jul 06 Javascript
微信小程序实现上传图片裁剪图片过程解析
Aug 22 Javascript
微信小程序3D轮播实现代码
Sep 19 Javascript
Vue之Mixins(混入)的使用方法
Sep 24 Javascript
vue项目创建步骤及路由router
Jan 14 Javascript
JavaScript 数组去重详解
Sep 15 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实现数字补零功能的2个函数介绍
2014/05/12 PHP
详解Yii2.0 rules验证规则集合
2017/03/21 PHP
prototype 源码中文说明之 prototype.js
2006/09/22 Javascript
jquery 操作DOM案例代码分享
2012/04/05 Javascript
将页面table内容与样式另存成excel文件的方法
2015/08/05 Javascript
快速掌握Node.js环境的安装与运行方法
2016/02/16 Javascript
JavaScript通过使用onerror设置默认图像显示代替alt
2016/03/01 Javascript
js导出excel文件的简洁方法(推荐)
2016/11/02 Javascript
Bootstrap输入框组件使用详解
2017/06/09 Javascript
微信小程序自定义导航隐藏和显示功能
2017/06/13 Javascript
原生js封装的ajax方法示例
2018/08/02 Javascript
Nodejs中使用puppeteer控制浏览器中视频播放功能
2019/08/26 NodeJs
jQuery实现全选按钮
2021/01/01 jQuery
Python函数学习笔记
2008/10/07 Python
python传递参数方式小结
2015/04/17 Python
python的mysqldb安装步骤详解
2017/08/14 Python
关于Python的一些学习总结
2018/05/25 Python
python通过微信发送邮件实现电脑关机
2018/06/20 Python
Python统计纯文本文件中英文单词出现个数的方法总结【测试可用】
2018/07/25 Python
Python中常用的高阶函数实例详解
2020/02/21 Python
python爬虫开发之Beautiful Soup模块从安装到详细使用方法与实例
2020/03/09 Python
HTML5本地数据库基础操作详解
2016/04/26 HTML / CSS
英国在线房屋中介网站:Yopa
2018/01/09 全球购物
阿联酋网上花店:Ferns N Petals
2018/02/14 全球购物
体验完美剃须:The Art of Shaving
2018/08/06 全球购物
怎么处理XML的中文问题
2015/03/26 面试题
C#如何进行LDAP用户校验
2012/11/21 面试题
专科毕业生求职简历的自我评价
2013/10/12 职场文书
《满井游记》教学反思
2014/02/26 职场文书
重阳节标语大全
2014/10/07 职场文书
2014年财务经理工作总结
2014/12/08 职场文书
销售督导岗位职责
2015/04/10 职场文书
文明上网主题班会
2015/08/14 职场文书
《普罗米修斯》教学反思
2016/02/22 职场文书
opencv检测动态物体的实现
2021/07/21 Python
javascript的var与let,const之间的区别详解
2022/02/18 Javascript