前言

本文接着前文所讲的执行上下文的概念,变量是和执行上下文息息相关的。本文还补充了上文没有讲完的执行上下文的一些内容,因为这些内容跟变量对象是有关的,所以放在这篇文章更容易理解。

请注意:本系列文章基于ES3标准展开,有些内容在ES5已经被修改了

变量对象(variable object)的定义

既然说执行上下文与变量是有关系的,那么执行上下文应该要知道变量存放在哪里和怎样获取变量。这种机制就称为变量对象(variable object),简称VO

A variable object (in abbreviated form — VO) is a special object related with an execution context and which stores:

  • variables (var, VariableDeclaration);
  • function declarations (FunctionDeclaration, in abbreviated form FD);
  • and function formal parameters declared in the context.

翻译一下:一个变量对象是一个与某个执行上下文相关的特殊对象,它存储着:变量、函数声明和函数的形参。变量对象在执行上下文中声明。

变量对象甚至可以用一个普通的对象来表示:

VO = {}
复制代码

实际上,变量对象是执行上下文的一个属性,所以我们还可以这样表示:

executionContext = {
	VO: {
		// 变量、函数声明、函数形参
	}
}
复制代码

只有全局上下文允许通过VO的属性名称间接访问VO,其他上下文是不能直接访问到VO的,VO是引擎内部的机制。至于全局上下文为什么能访问到,下面会讲。

var a = 10;
 
function test(x) {
  var b = 20;
};
 
test(30);
复制代码

这个例子对应的VO可以这么表示:

// 全局执行上下文的VO
VO(globalContext) = {
  a: 10,
  test: <reference to function>
};
  
// "test" 函数上下文的VO
VO(test functionContext) = {
  x: 30,
  b: 20
};
复制代码

实际上,在技术实现的层面上,VO是一个抽象的概念,在不同的执行上下文中,VO可能有不同的结构和名称,所以上面以及下面的例子中, 都只是为了方便理解,实际上可能不是这么回事。

变量对象在不同的执行上下文中的表现

在不同的执行上下文中,VO可能有不同的结构和名称,但在所有类型的执行上下文中,VO有一些操作和行为是相同的,比如变量的声明。从这个角度来看,将VO看成一个抽象的基本事物更容易理解, 函数执行上下文也可以通过VO定义一些相关的额外细节。

全局执行上下文中的变量对象

这里又引入了一个全局对象的概念,先来看看全局对象(Global object)的定义

Global object is the object which is created before entering any execution context; this object exists in the single copy, its properties are accessible from any place of the program, the life cycle of the global object ends with program end.

大概就是说:全局对象是一个在任何执行上下文还没推入执行栈之前就已经被创建的对象,这个对象只存在一份,在程序的任何地方都可以访问到该对象的属性。 它的生命周期随着程序的结束而结束。

在全局对象创建的过程中,像Math、String、Date这些属性也会被初始化,甚至可以将全局对象的引用作为它自身的属性,例如在BOM中,全局对象的window属性指向全局对象, 所以可以理解为以下:

global = {
  Math: <...>,
  String: <...>
  ...
  ...
  window: global
};
复制代码

全局对象是不能通过名字直接访问的,所以当引用全局对象的属性的时候,一般都会将前缀省略掉。但可以通过全局执行上下文中的this去访问它, 同样也可以通过引用自身的属性去访问,比如BOM中的window属性,就是一个引用了全局对象自身的属性。

String(10); // 应该是global.String(10),前缀被省略了
 
// with prefixes
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;
复制代码

现在要告诉你的是,全局执行上下文的VO就是全局对象本身。也就是VO(globalContext) === global, 基于这个事实,对于在全局执行上下文中声明一个变量时,我们才可以通过全局对象的属性间接访问到这个变量,(例如我们事先不知道变量的名字)

var a = new String('test');
 
alert(a); // directly, is found in VO(globalContext): "test"
 
alert(window['a']); // indirectly via global === VO(globalContext): "test"
alert(a === this.a); // true
  
var aKey = 'a';
alert(window[aKey]); // indirectly, with dynamic property name: "test"
复制代码

上面的例子只是想证明一件事,就是为了能够访问到全局执行上下文的VO(一般的VO是不能直接访问的),就通过全局对象这个概念去访问VO, 而一般又不可以直接访问全局对象,只能通过全局对象的属性去访问,而全局对象的属性允许一个引用自身的值,这个值就是我们熟悉的window对象, 或者node实现的global对象,这个由厂商实现。

至于为什么要大费周章访问全局执行上下文的VO,还记得上面说过吗,VO存放着变量、函数声明、函数参数这些东西,如果访问不到VO,那就访问不到所有的变量、函数了, 所以才会暴露一个全局对象的概念去让我们访问到这些变量和函数。那么函数执行上下文又怎么访问VO,接着往下看。

函数执行上下文中的变量对象

函数执行上下文中的变量对象值不能直接访问到的,要用活动对象(activation object)去扮演VO的角色,简称AO。

VO(functionContext) === AO
复制代码

活动对象是在进入函数执行上下文的时候被创建的,它通过函数的arguments属性被初始化,arguments属性的值是Arguments Object, 其实这个就是我们在函数里面常用的arguments属性,它的值是一个类数组对象,就叫Arguments Object。

AO = {
  arguments: <Arguments Object>
};
复制代码

arguments对象是AO的一个属性,它包括了以下的属性:

  • callee:指向当前函数的引用;
  • length:真正传递的参数的个数
  • properties-indexes:也就是参数的索引
function foo(x, y, z) {
  
  // quantity of defined function arguments (x, y, z)
  alert(foo.length); // 3
 
  // quantity of really passed arguments (only x, y)
  alert(arguments.length); // 2
 
  // reference of a function to itself
  alert(arguments.callee === foo); // true
  
  // parameters sharing
 
  alert(x === arguments[0]); // true
  alert(x); // 10
  
  arguments[0] = 20;
  alert(x); // 20
  
  x = 30;
  alert(arguments[0]); // 30
  
  // however, for not passed argument z,
  // related index-property of the arguments
  // object is not shared
  
  z = 40;
  alert(arguments[2]); // undefined
  
  arguments[2] = 50;
  alert(z); // 40
  
}
  
foo(10, 20);
复制代码

但值得注意的是,Arguments Object中对变量的引用只能是实际上传进来的参数,那些没有传进来的参数是不能通过Arguments Object访问的,这个相信大家很容易理解。

分阶段处理执行上下文中的代码

变量对象已经介绍得差不多了,接下来就是核心的地方。处理执行上下文中的代码被分为了两个基本的阶段:

  1. 进入执行上下文
  2. 执行代码

变量对象的改变与这两个阶段密切相关,而且两个阶段的处理是一种普遍的行为,不会因为执行上下文的种类不同而改变。

进入执行上下文阶段

在进入执行上下文(在执行代码之前),VO会被以下的属性填满,而且,是按照这个顺序去填充的。

  • 函数的每一个形参(如果是在一个函数执行上下文中)
  • 作为VO的一个属性,由形式参数的名称和值组成,如果没有传入实际参数,就由形式参数的名称和undefined组成
  • 所有的函数声明(FunctionDeclaration, FD)
  • 作为VO的一个属性,由一个函数的名称和值组成,如果存在相同的名称,会完全替换前面按个属性。
  • 所有的变量声明(var, VariableDeclaration)
  • 作为VO的一个属性,变量名称和undefined值组成,如果变量名称和已经声明的形式参数或者函数名冲突,则变量声明不起作用
function test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
}
  
test(10); 
复制代码

调用test(10);,可以理解为:进入“test”函数的执行上下文(传递参数10),AO(也是VO,因为AO===VO,通过AO间接访问VO)如下:

AO(test) = {
	a: undefined,
	b: undefined,
	c: undefined,
	d: <FunctionDeclaration "d">,
	e: undefined
}
复制代码

发现了没有,AO不包括函数x,因为x不是一个函数声明,而是一个函数表达式(FunctionExpression,简称FE),FE不会影响VO, 然而,_e也是一个FE,但它被赋给了变量e,可以通过e去访问。

至此,进入执行上下文阶段结束了。

执行代码阶段

到了执行代码阶段阶段,AO/VO已经被属性填满了,但大部分都是系统默认的初始值undefined,没有得到真正的值。

还是前面的那个例子,在这个阶段,AO/VO会被修改为:

AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;
复制代码

可以看到,c=10语句被执行了,所以c变成了10,而_e被保存到了e上,但x不存在于AO/VO中,调用x会报错。

alert(x); // function
 
var x = 10;
alert(x); // 10
 
x = 20;
 
function x() {}
 
alert(x); // 20
复制代码

再看一个例子,为什么第一个alert(x)语句表示x是一个函数,而且是在函数声明之前、变量声明之前调用的? 根据规范(上面有写),在第一个阶段,VO被填充的时候,是按照函数参数、函数声明、变量声明的顺序填充的,变量声明不会干扰VO中已经存在的同名函数声明或形式参数声明。 所以在进入全局执行上下文的阶段,VO的结构如下:

// 先去找函数声明
VO = {
	x: <FunctionDeclaration "x">
};
// 找到var x = 10
// 命名冲突,但不会起作用
// VO不变
VO = {
	x: <FunctionDeclaration "x">
};

复制代码

随后在执行代码阶段,VO会做以下的修改:

VO['x'] = 10;
VO['x'] = 20;
复制代码

这个时候,第二第三个alert(x)语句会打印10、20,而调用x函数,会发现提示x不是一个函数,因为VO已经被修改了。

if (true) {
  var a = 1;
} else {
  var b = 2;
}
 
alert(a); // 1
alert(b); // undefined, but not "b is not defined"
复制代码

在上面这个例子中,else语句永远不会被执行,但是b还是会存在于VO中,只是他的值是undefined,这就是为什么JS不能写出这样的语句:

if (true) {
  function a()  {}
} else {
  function b() {}
}
复制代码

按照规范,ab都会被声明,但是由于厂商对规范的实现细节不一样,可能出现的结果也不一样,可能不会出现你预期的效果,所以不提倡这样的写法,应该用变量保存函数表达式的引用,这样不会出奇奇怪怪的问题。

关于变量

相信很多人都看过这么一个说法:可以在任何地方通过var或者不带var声明一个全局变量,其他这个说法是错误的,当你学习了VO和执行上下文的概念之后, 你应该要知道,只能通过var关键字声明一个变量

a=10;
复制代码

像上面这个例子,a不是一个新的变量,而是在全局对象上创建了一个新的属性,不是变量不是指它不能被改变,而是说它不符合规范中对于变量的定义。

alert(a); // undefined
alert(b); // "b" is not defined
 
b = 10;
var a = 20;
复制代码

通过上面的例子可以看到,提示b没有定义,因为b根本就不是一个变量,分析一下,在进入执行上下文的阶段的VO:

VO = {
  a: undefined
};
复制代码

就可以看到,这个阶段中并没有b,因为b不是一个变量,b只有在执行阶段才会被加入到全局变量中。但这个例子中,还没执行b = 10;就引用了b,所以报错了。 改变一下代码:

alert(a); // undefined, we know why
 
b = 10;
alert(b); // 10, created at code execution
 
var a = 20;
alert(a); // 20, modified at code execution
复制代码

关于变量还有一个重要的点,就是变量有一个特性(attribute):{DontDelete},这个特性的含义就是不同通过delete操作符直接删除变量属性。 在ES5中,就是[[Configurable]]属性,但在eval中,没有这个属性,还是那句话,尽量不要使用eval。

总结

  1. 变量对象是执行上下文的一个重要属性,简称VO,每个执行上下文哦独有一个VO
  2. 全局执行上下文的VO等于全局对象,函数执行上下文的VO等于活动对象(AO)
  3. VO不能直接访问,全局执行上下文通过全局对象访问,函数执行上下文通过AO访问
  4. VO初始化分为三个部分的属性,而且是按顺序的:函数参数、函数声明、变量声明
  5. 处理执行上下文中的代码分为两个阶段:创建阶段、执行阶段

下一篇文章讲应该是讲this了,是前面和这篇文章内容的深化,这些内容一定要连着看才会理解,继续加油。

参考

dmitrysoshnikov.com/ecmascript/… www.cnblogs.com/justinw/arc…