深入Js作用域:变量对象

变量对象

Variable Object,简称 VO,它是一个与执行上下文相关的特殊对象。它存储着在上下文中定义的:

变量声明
函数声明 (FunctionDeclaration,FD)
函数的形参

我们可以把 VO 理解为执行上下文的一个属性(property)。当我们声明一个变量或一个函数的时候,就像我们为 VO 增加一个新属性一样。

变量对象只是一个抽象概念。(从本质上说,在具体执行上下文中,VO 名称是不一样的,并且初始结构也不一样),对于所有类型的执行上下文来说,变量对象的一些操作(如变量初始化)和行为都是共通的。

抽象变量对象 VO (变量初始化过程的一般行为)
  ║
  ╠══> 全局上下文变量对象 GlobalContextVO
  ║    (VO === this === global)
  ║
  ╠══> 函数上下文变量对象 FunctionContextVO
  ║    (VO === AO, 并且添加了 arguments 和 formal parameters)

无论有多少个函数上下文,但是全局上下文只有一个。

全局上下文VO

全局上下文的 VO 其实是我们非常熟悉的一个对象,全局对象(Global Object):

  • 在进入任何执行上下文之前就已经创建了的对象
  • 只存在一份,它的属性在程序中任何地方都可以访问,生命周期终止于程序退出那一刻

如:在客户端中 Js 中,全局对象就是 Window

注:只有全局上下文的 VO 允许通过 VO属性名称 来间接访问(因为在全局上下文里,全局对象自身就是变量对象),在其它上下文中是不能直接访问 VO 对象的,因为它只是内部机制的一个实现。

函数上下文VO/AO

只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

进入函数执行上下文之前

  • VO 中的属性是不能直接访问的。

进入函数执行上下文时

  • VO 转变成了活动对象(Activation Object,AO),里面的属性都能被访问了,也就是说 AO 是在进入到执行上下文的时候被激活
  • AO 通过函数的 arguments 属性初始化(arguments 属性的值是 Arguments 对象)。

处理上下文代码过程

执行上下文的代码被分成两个基本的阶段来处理:

1.创建阶段(进入执行上下文)
2.执行代码

变量对象的修改变化与这两个阶段紧密相关。

注:这2个阶段的处理是一般行为,和上下文的类型无关(即在全局上下文和函数上下文中的表现是一样的)。

进入执行上下文

当进入执行上下文时(此时代码还没执行),VO 会按如下顺序进行初始化:

1.函数的所有形参(如果是在函数执行上下文中)

  • 由名称和对应值组成的一个变量对象的属性被创建
  • 没有传递对应参数的,属性值为 undefined

2.函数声明(FunctionDeclaration,FD)

  • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
  • 如果变量对象已经存在相同名称的属性,则完全替换这个属性

3.变量声明(VariableDeclaration,VD)

  • 由名称和对应值(undefined)组成一个变量对象的属性被创建
  • 若变量名称跟已经声明的形式参数函数相同,则变量声明不会干扰已经存在的这类属性

例:

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

test(10); // call

进入函数 EC 时:

AO(test) = {
    arguments: {
    0: 10,
    1: undefined,
    length: 2
},
  a: 10,
  b: undefined,
  c: undefined,
  d: <reference to FunctionDeclaration "d">
  e: undefined
};

注:AO 里并不包含函数 x 。这是因为 x 是一个函数表达式(FunctionExpression, FE)而不是函数声明,函数表达式不会影响变量对象。

执行代码

在执行代码阶段,AO 已经拥有了属性(不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值 undefined)。
此时会顺序执行代码,根据代码,修改变量对象的值。接上边例子,此时 AO:

AO(test) = {
    arguments: {
      0: 10,
      1: undefined,
      length: 2
    },
    a: 10,
    b: undefined,
    c: 10,
    d: <reference to FunctionDeclaration "d">
    e: <reference to FunctionDeclaration "_e">
};

注:通常,当执行期上下文被销毁时,函数的 AO 也就被销毁了。但是当有闭包引用时,激活对象就不会被销毁,因为他仍然被引用。
别急,闭包我们会在后边的章节细说。

示例

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

结果:打印函数。

分析:之前我们是用变量提升来解释的,现在我们可以更深一层,用变量对象来解释了,由 VO 的初始化顺序得知:若变量名跟已声明的函数相同,则变量声明不会干扰它。
也就是说,初始化会先处理函数声明,其次才处理变量声明

关于变量(拓展)

例一:

a = 10;

分析:
这仅仅是给全局对象创建了一个新属性,然而它并不是变量,因为它不符合 ECMAScript 规范中的变量概念。它之所以能成为全局对象的属性,完全是因为 VO(globalContext) === global

例二:

alert(a); // undefined
alert(b); // Uncaught ReferenceError: b is not defined

b = 10;
var a = 20;

分析:
根源仍然是 VO、进入上下文阶段、代码执行阶段。
在进入上下文阶段:

VO = {
  a: undefined
};

我们可以看到,因为 b 不是一个变量,所以在这个阶段根本就没有 b,b 将在代码执行阶段才会出现(但是在我们这个例子里,还没有到那就已经出错了)。

例三:

alert(a); // undefined, 这个大家都知道

b = 10;
alert(b); // 10, 代码执行阶段创建

var a = 20;
alert(a); // 20, 代码执行阶段修改