执行上下文
首先明确几个概念:
EC
:函数执行环境(或执行上下文)ECS
:执行环境栈VO
:变量对象AO
:活动对象scope chain
: 作用域链
EC (执行上下文)
每当控制器转到 ECMAScript 可执行代码的时候,就会进入到一个执行上下文
那什么是可执行代码呢?
可执行代码的类型
全局代码(
)
这种类型的代码是在”程序”级处理的:例如加载外部的 js 文件或者本地
<script></script>
标签内的代码。全局代码不包括任何 function 体内的代码。这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
函数代码(
)
任何一个函数体内的代码,但是需要注意的是,具体的函数体内的代码是不包括内部函数的代码。
Eval 代码(
)
eval 内部的代码
ECS (执行环境栈)
由于浏览器中的 JS 解释器被实现为单线程,这也就意味着同一时间只能发生一件事情,其他行为将会放在叫做执行栈里面排队。
当浏览器首次载入你的脚本,它将默认进入全局执行上下文。当在全局代码中调用一个函数,程序将暂停当前全局代码而进入调用的这个函数,并创建一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。
如果在此函数中又有其他调用的函数执行,则相同的事情会再次上演。程序的进去执行新的内部函数,创建一个新的执行上下文并把它压入执行栈的顶部。浏览器总会执行位于栈顶的执行上下文,一旦当前上下文函数执行结束,它将从栈顶移出,并将上下文控制权交给其他的栈。这样,堆栈中的上下文就会依次执行并且弹出堆栈,直到回到全局的上下文。
VO (变量对象) / AO (活动对象)
按照字面解释,AO 其实就是被激活的 VO
变量对象 (Variable object) 是说 JS 的执行上下文中都有个对象用来存放执行上下文中可被访问但不能被
delete
的函数标示符、形参、变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字,对象属性的值对应它们的值。但这个对象是 规范上或者说是引擎实现上不可在 JS 环境中访问到活动对象。
激活对象 (Activation object) 有了变量对象存放每个上下文中的东西,但是它什么时候能被访问到呢?就是没进入一个执行上下文时,这个执行上下文中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就可以被访问到了。
EC
建立的细节
EC
建立的细节创建阶段【当函数被调用,但未执行任何其内部的代码之前】
创建作用域链 (Scope Chain)
创建变量,函数和参数
求”
this
“的值
执行阶段
初始化变量和值得引用,解释/执行代码
我们可以将每个执行上下文抽象为一个对象,这个对象具有三个属性
解释器执行代码的伪逻辑
查找调用函数的代码。
执行代码之前,先进入创建上下文阶段
初始化作用域链
创建变量对象:
创建 arguments 对象,检查上下文,初始化参数名称和值并创建引用的复制
扫描上下文中的函数声明(而非函数表达式)
给发现的每一个函数,在变量对象上创建一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用。
如果函数的名字已经存在,引用指针将被重写。(后声明的函数覆盖之前声明的函数)
扫描上下文的变量声明:
给发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为 undefined
如果变量的名字已经在变量对象里存在,将不会进行任何操作并继续扫描。
求出上下文内部”this”的值
激活/代码执行阶段:
在当前上下文运行/解释函数代码,并随着代码一行行执行指派变量的值
VO 对应的就是上述第二个阶段
当我们调用foo(2)
时,整个创建阶段是这样的
在上下文创建阶段,VO 的初始化过程如下(函数的形参 ==> 函数声明 ==> 变量声明): - 函数的形参 (当进入函数执行上下文时) ———— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值,对于没有传递的参数,其值为 undefined - 函数声明 (FunctionDeclaration FD) ———— 变量对象的一个属性,其属性名和值都是函数对象创建出来的,如果变量对象已经包含了相同名字的属性,则替换它的值。 - 变量声明 (VariableDeclaration var) ———— 变量对象的一个属性,其属性名即为变量名,其值为 undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
如何理解函数声明过程中如果变量对象已经包含了相同名字的属性,则替换它的值
这句话?
根据上面的介绍,我们知道 VO 创建过程中,函数形参的优先级是高于函数的声明的,结果是函数体内部声明的function a(){}
覆盖了函数形参a
的声明,因此最后输出a
是一个function
如何理解变量声明过程中如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性
这句话?
函数声明比变量优先级要高,并且定义过程不会被变量覆盖,除非是赋值
AO 对应第三个阶段 创建的过程仅负责处理定义属性的名字,而并不为它们指派具体的值,当然还有对形参/实参的处理。一单创建阶段完成,执行流进入函数并激活/代码执行阶段。
提升 (Hoisting)
1.为什么我们在 foo 声明之前能访问它? 在VO
的创建阶段,函数在该阶段就已经被创建在变量对象中,所以在函数开始执行之前,foo 已经被定义了。
2.Foo 被声明了两次,为什么 foo 显示函数,而不是 undefined 或字符串呢? 在创建阶段,函数声明优先于变量被创建。而在变量创建中,发现VO
已经存在形同名称的属性,则不会影响已经存在的属性。 因此,对foo()
函数的引用首先被创建在活动对象,当我们执行到var foo
时,发现foo
属性名已经存在,所以代码什么都不做并继续执行。
3.为什么 bar 的值是 undefined? bar
采用的是函数表达式的方式来定义的,所以bar
实际上是一个变量,变量的值是一个函数,在变量创建阶段他们被初始化为undefined
。这也是函数表达式不会被提升的原因。
总结
EC
分为两个阶段,创建执行上下文和执行代码阶段。每个
EC
可以抽象为一个对象,这个对象具有三个属性,分别是:作用域链Scope
,VO|AO
(AO
,VO
只能有一个)以及this
。函数
EC
中的AO
在进入函数EC
时,确定了 Arguments 对象的属性;在执行函数EC
时,其他变量属性具有一体化。EC
创建的过程是有先后顺序的: 参数声明 => 函数声明 => 变量声明。
最后更新于