执行上下文
execution context
又称执行上下文
或者执行环境
.
执行上下文
是JavaScript中一个极为重要的概念. 我们可以简单的理解执行上下文就是我们代码执行时所在的环境. 这个环境中包含着许多对我们来说十分重要的数据. 例如作用域链 , 数据的访问权,执行代码等等 .
当我们的代码在JavaScript中运行时,便会进入不同的执行上下文
.
其中Global execution context
是最外围层的一个执行环境 , 即JavaScript代码开始运行的默认环境.也就是我们常说的全局环境. 这个执行上下文是由浏览器创建并初始化的 .
除此之外,每个函数都有自己的一个执行上下文
. 当我们进行函数调用时, 其执行上下文
便会创建. 这也是我们所认为的局部环境( local context) .
执行环境栈
JavaScript引擎在执行代码之前, 首先会创建一个执行环境栈(execution context stack)
. 然后创建全局环境
并将其压入栈中作为栈底. 再之后 , 随着执行流进入函数中,函数的执行上下文
便会被推入 执行环境栈
中(即进入一个执行上下文). 而在函数执行完毕后, 执行环境栈
便会把这个函数的环境弹出 , 并将控制权返回给之前的执行环境
.
我们来分析一下如下代码中的执行环境栈是如何变化的:
<script>
function fn() {
function fn2() {};
fn2();
};
fn();
</script>
其大致流程如下图
variable object
在javascript高级程序
中是这样描述variable object
的:
每个执行环境都有一个与之相关联的对象,并且环境中定义的变量和函数都在其中.
由此我们便知道,variable object
中存储了在上下文中定义的变量和函数声明. 也就是说我们所使用的变量或者函数的标识符
则都是在variable object
上来查询其是否可用.
注: 下文会将其简写为VO
通常情况下,一个variable object
有以下信息:
- 变量 (var, Variable Declaration);
- 函数声明 (Function Declaration, FD);
- 函数的形参
并且因为执行环境
的不同,所以variable object
也有更细的划分或是不同的表现.其关系如下图:
- global object
对于全局环境,浏览器中用window
对象来作为它的variable object
. 该全局对象中存储着JavaScript所定义的全部内置对象和全局函数,以及宿主环境的宿主对象和我们在全局环境中定义的函数和变量等.
这也是为什么我们在全局环境中声明一个变量和函数后,其会成为window对象
的属性.
var a = 11;
function fn(){};
window.a ===a ; // -> true
window.fn ===fn; // -> true
- activation object
对于局部环境,我们通常用activation object
来作为variable object
.并且这个对象我们是不能用代码访问到的.这个对象中存储着我们在函数内部声明的变量和函数以及参数(包含隐式参数arguments).
注: 下文会将其简写为AO
我们还要说明一下,activation object
在最开始的时候只有一个变量,即arguments对象(这个对象在全局环境上是不存在的).
执行上下文的两个阶段
常言道: 兵马未动 粮草先行
因此当我们进入一个执行上下文
的时候,js解释器并不会立即就去执行其内部代码, 而是先做一些预备工作来保证之后的执行无后顾之忧.
其大致可分为两个阶段:
- 准备阶段
- 创建
activation object
- 创建
Scope chain
(即作用域链) - 设置
this
的值 - 抽离出执行代码.(剔除声明部分的代码)
- 创建
- 激活/执行阶段
- 设置变量的值、函数的引用,然后解释/执行代码
然后下文中我们会着重讲一下准备阶段
中一些过程的细节.
AO(activation object)的创建过程
在创建
activation object
之初,就为其添加了一个名为arguments
对象,里面存储着函数调用时传入的所有参数.-
扫描函数的形式参数
- 对于所有找到的参数,将由参数名和其对应值以
key-value
的形式存入VO/AO中,如果没有传递对应参数的话,那么将参数名和undefined值以key-value
的形式存入VO/AO中.
- 对于所有找到的参数,将由参数名和其对应值以
-
扫描函数内部代码,查找函数声明(Function declaration).
- 对于所有找到的函数声明,将函数名和函数引用存入VO/AO中.
- 并且如果VO/AO中已经有同名的标识符, 那么就对其标识符的引用进行覆盖.
-
扫描函数内部代码,查找变量声明(Variable declaration)
- 对于所有找到的变量声明,将变量名存入VO/AO中,并初始化为"undefined".
- 如果变量名称跟已经声明的参数或函数名相同,则变量声明不会干扰已经存在的标识符和其引用.
我们以如下代码为例:
function add(a,b){
var z ;
function fn(){
console.log(a,b)
}
z = a+b;
return z;
}
add(10,20)
接着我们来分析一下AO
的创建过程, 如图:
然后我们来验证一下当标识符命名发生冲突时会怎样:
- 参数名和函数名冲突
function fn(x){
function x(){};
console.log(x)
}
fn(10) // -> ƒ x(){}
我们会发现当参数名和函数名冲突时,函数的引用值会覆盖参数名的引用值.
- 参数名和变量名冲突
function fn(x){
var x ;
console.log(x)
}
fn(10) // -> 10
我们会发现当参数名和变量名冲突时,变量声明不会干扰已经存在的标识符和其引用.
- 变量名和函数名冲突
function fn(){
function x(){};
var x ;
console.log(x)
}
fn() // -> ƒ x(){}
这一结果又刚好进一步证明了我们上一步得出结论.
我们再来看最后一段代码:
function fn(){
console.log(x)
var x = 10 ;
}
fn() // -> undefined
我们看见了,其输出的结果是undefined
. 不知你有没有发现,上述代码与变量提升有关.而AO
的创建过程则正是造成变量提升
这一现象的根源.
函数对象的[[scope]]
属性
在JavaScript中, 一切皆为对象
JavaScript 中每个函数都表示为一个函数对象(函数实例),既然是对象,就有相关的属性和方法。除了正常的属性,函数对象具有仅供 JavaScript 引擎内部使用,但不能通过代码访问的一系列内部属性。这些属性中,其中一个就是 [[scope]] 属性。
函数内部的 [[scope]]
属性实际是一个variable object
的集合.其包含了当前执行环境栈
中所有执行上下文
的variable object
. 而这也正是作用链
的雏形.
注意: 因为该函数还未调用,所以执行环境栈中没有这个函数的执行上下文.
作用域链
正如上文所述, 执行上下文中有很多对我们来说十分重要的数据. 除了variable object
以外 ,作用域链也是其中之一.
- 创建
作用域链
的过程:
- 将函数内部[scope]属性中的所有对象按顺序复制到作用域链中,完成作用域链的初始化.
- 将当前
执行上下文
的variable object
放入作用域链的顶端,彻底完成作用域链的创建.
正因此,当前执行上下文的variable object
一直都是其作用域链的开头 .
- 标志符的查询
当在某个环境中为了读取或是写入而引用一个标志符,必须通过搜索来确定标识符代表什么. 搜索过程从作用域链的顶端开始,向上逐级查询与给定名字匹配的标识符.
在此我们再详细说明一下其搜索过程的细节. 如果在局部环境中找到了该标识符,搜索就会停止,变量就绪. 若是没有找到该变量名/函数名
,那么就会沿着作用域链查找.搜索过程会一种追溯到全局对象上,若是还没有查找到该标识符,那么便意味着该变量/函数并未声明,输出错误信息.
var a = 10 ;
var b = 100;
function fn(){
var b = 20 ;
console.log(a) // -> 10
console.log(b) // -> 20
}
fn()
以上段代码为例,其在沿着作用域链上的查询过程如图:
结束语
写这篇文章的原因来自一次偶然的思考, 然后便是大量的翻阅相关资料.甚至为此我还用我那有些蹩脚的英文翻墙去查阅一些外文资料.其过程自然有些艰辛的,但是黄天不负有心人,我总算是理解了JavaScript中的一些核心概念.并且我能感受的到,一扇新的大门即将为我开启, 征程还未停止.不过我有些喜欢上了这种在路上
的感觉了.