浅析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 相关文章推荐
js向上无缝滚动,网站公告效果 具体代码
Nov 18 Javascript
JavaScript实现的简单拖拽效果
Jun 01 Javascript
封装好的javascript前端分页插件pagination
Jan 04 Javascript
JQuery导航菜单选择特效
Apr 11 Javascript
浅谈在js传递参数中含加号(+)的处理方式
Oct 11 Javascript
Vue.directive自定义指令的使用详解
Mar 10 Javascript
js字符串与Unicode编码互相转换
May 17 Javascript
基于JavaScript实现微信抢红包功能
Jul 20 Javascript
[js高手之路]单例模式实现模态框的示例
Sep 01 Javascript
JavaScript 中使用 Generator的方法
Dec 29 Javascript
node.js使用net模块创建服务器和客户端示例【基于TCP协议】
Feb 14 Javascript
vue-列表下详情的展开与折叠案例
Jul 28 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
解决dede生成静态页和动态页转换的一些问题,及火车采集入库生成动态的办法
2007/03/29 PHP
php使用filter过滤器验证邮箱 ipv6地址 url验证
2013/12/25 PHP
通过Email发送PHP错误的方法
2015/07/20 PHP
js字符串的各种格式的转换 ToString,Format
2011/08/08 Javascript
javascript(js)的小数点乘法除法问题详解
2014/03/07 Javascript
window.location.href IE下跳转失效的解决方法
2014/03/27 Javascript
深入分析JSONP跨域的原理
2014/12/10 Javascript
深入探密Javascript数组方法
2015/01/08 Javascript
在AngularJS中如何使用谷歌地图把当前位置显示出来
2016/01/25 Javascript
举例讲解如何判断JavaScript中对象的类型
2016/04/22 Javascript
JS实现iframe自适应高度的方法(兼容IE与FireFox)
2016/06/24 Javascript
AngularJS中transclude用法详解
2016/11/03 Javascript
JS调用Android、Ios原生控件
2017/01/06 Javascript
js常用DOM方法详解
2017/02/04 Javascript
vue通过watch对input做字数限定的方法
2017/07/13 Javascript
新手如何快速入门Python(菜鸟必看篇)
2017/06/10 Python
Python调用adb命令实现对多台设备同时进行reboot的方法
2018/10/15 Python
Django 模型类(models.py)的定义详解
2019/07/19 Python
使用python快速在局域网内搭建http传输文件服务的方法
2019/11/14 Python
Matplotlib绘制雷达图和三维图的示例代码
2020/01/07 Python
python3实现raspberry pi(树莓派)4驱小车控制程序
2020/02/12 Python
如何通过Python3和ssl实现加密通信功能
2020/05/09 Python
numpy矩阵数值太多不能全部显示的解决
2020/05/14 Python
用于ETL的Python数据转换工具详解
2020/07/21 Python
Python logging模块handlers用法详解
2020/08/14 Python
如何把python项目部署到linux服务器
2020/08/26 Python
Html5写一个简单的俄罗斯方块小游戏
2019/12/03 HTML / CSS
阿迪达斯俄罗斯官方商城:adidas俄罗斯
2017/03/08 全球购物
Bluebella法国官网:英国性感内衣品牌
2019/05/03 全球购物
企业承诺书格式
2014/05/21 职场文书
小学学校评估方案
2014/06/08 职场文书
纪念九一八爱国演讲稿600字
2014/09/14 职场文书
清明节寄语2015
2015/03/23 职场文书
2015年物流客服工作总结
2015/07/27 职场文书
Python Flask请求扩展与中间件相关知识总结
2021/06/11 Python
vue elementUI表格控制对应列
2022/04/13 Vue.js