前言
之前在阅读《Javascript高级程序设计》「4.2执行环境及作用域的」时候,对相关的概念理解得并不是非常的透彻,只是懂了大概的意思。后来在看到「闭包」这一节时书中再一次提到了相关的概念,并且这些是充分理解闭包的必要背景知识,于是这一次我不能再略读了,必须彻彻底底地弄明白。啃了两天的相关文章、资料后,算是有一个比较清晰的认识了,现在记录下来,希望可以帮到同样对相关概念不熟悉的同学,也可以用作自己日后的回顾和修正。
注:Execution Context 可以被翻译为「执行上下文」或者「 执行环境」,文中可能都会用到,大家记住是一个东西就可以了。
什么是执行环境(Execution Context)?
“每当程序的执行流进入到一个可执行的代码时,就进入到了一个执行环境中。”
执行环境是 ECMA-262 中用以区分不同的可执行代码的抽象概念
可执行代码的类型可以为分为:
- 全局代码:程序载入后的默认环境,是运行在程序级别的代码。
- 函数代码:当执行流进入一个函数后。
- Eval代码:Eval 内部的代码。
#
执行环境栈(Execution Stack)
执行流依次进入的执行环境在逻辑上形成了一个栈,栈的底部永远是全局环境,栈的顶部则是处于活动状态当前的执行环境(浏览器总是执行处于栈顶的上下文)。当执行流进入一个函数时,函数的环境就会被推入这个环境栈中,当函数执行完毕之后,栈将这个执行环境弹出,然后把控制权返回给之前的执行环境。这样实现的原因是由于 Javascript 解释器是单线程的,也就是同一时刻只能发生一件事情,其他等待执行的上下文或事件就会在这个环境栈中排队等待。值得注意的一点是:每次函数的调用都会创建一个执行环境压入栈中,无论是函数内部的函数、还是递归调用等。
我们用数组来表示执行环境栈:
ECStack = [];
来看下面这个例子:
(function foo(i){
if(i === 3){
return console.log("Well,the current FunctionContext is finished.");
}
else{
foo(++i);
}
})(1);
这个函数会被调用3次,分别是 i = 1,i = 2,i = 3 的时候,每次被调用的时候都会创建一个执行上下文然后压入栈中,执行完毕之后再被弹出,最后将控制权交给栈底的全局环境。当第三次调用 foo 函数也就是 i = 3 时,ECStack 状态如下:
ECStack =
[
//栈顶
FunctionContext - foo(3);
FunctionContext - foo(2);
FunctionContext - foo(1);
GlobalContext
//栈底
]
变量对象 (Variable Object)
每一个执行环境都有一个与之相关的变量对象,其中存储着上下文中声明的:
- 变量 VariableDeclaration VD
- 函数 FunctionDeclaration FD
- 形式参数 formal parameters
我们可以用一个对象来表示变量对象:
VO = {
// 执行上下文中声明的变量、函数、形式参数
}
不同执行环境下的变量对象
变量对象是一个抽象的概念,在进入具体的执行上下文时,变量对象在具体实现上也会有相应地差别。
AbstractVO (generic behavior of the variable instantiation process)
║
╠══> GlobalContextVO
║
(VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, <arguments> object and <formal parameters> are added)
全局上下文中的变量对象
全局对象是一个在进入任何执行上下文前就创建出来的对象;此对象以单例形式存在;它的属性在程序任何地方都可以直接访问,其生命周期随着程序的结束而终止。
全局对象的属性在任何地方都可以被访问到,可以通过 this 或者 DOM 中的 Window 对象来访问。全局对象中的变量对象就是全局对象本身,理解这一点很重要,正是因为这个原因才使得可以通过全局对象的属性来访问在全局上下文中声明的变量。
函数上下文中的变量对象
当函数被调用时,一个特殊的对象——活动对象就随之创建了。变量对象通过函数的 arguments 对象来初始化,arguments 对象是活动对象上的属性,包含了以下属性:
- callee 对当前函数的引用
- length 传入的实参个数
- properties-indexes 参数对应的索引值,相应的值和实际传入的参数值是共享的,但不并是存储在同一个地方的
执行环境的具体细节
我们同样也可以用一个对象来表示执行上下文:
ExecutionContextObj = {
scopeChain: { 变量对象(variableObject)+ 所有父执行上下文的变量对象 },
variableObject: { <arguments>对象,内部变量声明和函数声明 },
this:{}
}
每当一个函数被调用的时候,就会随之创建一个执行上下文,在 Javascript 解释器内部处理执行上下文有两个步骤:
- 第一步:创建阶段 (在函数调用之后,函数体执行之前),解释器扫描传递给函数的参数或arguments,本地函数声明和本地变量声明,并创建executionContextObj对象。扫描的结果将完成变量对象的创建
*创建作用域链 (Scope Chain)
-
-
扫描上下文中声明的形式参数、函数以及变量,并依次填充变量对象的属性
-
函数的形参:形参作为属性,对应的实参作为值。对于没有实参的形参,值为undefined。
- 函数声明(FunctionDeclaration FD):由函数对象创建出相应的名、值,名就是函数名、值就是函数体。如果变量对象已经包含了同名的属性,就会替换掉它的值。
- 变量声明(VariableDeclaration):属性名是变量名,值初始化为 undefined。如果变量名和已经存在的属性同名,不会影响到同名的属性。
-
注意:函数表达式(FunctionExpression FE)不会成为变量对象的属性,也就是说函数表达式不会影响到变量对象。
-
求出上下文“this”的值
-
第二步:代码执行阶段
-
- 这一阶段就会给第一步中初始值为 undefined 的变量赋上相应的值
我们来看下面这个例子:
(function foo(x,y,z){
var a = 1;
var b = function(){};
function c(){}
(function d(){})();
})(10,20);
函数调用后,相应的executionContextObj如下:
第一阶段
executionContextObj = {
scopeChain:{...},
VO: {
arguments:{
x:10,
y:20,
Z:undefined,
length:2,//这里是实际传入参数的个数
callee:pointer to function foo()
}
a:undefined,
b:undefined,
c:pointer to function c()
},
this:{...}
}
第二阶段:
executionContextObj = {
scopeChain:{...},
VO: {
arguments:{
x:10,
y:20,
Z:undefined,
length:2,//这里是实际传入参数的个数
callee:pointer to function foo()
}
a:1,
b:pointer to function b(),
c:pointer to function c()
},
this:{...}
}
在第二阶段,就会为局部变量 a 、b 赋值,注意到 d 并没有在变量对象中,正如上文中提到的那样,函数表达式是不会影响变量对象的,所以在作用域中任何一个位置引用d都会出现“d is not defined”的错误。
现在你应该非常清楚JS中的变量、函数声明提升是怎么回事了吧。
举个例子吧:
(function foo(){
console.log(typeof x);//"function"
var x = 10;
console.log(y);//undefined 而不是 “y is not defined” ,这就是变量声明提升!
var y = 20;
console.log(typeof x);//"number"
function x(){}
})();
为什么第一次打印x的类型是函数,第二次打印x的类型又是数字呢。这是因为,根据创建上下文时的规则,函数调用之后会按照顺序依次把函数参数、函数声明、变量声明填充为VO的属性,并且填充变量声明的时候如果同名是不会造成任何影响的,x的值还是函数。
在进入上下文阶段,VO的状态:
VO = {
x:pointer to function x()
}
//发现var x = 10;
//如果函数“x”还未定义,则 "x" 的值为undefined,
//但是,在这个例子中
//变量声明并不会影响同名的值为函数的x
VO[‘x’] 的值仍未改变
在代码执行阶段,VO的状态:
VO['x'] = 10;
这一阶段,局部变量 x 被赋值,此时之前同名的值为函数的 x 就会被覆盖,大家注意声明和赋值!!第一阶段,局部变量声明同名不会影响;第二阶段局部变量赋值就会产生影响了,毕竟人家是最后赋值的嘛。
最后,再来说说关于变量声明的问题:
在《Javascript高级程序设计》4.2.2一节当中有这么一句话:“如果初始化变量时没有使用var声明,该变量会自动被添加到全局环境中。” 首先,我们应该先明确一点,使用var关键字是声明变量的唯一方式。如果没有var 的话,例如 a = 5 ,a 就将作为全局对象的一个属性,而不是一个变量。
区别如下:
alert(x); //"x" is not defined
alert(b); //"undefined
x = 10;
var y = 20;
进入上下文后第一阶段:
VO 中并没有y的原因是,y 并不是变量。另外还要注意的一点就是,没有通过 var 声明的属性可以通过delete操作符删除,而通过var声明的变量就不可以。