深入理解上下文

概念
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(){}' //不是参数优先级比函数声明优先级高吗?看来这两个都是一个特例

你可能感兴趣的:(JavaScript深入浅出)