浅谈AO/VO详解
首先说明一下AO和VO的含义
- AO:Activive Object,即函数的活动对象。
- VO:Variable Object,即变量对象。
它们的作用是帮助js引擎在引用变量的时候能够去顺利找到变量。并且它们之间的联系可以实现作用域链。
VO
在执行函数的时候,会经历执行上下文的创建和代码的执行。如下图。
我们会先进行上下文的创建,创建VO、通过[Scope]
属性指向外层的VO
来进行指向外层的AO对象,那么这样就形成了作用域链。接下来是this的指向问题。
在创建VO对象的时候,会先把所有变量的声明放到一个对象属性上,但是他们的属性为空,所以这就是变量的提升。
所以简单来说。VO的作存储用就是存储变量,然后本代码或者子代码在执行的时候能够知道变量的值。并且变量定义的顺序如下:
- 参数
- 变量
- 函数
AO
AO和VO的关系:
AO可以理解为VO的一个实例,也就是VO是一个构造函数,然后VO(Context) === AO,所以VO提供的是一个函数中所有变量数据的模板。
对于同一个函数分多次执行,那么里面的变量、形参和定义的函数肯定是不同的函数,所以每次执行都会产生一个AO对象,即VO是AO的一个实例,但是这个实例并不是new 出来的,而是在同一段执行代码执行的时候放进来的。
- VO是不能访问的(除了全局上下文的VO可以间接访问),但是可以访问AO的成员(属性)。
- VO和AO其实是一个东西,只是处于不同的执行上下文生命周期。AO存在于执行上下文位于执行上下文堆栈顶部(就是上边说的’当控制进入函数代码的执行上下文时’)的时期。再粗暴点,就是函数调用时,VO被激活成了AO。
- AO通过函数的arguments属性初始化,其值是一个ArgO,包括 callee、length、arg属性。其中arg属性就相当于下标,比如第一个参数对应arg = 0。
以一个例子来说明
var a = 1;
function A() {
function B() {
var b;
}
B();
}
A();
在执行上面代码的时候,首先会进行全局初始化,会执行以下操作
-
初始化环境执行栈
-
初始化全局vo和全局ao,由于全局对象只有一个,所以vo和ao一致。
-
当执行A函数的时候,首先会创建环境变量对象,也就是A函数的VO对象,然后初始化作用域链等
{ A: { [scoped]: VO(G), // 即AO B: <Func> }, G: { A: <Func> window: G, Math: <any> } }
-
外边是一个栈的形式,如果A函数中找不到变量的话,会沿着
scoped
找到Global的AO对象,然后进行查询所需要的变量。 -
要注意到作用域链是词法作用域,跟执行调用关系无关,所以并不是随着上面的栈来进行的,而是通过词法关系来产生的。
-
接下来就是执行B函数
{ B: { [scoped]: VO(A), // 即AO b: undefined }, A: { [scoped]: VO(G), B: <Func> }, G: { A: <Func> window: G, Math: <any> } }
-
为什么
scoped
指向的是AO而不是VO呢?上面我们知道VO只是一个模板,由AO实例化,里面B函数执行两遍,本身产生的AO可能不同,但是指向scoped是同一个,也就是上一次执行AO的情况。更形象的在下面
function A() { let count = 0; return function() { console.log(count++); } }
上面个的例子如果把A函数执行两遍,里面的count不会共享,也就是AO对象不同,所以AO就是VO的实例。