浅析JavaScript作用域链、执行上下文与闭包


Posted in Javascript onFebruary 01, 2016

闭包和作用域链是JavaScript中比较重要的概念,这两天翻阅了一些资料,把相关知识点给大家总结了以下。

JavaScript 采用词法作用域(lexical scoping),函数执行依赖的变量作用域是由函数定义的时候决定,而不是函数执行的时候决定。以下面的代码片段举例说明,通常来说(基于栈的实现,如 C 语言) foo 被调用之后函数内的本地变量 scope 会被释放,但是从词法上看 foo 的内嵌匿名函数中 scope 应该指的是 foo 的本地变量 scope ,并且实际上代码的运行结果跟词法上的表达式一致的,f 被调用之后返回的是local scope。函数对象 f 在其主体函数 foo 调用结束之后,依然保持着 foo 函数体作用域变量的引用,这就是所谓的闭包 。

var scope = 'global scope';
function foo() {
var scope = 'local scope';
return function () {
return scope;
}
}
var f = foo();
f(); // 返回 "local scope"

那么闭包到底是如何工作的呢?了解闭包首先需要了解变量作用域和作用域链,另外一个重要的概念是执行上下文环境。

变量作用域

JavaScript 中全局变量拥有全局的作用域,函数体内申明的变量的作用域是整个函数体内,是局部的,当然也包括函数体内定义的嵌套函数。函数体内局部变量的优先级高于全局变量,如果局部变量与全局变量重名,全局变量会被局部变量掩盖;同样嵌套函数内定义的局部变量的优先级高于嵌套函数所在函数的局部变量。这简直是显而易见的,几乎所有人都了解。
接下来谈谈可能大家比较陌生的。

函数声明提升

用一句话来说明函数申明提升,指的是函数体内部申明的变量再整个函数内有效。也就是说,就是在函数体最底部申明的变量,也会被提升到最顶部。举个例子:

var scope = 'global scope';
function foo() {
console.log(scope); // 这里不会打印出 "global scope",而是 "undefined"
var scope = 'local scope'; 
console.log(scope); // 很显然,打印出 "local scope"
}
foo();

第一个console.log(scope)会打印出undefined而不是global scope,是因为局部变量的申明被提升了,只是还未赋值。

作为属性的变量

在 JavaScript 中,有三种定义全局变量的方式,如下示例代码中的 globalVal1 、globalVal2 和 globalValue3 。一个有趣的现象是,实际上全局变量仅仅只是全局对象 window/global (在浏览器中是 window,在 node.js 中是 global)的属性而已。为了更加符合通常意义的变量定义, JavaScript 把用 var 定义的全局变量,设计成了不可删除的全局对象属性。 通过Object.getOwnPropertyDescriptor(this, 'globalVal1')可以得到,其 configurable 属性为 false 。

var globalVal1 = 1; // 不可删除的全局变量
globalVal2 = 2; // 可删除的全局变量
this.globalValue3 = 3; // 同 globalValue2
delete globalVal1; // => false 变量没有被删除
delete globalVal2; // => true 变量被删除
delete this.globalValue3; //=> true 变量被删除

那么问题来了,函数体内定义的局部变量是不是也作为某个对象的属性呢?答案是肯定的。这个对象是跟函数调用相关的,在 ECMAScript 3中称为“call object”、ECMAScript 5中称为“declaravite environment record”的对象。这个特殊的对象对我们来说是一种不可见的内部实现。

作用域链

从上一节我们知道,函数局部变量可与看做是某个不可见的对象的属性。那么 JavaScript 的词法作用域的实现可以这样描述:每一段 JavaScript 代码(全局或函数)都有一个跟它关联的作用域链,它可以是数组或链表结构;作用域链中的每一个元素定义了一组作用域内的变量;当我们要查找变量 x 的值,那么从作用域链的第一个元素中找这个变量,如果没有找到者找链表中的下一个元素中查找,直到找到或抵达链尾。了解作用域链的概念对理解闭包至关重要。

执行上下文

每段 JavaScript 代码的执行都与执行上下文绑定,运行的代码通过执行上下文获可用的变量、函数、数据等信息。全局的执行上下文是唯一的,与全局代码绑定,每执行一个函数都会创建一个执行上下文与其绑定。JavaScript 通过栈的数据结构维护执行上下文,全局执行上下文位于栈底,当执行一个函数的时候,新创建的函数执行上下文将会压入栈中,执行上下文指针指向栈顶,运行的代码即可获得当前执行的函数绑定的执行上下文。如果函数体执行嵌套的函数,也会创建执行上下文并压入栈,指针指向栈顶,当嵌套函数运行结束后,与它绑定的执行上下文被推出栈,指针重新指向函数绑定的执行上下文。同样,函数执行结束,指针会指向全局执行上下文。

执行上下文可以描述成式一个包含变量对象(对应全局)/活动对象(对应函数)、作用域链和 this 的数据结构。当一个函数执行时,活动对象被创建并绑定到执行上下文。活动对象包括函数体内申明的变量、函数、arguments 等。作用域链在上一节以及提到,是按词法作用域构建的。需要注意的是 this 不属于活动对象,在函数执行的那一刻就以及确定。
执行上下文的创建是有特定的次序和阶段的,不同阶段有不同的状态,具体的细节可以看一下参考资料,在结尾部分会列出。

闭包

了解了作用域链和执行上下文,回过头看篇首的那段代码,基本上就可以解释闭包式如何工作了。函数调用的时候创建的执行上下文以及词法作用域链保持函数调用所需要的信息, f 函数调用之后才可以返回local scope。

需要注意的是,函数内定义的多个函数使用的是同一个作用域链,在使用 for 循环赋值匿名函数对象的场景比较容易引起错误,举例如下:

var arr = [];
for (var i = 0; i < 10; i++) {
arr[i] = {
func: function() {
return i;
}
};
}
arr[0].func(); // 返回 10,而不是 0

arr[0].func()返回的是 10 而不是 0,跟感官上的语义有偏差。在 ECMAScript 6 引入 let 之前, 变量作用域范围是在整个函数体内而不是在代码区块之内,所以上面的例子中所有定义的 func 函数引用了同一个作用域链在 for 循环之后, i 的值已经变为 10 。

正确的做法是这样:

var arr = [];
for (var i = 0; i < 10; i++) {
arr[i] = {
func: getFunc(i)
};
}
function getFunc(i) {
return function() {
return i;
}
}
arr[0].func(); // 返回 0

以上内容给大家介绍了JavaScript作用域链、执行上下文与闭包的相关知识,希望对大家有所帮助。

Javascript 相关文章推荐
从sohu弄下来的flash中展示图片的代码
Apr 27 Javascript
js 实现无干扰阴影效果 简单好用(附文件下载)
Dec 27 Javascript
通过AngularJS实现图片上传及缩略图展示示例
Jan 03 Javascript
jQuery插件FusionCharts实现的2D饼状图效果【附demo源码下载】
Mar 03 Javascript
jQuery插件HighCharts绘制2D带Label的折线图效果示例【附demo源码下载】
Mar 08 Javascript
基于JavaScript实现的顺序查找算法示例
Apr 14 Javascript
ES6中的rest参数与扩展运算符详解
Jul 18 Javascript
js实现数字从零慢慢增加到指定数字示例
Nov 07 Javascript
jQuery实现简单弹幕效果
Nov 28 jQuery
JQuery事件委托(适用于给动态生成的脚本元素添加事件)
Feb 01 jQuery
Vue + Scss 动态切换主题颜色实现换肤的示例代码
Apr 27 Javascript
详解微信小程序动画Animation执行过程
Sep 23 Javascript
jQuery 3.0 的变化及使用方法
Feb 01 #Javascript
jQuery与Ajax以及序列化
Feb 01 #Javascript
js格式化输入框内金额、银行卡号
Feb 01 #Javascript
javascript嵌套函数和在函数内调用外部函数的区别分析
Jan 31 #Javascript
JavaScript中eval函数的问题
Jan 31 #Javascript
JS排序方法(sort,bubble,select,insert)代码汇总
Jan 30 #Javascript
JavaScript中的this机制
Jan 30 #Javascript
You might like
第三章 php操作符与控制结构代码
2011/12/30 PHP
PHP的SQL注入过程分析
2012/01/06 PHP
PHP正确解析UTF-8字符串技巧应用
2012/11/07 PHP
windows下PHP_intl.dll正确配置方法(apache2.2+php5.3.5)
2014/01/14 PHP
php绘图中显示不出图片的原因及解决
2014/03/05 PHP
WordPress中缩略图的使用以及相关技巧
2015/11/24 PHP
讲解WordPress开发中一些常用的debug技巧
2015/12/18 PHP
laravel 5.3 单用户登录简单实现方法
2019/10/14 PHP
javascript iframe内的函数调用实现方法
2009/07/19 Javascript
查询绑定数据岛的表格中的文本并修改显示方式的js代码
2009/12/15 Javascript
javascript 多种搜索引擎集成的页面实现代码
2010/01/02 Javascript
Javascript笔记一 js以及json基础使用说明
2010/05/22 Javascript
关于JavaScript中原型继承中的一点思考
2012/07/25 Javascript
JS跨域总结
2012/08/30 Javascript
使用AngularJS创建自定义的过滤器的方法
2015/06/18 Javascript
jquery实现简单合拢与展开网页面板的方法
2015/09/01 Javascript
轻松掌握JavaScript中介者模式
2016/08/26 Javascript
快速使用node.js进行web开发详解
2017/04/26 Javascript
vue2.0 axios前后端数据处理实例代码
2017/06/30 Javascript
Express系列之multer上传的使用
2017/10/27 Javascript
Vue的移动端多图上传插件vue-easy-uploader的示例代码
2017/11/27 Javascript
关于 angularJS的一些用法
2017/11/29 Javascript
JS通过ajax + 多列布局 + 自动加载实现瀑布流效果
2019/05/30 Javascript
Vue实现点击当前元素以外的地方隐藏当前元素(实现思路)
2019/12/04 Javascript
功能完善的小程序日历组件的实现
2020/03/31 Javascript
[01:02:25]2014 DOTA2华西杯精英邀请赛5 24 NewBee VS VG
2014/05/25 DOTA
浅谈Pandas中map, applymap and apply的区别
2018/04/10 Python
python GUI实现小球满屏乱跑效果
2019/05/09 Python
CSS3 3D立方体效果示例-transform也不过如此
2016/12/05 HTML / CSS
加拿大消费电子和手机购物网站:The Source
2017/01/28 全球购物
在线购买世界上最好的酒:BoozeBud
2018/06/07 全球购物
How TDD works
2012/09/30 面试题
广告设计专业自荐信范文
2013/11/14 职场文书
领班岗位职责范文
2014/02/06 职场文书
毕业典礼邀请函
2015/01/31 职场文书
接收函
2019/04/22 职场文书