定义提升(2)

在上一篇文章中,对变量声明和函数声明的定义提升进行了一个简要的说明,这里对其后面的运行机制再进行深一步的讨论。

        其实定义提升是执行上下文代码的过程的一个很自然的结果。

执行上下文(Execute Context)

每次当控制器转到ECMAScript可执行代码的时候,就会进入到一个执行上下文(EC)。执行上下文按类型可以分为全局执行上下文和函数执行上下文。执行上下文在逻辑上组成一个堆栈。堆栈底部永远都是全局执行上下文(global execute context),而顶部就是当前(活动的)执行上下文。堆栈在EC进入和退出的时候被修改(进栈或出栈)。此外,每一次函数调用(也包括递归调用和使用Function构造函数)或者使用eval()方法时,都会创建一个新的EC,并入栈。


变量对象(Variable Object)

变量对象(缩写为VO)是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:
    变量 (var, 变量声明);
    函数声明 (不包括函数表达式);
    函数的形参。
我们可以将执行上下文中声明的这三类东西当作VO的属性,在全局执行上下文中,VO就是全局对象window自身,在函数执行上下文中,VO由活动对象(AO)代替。VO和AO区别不大,只是前者不能直接访问后者可以直接访问,可以当成一个东西(据说这是ECMA3里的老词,ECMA5没有提及了)。至此,EC的结构大致如下:

EC={
VO/AO:{
    变量声明;
    函数声明;
    函数参数;
}
...
}

上下文代码执行

上下文代码的执行分为两个阶段

1、进入执行上下文

在这个阶段会初始化VO/AO对象。例如:

function test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
}
 
test(10); // call

当调用test(10)时,进入test的函数执行上下文,此时AO对象初始化如下:

AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: <指向函数"d">
  e: undefined
};

2、执行代码

在这个阶段真正执行代码,并更新VO/AO的相关属性。例如运行到var c = 10时,c被更新为10,运行到var e = function _e(){}时,e指向了函数_e。

至此,我们应该明白了为什么会出现定义提升了。原来在执行代码前,也就是进入执行上下文时,就将声明的变量、函数和形参添加给了EC的VO/AO对象。这样在正式执行代码时,这些变量都是存在的并可以访问的。


从上面的论述来看,当执行代码时,遇到一个变量,可以在当前EC的VO/AO中进行查找。事实上确实是如此,只是有一点不同。EC有一个专门用于标识符解析的属性,叫作用域(scope)。于是EC的结构又变为如下形式:

EC={
VO/AO:{
    变量声明;
    函数声明;
    函数参数;
}
scope:VO/AO+[[Scope]]
}

其中VO/AO就是当前EC的变量对象,[[Scope]]是所有父VO/AO的层级链,[[Scope]]是被调用函数的一个内部属性,通过它就可以进入到上层的VO/AO中。scope类似于一个数组,当前EC的变量对象总是被添加到[[Scope]]的最前面,这样scope就像一个链,被称为作用域链。如下面所示:

scope=[//作用域链
   当前变量对象,
   父变量对象层级链
]


作用域链专门用于标识符解析,标示符解析是一个处理过程,用来确定一个变量(或函数声明)属于哪个变量对象。标识符解析过程包含与变量名对应属性的查找,即作用域链中变量对象的连续查找,从最深的上下文开始,绕过作用域链直到最上层。这样一来,在向上查找中,一个上下文中的局部变量较之于父作用域的变量拥有较高的优先级。万一两个变量有相同的名称但来自不同的作用域,那么最深作用域起作用。

标识符解析的过程又与函数生命周期有关。

函数声明周期

函数生命周期分为声明和调用两个阶段。

声明阶段

在函数的声明阶段,函数声明会写入到执行环境的VO/AO中,而[[Scope]]是所有父级VO/AO的层级链,在函数声明阶段就存在,而且是静态存储,不会再改变。这里的父子关系由函数声明时的嵌套关系确定,而不是调用时确定。因为此时还没调用函数。

调用阶段

函数调用时,进入到函数执行上下文中,此时VO/AO创建并初始化,并被添加到[[scope]]的最前面,作用域链也随之确定。

下面用两个例子进行说明:

例子1:
var x = 10;
 
function foo() {
  alert(x);
}
 
function test() {
  var x = 20;
  foo(); // 10
  alert(x);//20
}
test();

分析:

调用test()时,进入到test函数的执行上下文,EC(test)={

AO:{

x: undefined

},

scope:AO+[[Scope]]([[Scope]]=全局变量对象(Global VO))

}

调用foo()时,进入到foo函数的执行上下文,EC(foo)={

AO:{},

scope:AO+[[Scope]]([[Scope]]=全局变量对象(Global VO))

}

foo弹框时需要查找x时,在foo的AO中找不到,于是去[[Scope]]中查找,x=10,所以弹出10。foo返回后,EC出栈,进入到test的EC中,弹出20。

例子2:
var x = 10;
function test() {
  var x = 20;
  foo(); //20
function foo() {
  alert(x);
}
  alert(x);//20
}
test();

分析:

调用test()时,进入到test函数的执行上下文,EC(test)={

AO:{

x: undefined,

foo:指向函数'foo'

},

scope:AO+[[Scope]]([[Scope]]=全局变量对象(Global VO))

}

调用foo()时,进入到foo函数的执行上下文,EC(foo)={

AO:{},

scope:AO+[[Scope]]([[Scope]]=EC(test).VO)

}

foo弹框时需要查找x时,在foo的AO中找不到,于是去[[Scope]]中查找,x=20,所以弹出10。foo返回后,EC出栈,进入到test的EC中,弹出20。


总结

JavaScript只有全局作用域和函数作用域,没有块级作用域。搞懂执行上下文、变量对象、函数周期、作用域和作用域链是很重要的,理解了这些,定义提升就很简单了,甚至闭包都不在话下。

参考文章:

1、执行上下文

2、变量对象

3、作用域链

4、js作用域

5、作用域与作用域链详解

6、深入理解JS中的变量作用域

7、JavaScript作用域链

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值