概念
EC:函数执行环境(或执行上下文),Execution Context
ECS:执行环境栈,Execution Context Stack
VO:变量对象,Variable Object
AO:活动对象,Active Object
scope chain:作用域链
EC执行上下文
每次当控制器转到ECMAScript可执行代码的时候,就会进入到一个执行上下文。
可执行代码的类型
1、全局代码(
Global code
)
这种类型的代码是在“程序”级处理的:例如处理外部加载的js文件或本地的标签内的代码。全局代码不包括任何function体内的代码,这是个默认的代码运行环境,一旦代码被载入,引擎最先进入这个环境。
2、函数代码(Function code)
任何一个函数体内的代码,但是需要
注意
的是,具体的函数体内的代码是不包括内部函数的代码。
3、Eval代码(Eval code)
eval内部的代码
ESC(执行环境栈)
例:
function foo(i) { if (i < 0) return; console.log('begin:' + i); foo(i - 1); console.log('end:' + i);}foo(2);// 输出:// begin:2// begin:1// begin:0// end:0// end:1// end:2
浏览器中的JS解释器被实现为单线程,这也就意味着同一时间只能发生一件事情,其他的行为或事件将会被放在叫执行栈里面排队。下面是单线程栈的抽象视图:
当浏览器首次载入你的脚本,它将默认进入全局执行上下文。如果,你在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并创建一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。
如果你调用当前函数内部的其他函数,相同的事情会在此上演。代码的执行流程进入内部函数,创建一个新的执行上下文并把它压入执行栈的顶部。浏览器总会执行位于栈顶的执行上下文,一旦当前上下文函数执行结束,它将被从栈顶弹出,并将上下文控制权交给当前的栈。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。
看到这里,想必大家都已经深谙上述例子输出结果的原因了,这里我大概绘了一个流程图来帮助理解。
VO(变量对象)/AO(活动对象)
这里为什么要用一个
/
呢?按照字面理解,AO其实就是被激活的VO,两个其实是一个东西。
变量对象
(Variable object)
是说JS的执行上下文中
都有个对象
用来存放执行上下文中可被访问但是不能被
delete
的
函数标示符
、
形参
、
变量声明
等。它们会被挂在这个对象上,
对象的属性
对应
它们的名字
,
对象属性的值
对应
它们的值
,
但这个
对象
是
规范上
或者说是
引擎实现上
的
不可在JS环境中访问
到活动对象
激活对象
(Activation object)
有了变量对象存每个上下文中的东西,但是它什么时候能被访问到呢?就是
每进入一个执行上下文
时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了
EC建立的细节
1、创建阶段【当函数被调用,但未执行任何其内部代码之前】
- 创建作用域链(Scope Chain)
- 创建变量,函数和参数。
- 求”this“的值
2、执行阶段
初始化变量的值和函数的引用,解释/执行代码。
我们可以将每个执行上下文抽象为一个对象,这个对象具有三个属性
ECObj
: {
scopeChain
: {
/* 变量对象(variableObject)+ 所有父级执行上下文的变量对象*/
},
variableObject
: {
/*函数 arguments/参数,内部变量和函数声明 */
},
this
: {} }
解释器执行代码的伪逻辑
1
、查找调用函数的代码。
2
、执行代码之前,先进入创建上下文阶段: - 初始化作用域链 - 创建变量对象: - 创建
arguments
对象,检查上下文,初始化参数名称和值并创建引用的复制。 - 扫描上下文的函数声明(而非函数表达式): - 为发现的每一个函数,在变量对象上创建一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用。 - 如果函数的名字已经存在,引用指针将被重写。 - 扫描上下文的变量声明: - 为发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为
undefined
- 如果变量的名字已经在变量对象里存在,将不会进行任何操作并继续扫描。 - 求出上下文内部
this
的值。
3
、激活/代码执行阶段:- 在当前上下文上运行/解释函数代码,并随着代码一行行执行指派变量的值。
VO --- 对应上述第二个阶段
function
foo
(i)
{
var
a =
'hello'
var
b =
function
()
{}
function
c
()
{}}foo(
22
)
当我们调用
foo(22)
时,整个创建阶段是下面这样的
ECObj = { scopChain: {...},
variableObject:
{
arguments:
{
0
:
22
,
length:
1
},
i:
22
,
c:
pointer to function c()
a:
undefined,
b:
undefined },
this:
{ ... }}
正如我们看到的,在上下文创建阶段,VO的初始化过程如下(
该过程是有先后顺序的:
函数的形参==>>函数声明==>>变量声明
):
- 函数的形参(当进入函数执行上下文时) —— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined
- 函数声明(FunctionDeclaration, FD) —— 变量对象的一个属性,其属性名和值都是函数对象创建出来的;如果变量对象已经包含了相同名字的属性,则替换它的值
- 变量声明(var,VariableDeclaration) —— 变量对象的一个属性,其属性名即为变量名,其值为undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
对于函数的形参没有什么可说的,主要看一下函数的声明以及变量的声明两个部分。
1、如何理解函数声明过程中
如果变量对象已经包含了相同名字的属性,则替换它的值
这句话?
例:
function
foo1
(
a
)
{
console
.log(a)
function
a
()
{} }foo1(
20
)
//'function a(){}'
根据上面的介绍,我们知道VO创建过程中,函数形参的优先级是高于函数的声明的,
结果是
函数体内部声明的
function a(){}
覆盖了函数形参
a
的声明,因此最后输出
a
是一个
function
2、如何理解变量声明过程中
如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性
这句话?
例
//情景一:与参数名相同
function
foo2
(
a
)
{
console
.log(a)
var
a =
10
}foo2(
20
)
//'20'
//情景二:与函数名相同
function
foo2
()
{
console
.log(a)
var
a =
10
function
a
()
{}}foo2()
//'function a(){}'
下面是几个比较有趣的例子,当做加餐小菜,大家细细品味。这里给出一句话当做参考:
函数声明比变量优先级要高,并且定义过程不会被变量覆盖,除非是赋值
function
foo3
(
a
)
{
var
a =
10
function
a
()
{}
console
.log(a)}foo3(
20
)
//'10'
function
foo3
(
a
)
{
var
a
function
a
()
{}
console
.log(a)}foo3(
20
)
//'function a(){}',
^上面的代码自我感觉有些问题,特别是console.log放在最前面或是最后面都不能如定义所说的结果一样
AO --- 对应第三个阶段
正如我们看到的,创建的过程仅负责处理定义属性的名字,而并不为他们指派具体的值,当然还有对形参/实参的处理。一旦创建阶段完成,执行流进入函数并且激活/代码执行阶段,看下函数执行完成后的样子:
ECObj = { scopeChain: { ... }, variableObject: {
arguments
: {
0
:
22
, length:
1
}, i:
22
, c: pointer to
function
c
()
a
: '
hello
',
b
:
pointer
to
function
privateB
() },
this
:
{ ... }}
提升(Hoisting)
对于下面的代码,相信很多人都能一眼看出输出结果,但是却很少有人能给出为什么会产生这种输出结果的解释。
(
function
()
{
console
.log(
typeof
foo);
// 函数指针
console
.log(
typeof
bar);
// undefined
var
foo =
'hello'
, bar =
function
()
{
return
'world'
; };
function
foo
()
{
return
'hello'
; }}());
1、为什么我们能在foo声明之前访问它?
回想在
VO
的创建阶段,我们知道函数在该阶段就已经被创建在变量对象中。所以在函数开始执行之前,foo已经被定义了。
2、Foo被声明了两次,为什么foo显示为函数而不是undefined或字符串?
我们知道,在创建阶段,函数声明是优先于变量被创建的。而且在变量的创建过程中,如果发现
VO
中已经存在相同名称的属性,则不会影响已经存在的属性。
因此,对
foo()
函数的引用首先被创建在活动对象里,并且当我们解释到var foo时,我们看见
foo
属性名已经存在,所以代码什么都不做并继续执行。
3、为什么bar的值是undefined?
bar
采用的是函数表达式的方式来定义的,所以
bar
实际上是一个变量,但变量的值是函数,并且我们知道变量在创建阶段被创建但他们被初始化为
undefined
,这也是为什么函数表达式不会被提升的原因。
总结:
1、
EC
分为两个阶段,创建执行上下文和执行代码。
2、每个
EC
可以抽象为一个对象,这个对象具有三个属性,分别为:作用域链
Scope
,
VO|AO
(
AO
,
VO
只能有一个)以及
this
。
3、函数
EC
中的
AO
在进入函数
EC
时,确定了Arguments对象的属性;在执行函数
EC
时,其它变量属性具体化。
4、
EC
创建的过程是由先后顺序的:参数声明
>
函数声明
>
变量声明
^
例1:
function foo3(a){
var a=10;
function a(){};
console.log(a);
}
foo3(20); //'10'
例2:
function foo3(a){
console.log(a)
var a = 10
function a(){}
}
foo3(20) ; //'function a(){}' ,根据上面的定义,总感觉这两段代码怪怪的,有问题
例3:
(function() {
console.log(typeof foo); //function
console.log(typeof bar); //undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
console.log(typeof foo); //string
console.log(typeof bar); //function
}());
例4:
function foo3(a){
var a=10;
console.log(a);
}
foo3(20); //10,奇葩,不是参数优先级比变量声明优先级高吗?还有这段:
function foo1(a){
console.log(a);
function a(){};
}
foo1(20)//'function a(){}' //不是参数优先级比函数声明优先级高吗?看来这两个都是一个特例