一、variable object
variable object以下简称为VO,VO是执行上下文相关的范围内的值。VO存储了在上下文内已经定义的变量和函数定义(function declarations)。注意函数的表达(function expressions)并不在VO中。VO是一个抽象的概念,在不同类型的上下文中,表现为使用不同的对象。例如:在全局上下文中VO是全局对象自身(global object itself)(这就是为什么我们能够通过全局对象的属性名字直接访问全局属性)
考虑下下面这个全局执行上下文的例子:
var foo = 10;
function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE
console.log(
this.foo == foo, // true
window.bar == bar // true
);
console.log(baz); // ReferenceError, "baz" is not defined
全局上下文的VO拥有下面的属性:
我们再次看到像brz这种函数表达式不会包含在VO中的,这就是为什么当我们尝试在外面访问函数baz会有ReferenceError 。
注意,ECMA相对于其他语言(C、C++),只有函数创建一个新的范围。函数范围内定义的变量和内部函数在外部是不可见的,所以不会污染全局变量对象(global variable object)。
使用eval我们也会进入一个新的执行上下文,但是eval使用的是全局变量对象或者是调用者的VO。
函数和函数的VO是什么?在函数的上下文中,VO会被表示为AO(activation object)。
二、activation object
当调用者调用一个函数时,AO(activation object)会被创建。AO里面会有形参和arguments 对象(arguments object是一个形参的map,里面存放属性的下标和值)。接下来,在函数的上下文中AO被当成一个VO来使用(也就是说函数的VO和普通的VO一致,只不过除了变量和函数定义之外,他还存储了形参以及arguments 对象)
function foo(x, y) {
var z = 30;
function bar() {} // FD
(function baz() {}); // FE
}
foo(10, 20);
上面的那段代码在函数上下文中VO为:
同样函数表达baz不被包含在AO/VO中。
接下来,我们进入下一部分。大家都知道,在ECMAScript中我们可以使用内部函数,在内部函数中我们可以访问父函数或者全局上下文中的变量。就像我们把VO作为上下文中的Scope Object,和原型链相类似的还有一个范围链(scope chain)。
三、Scope chain
A scope chain 出现在代码上下文中的标示符所组成的对象列表(a list of objects that are searched for identifiers appear in the code of the context.)。
规则和原型链基本类似:如果一个变量在当前范围(VO/AO)内未找到,则继续搜索父亲的VO,直到顶端。
考虑到上下文,标示符(identifiers)是指:变量的名字、函数的定义、形参等等。当一个函数引用到代码里面的一个标示符,而这个标示符不是local 变量(或者local 函数、形参),这种变量被称之为free variable。为了寻找到这些free variables,scope chain就会被使用到。
一般情况下:scope chain是一个所有父亲VO以及函数自身的VO/AO对象列表。当然,scope chain还包含一些其他的,例如在代码执行中动态添加的内容(with-objects 、catch-clauses)
当查找一个标示符,scope chain从AO开始,一直查找到最顶端。
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x" and "y" are "free variables"
// and are found in the next (after
// bar's activation object) object
// of the bar's scope chain
console.log(x + y + z);
})();
})();
我们假设通过隐式属性__parent__ 来访问scope chain中的下一个对象(在ES5中为outer link)。通过下图来表示出上面代码(父亲的VO被保存在函数的[[Scope]] 属性中)。
在代码的执行过程中,scope chain会随着with语句和catch子句对象而增加。既然这些对象也是简单的对象,他们也有原型链。这就导致了scope会查找2个分支:(1)scope chain被搜索 然后(2)从每一个scope chain的link进入link的原型链(on every scope chain’s link — into the depth of the link’s prototype chain )。
Object.prototype.x = 10;
var w = 20;
var y = 30;
// in SpiderMonkey global object
// i.e. variable object of the global
// context inherits from "Object.prototype",
// so we may refer "not defined global
// variable x", which is found in
// the prototype chain
console.log(x); // 10
(function foo() {
// "foo" local variables
var w = 40;
var x = 100;
// "x" is found in the
// "Object.prototype", because
// {z: 50} inherits from it
with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
}
// after "with" object is removed
// from the scope chain, "x" is
// again found in the AO of "foo" context;
// variable "w" is also local
console.log(x, w); // 100, 40
// and that's how we may refer
// shadowed global "w" variable in
// the browser host environment
console.log(window.w); // 20
})();
我们会得到下面的结构(在使用__parent__link之前,首先会考虑__proto__ chain)
注意不是所有全局对象的实现都会从Object.prototype继承。上图的例子可以用SpiderMonkey测试。
在所有的父亲对象都存在的情况下,从内部函数得到外部的数据并没有什么特别,我们只需要查询scope chain即可。可是,就像我们上面提到的,当上下文结束之后,所有的状态和自身都会被销毁。同时内部函数可能会从外部函数中被返回。再者,这个返回的函数在之后可能会在另一个上下文中被激活。但是这时候那些free variable已经消失不见了,由此引出另一个问题闭包,闭包直接就涉及到scope chain这个概念。