什么是AO和VO
- AO:Activive Object,即函数的活动对象。
- VO:Variable Object,即变量对象。
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。
执行上下文
当执行js代码时,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
执行上下文的代码会分成两个阶段进行处理:创建和执行
- 创建阶段(当函数被调用,但是开始执行函数内部代码之前,也就是所谓的 预编译/词法分析阶段)
- 创建Scope chain
- 创建VO/AO(variables, functions and arguments)
- 设置this
- 激活/代码执行阶段
- 设置变量的值、函数的引用,然后解释/执行代码
注意,在执行代码阶段,如果存在对于 VO/AO
内不存在的变量进行赋值,则该变量会被创建到global
,并且这种创建变量方式给global
一个属性,而不会在VO
中,比如
(function(){
testProp = '123' // 这个函数执行之后,可以从window.testProp访问到
})()
// 执行上下文可以理解为一个对象,型如:
executionContext:{
//这就是变量对象VO VO = 变量 + 函数声明 + AO(其实就是函数执行时候的实参)
variable object:vars,functions,arguments,
//作用域链 = VO + 所有父级的作用域
scope chain: variableObject + all parents scopes
// 上下文对象,就是this指针,这个也是在进入上下文的时候进行一次性的确定
thisValue: contextObject
}
this指向
这里要注意的是,this永远代表方法所属的对象,this
是在进入上下文的时候进行确定的,之后不会改变,如果函数在执行过程中没有被关联到任何对象,那么 this
指向全局对象(这里和将变量创建到全局有些类似),简而言之,所有的匿名函数,this
都指向全局。
this的值 contextObject
是直接从执行上下文中获取的,不会进行任何任何作用域链查找,而是在进入上下文的时候一次性确定。
var context = "global";
var obj = {
context: "object",
method: function () {
console.log(this + ":" +this.context);
function f() {
var context = "function";
console.log(this + ":" +this.context);
};
f();
(function(){
var context = "function";
console.log(this + ":" +this.context);
})();
}
};
obj.method();
// [object Object]:object
// [object Window]:global
// [object Window]:global
变量对象(Variable Object,VO)
变量对象是与执行上下文相关的数据作用域。它是一个与上下文相关的特殊对象,一般VO中会包含以下信息:
- 变量声明 (var, Variable Declaration);
- 函数声明 (Function Declaration, FD);
- 函数的形参
注意,这里的 Declaration
是声明,也就是javascript中有两种声明方式
变量对象在函数创建阶段大致如下
- 创建
arguments
对象,检查当前环境的参数,初始化属性和属性值。 - 检查函数声明,当前环境中每发现一个函数就在 VO 中用函数名创建一个属性,以此来引用函数。如果函数名已经存在,就覆盖这个属性。
- 检查变量声明,当前环境中每发现一个变量就在 VO 中用变量名创建一个属性,并初始化其值为
undefined
。如果变量名存在,则跳过(注意这是在创建阶段,执行阶段会被赋值,也就是覆盖),继续检查。
看下面的代码执行过程的对比
console.log(hello); // [Function: hello]
function hello() { console.log('how are u') }
var hello = 10;
- 首先进入全局环境创建阶段
- 检查函数声明,将函数
hello
放入变量对象(全局环境为window
对象)。 - 检查变量声明,发现变量
hello
已经存在,所以跳过。(注意了这里是变量var hello
,如果是函数function hello(){}
则会进行覆盖) - 进入执行阶段,执行代码
console.log(hello)
时,会在全局环境的变量对象中寻找hello
,找到了函数hello
- 执行到赋值语句,
hello
被赋值为字符串变量
console.log(hello); // 打印undefined
var hello = 10;
hello = 'i am hello'
- 这里之所以打印的是
undefined
而不是'i am hello'
,就是因为在创建过程中,hello的变量名已经存在,而'I am hello'
又是个字符串而不是函数,所以是跳过而不是覆盖VO
中的hello
活动对象(Activation Object,AO)
只有全局上下文的变量对象允许通过VO
的属性名称间接访问;在函数执行上下文中,VO
是不能直接访问的,此时由激活对象(Activation Object,缩写为AO)扮演VO的角色。AO
是在进入函数上下文时刻被创建的,它通过函数的 arguments
属性初始化。
对于VO
和AO
的关系可以理解为,VO
在不同的 Execution Context
中会有不同的表现:当在Global Execution Context
中,可以直接使用VO
;但是,在函数Execution Context
中,AO
就会被创建
作用域链
var glb = 'global'
var parentFunc = function(parentNum) {
var b = 5
function childFunc(childNum) {
var a = 2
return a + b + childNum
}
let c = childFunc(4)
return b + c + parentNum
}
parentFunc(1)
-
执行流开始,代码进入
globalExecutionContext
,此时创建Global VO
。//这里代表执行上下文栈(Execution context stack) // 全局上下文 globalExecutionContext = { scopeChain: {...}, variableObject: { glb: 'global', parentFunc: undefined, ... } }
-
当代码执行到
parentFunc(1)
时,代码进入parentFuncExecutionContext
,parentFunc
维护一个私有属性[[scope]]
,用来保存scopeChain
。在parentFunc
被定义的时候,只有一个元素,在这里只有一个就是VO(global)
。当parentFunc
被执行的时候,parentFunc
函数的AO
通过arguments
进行初始化,然后形参也被加入到AO
中,同时AO(parentFunc)
被添加到scopeChain
的顶部。//这里代表执行上下文栈(Execution context stack) // 全局上下文 globalExecutionContext = { scopeChain: {...}, variableObject: { glb: 'global', parentFunc: undefined, childFunc: pointer to function childFunc, ... } } // parentFunc的执行上下文 parentFunc.[[scope]] = parentFuncExecutionContext.scopeChain // 执行后 parentFuncExecutionContext = { scopeChain: [AO(parentFunc),VO(global)], variableObject: { arguments:{ 0: 1, length: 1, callee: pointer to function parentFunc }, parentNum: 1 b: '5', c: undefined // 虽然实际无法打印出undefined,但是在VO中这个变量是存在的 } }
-
parentFunc
被执行,当执行到 childFunc 时,代码进入childFuncExecutionContext
,childFunc
维护一个私有属性[[scope]]
,用来保存scopeChain
。在childFunc
被定义的时候(这里注意parentFunc
被执行的时候childFunc
才被定义),有两个元素,在这就是VO(global)
和AO(parentFunc)
。当childFunc
被执行的时候,childFunc
函数的AO
通过arguments
进行初始化,然后形参也被加入到AO
中,同时AO(childFunc)
被添加到scopeChain
的顶部。//这里代表执行上下文栈(Execution context stack) // 全局上下文 globalExecutionContext = { scopeChain: {...}, variableObject: { glb: 'global', parentFunc: undefined, childFunc: pointer to function childFunc, ... } } // parentFunc的执行上下文 parentFunc.[[scope]] = parentFuncExecutionContext.scopeChain // 执行后 parentFuncExecutionContext = { scopeChain: [AO(parentFunc),VO(global)], variableObject: { arguments:{ 0: 1, length: 1, callee: pointer to function parentFunc }, parentNum: 1 b: '5', c: undefined, childFunc: pointer to function childFunc } } // childFunc的执行上下文 childFunc.[[scope]] = childFuncExecutionContext.scopeChain // 执行后 childFuncExecutionContext = { scopeChain: [AO(childFunc), AO(parentFunc), VO(global)], variableObject: { arguments:{ 0: 4, length: 1, callee: pointer to function childFunc }, childNum: 4 a: 2 } }
-
childFunc
执行完毕,返回值后退出执行上下文栈 -
parentFunc
执行完毕,返回值后退出执行上下文栈
创建作用域
对比下面的两个输出结果,其实就是通过函数执行阶段的AO
,通过函数包裹,在f(i)
执行时,函数的执行上下文的创建阶段(词法分析阶段),i
就已经确定了,也没有在后续的作用域内发生改变,这里的使用方法类似于闭包
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i)
}, 100 * i)
}
// 输出10, 10, 10, 10...
for (var i = 0; i < 10; i++) {
f(i)
}
function f (i) {
setTimeout(function () {
console.log(i)
}, 100)
}
// 输出1, 2, 3, 4...
关于块级作用域
JS有:全局作用域、函数作用域,对于一个var
变量,只能存在全局作用域或函数作用域中的一个。而ES6中新增了块级作用域let
和const
。
这里的块指的是代码块,块级作用域由 {}
包裹,if
语句和 for
语句里面的 {}
也属于块作用域。
for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i)
}, 100)
}
// 10,10,10,10...
由于使用 var 变量的情况下不存在块级作用域,所以对应的变量i
是创建到全局的,所以循环结束后去全局变量i=10
for(let i=0;i<10;i++){
setTimeout(function(){
console.log(i)
}, 100)
}
// 1,2,3,4,5...
使用let
后实际是创建了10个{}
代码块和10个i
值,所以最终函数执行时获取的是对应的{}
内部的i
值
作用域链和原型链的关系
作用域链(scop chain)的主要作用是用来进行变量查找,他和原型链(prototype chain)的关系相当于一个二维查找,当代码需要查找一个属性(property)或者描述符(identifier)的时候,首先会通过作用域链(scope chain)来查找相关的对象;一旦对象被找到,就会根据对象的原型链(prototype chain)来查找属性(property)
延长作用域链
在 js 中,某些语句可以在作用域链前端临时添加一个变量对象,该变量对象会在代码执行完毕后移除。具体来说当执行流进入到下列两种语句时,作用域就会得到加长:
-
try-catch
语句的catch
块在 catch 块中,错误对象 e 被添加到了其作用域链前端,这使得在 catch 块内部能够访问到错误对象。执行完后,catch 块内部的变量对象被销毁,因此在 catch 块外部就不能访问到错误对象 e 了
var test = () => { try { throw Error("出错误了"); } catch(e) { console.log(e); //Error: 出错误了 } console.log(e); //Uncaught ReferenceError: e is not defined } test();
-
with(obj)
语句将 obj 对象加入到作用域链前端。语句
with(persion)
将对象persion
作为VO
添加到了函数getName
作用域链的前端,语句var myName = name
在查找变量name
时 会首先在其作用域链前端,即person
对象中查找,查找到name
属性为snow
。又因为with
语句内的VO
是只读的,在本层定义的变量,不能存储到本层,而是存储到它的上一层作用域。这样在函数getName
的作用域内就能访问到变量myName
了var persion = { name: 'snow' }; var name = 'summer'; var getName = () => { with(persion) { var myName = name; } return myName; } console.log(getName()) // 打印 => snow
总结
- 全局环境没有 arguments 对象。
- 我们编写代码时并不能访问函数的变量对象,但解释器在处理数据使其成为活动对象时就可以使用它。
- 作用域链的搜索始终是从作用域链的前端开始,然后逐级的向后回溯,直到全局环境,不能反向搜索