JavaScript深入理解之变量对象

JavaScript深入理解之变量对象

写在前面

在文章《JavaScript深入理解之执行上下文》中我们谈到了执行上下文一共有三个属性:变量对象、作用域链、this指针。本篇文章我们将介绍执行上下文第一个重要的属性 —— 变量对象,希望大家建立在对执行上下文概念有一定了解的基础上阅读。

定义

变量对象(Variable object,VO)是与执行上下文相关的数据作用域,存储了在执行上下文中定义的所有变量和函数声明,保证代码执行时对变量和函数的正确访问。

简单的来它说存储着执行上下文中的以下内容:

  1. 函数的所有形参(如果是函数执行上下文)
    • 由名称和对应值组成,作为变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明
    • 由名称和对应值(函数对象(function-object),指向对函数的引用)组成,作为变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明
    • 由名称和对应值(undefined)组成,作为变量对象的属性被创建
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

我们用一个例子来理解一下:

1
2
3
4
5
6
7
8
9
10
function foo(a) {
var b = 2;
function c() {}
var d = function() {};

b = 3;

}

foo(1);

foo(1) 函数执行上下文对应的 VO (其实是AO,后边会介绍到)为:

1
2
3
4
5
6
7
8
9
10
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}

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

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

不同执行上下文中的变量对象是不同的,根据可执行代码的不同我们可以将执行上下文分为两种,一种是全局执行上下文,一种是函数执行上下文。因此我们也可以将变量对象分为对应的两种,一种是全局上下文变量对象,一种是函数上下文变量对象。那么它们和变量对象是什么关系呢?可以这样理解:

1
2
3
4
5
6
7
抽象变量对象VO (变量初始化过程的一般行为)

╠══> 全局上下文变量对象GlobalContextVO,等同于全局对象
║ (VO === this === global)

╚══> 函数上下文变量对象FunctionContextVO,等同于活动对象(Variable object,VO)
(VO === AO, 并且添加了<arguments>和<formal parameters>)

变量对象其实只是一个抽象的基本事物,它规定了一些基本的操作(如变量初始化)和行为,不同执行上下文中的不同实现都是基于这些行为来创建的。其实不必太过纠结于这个概念,我们只需要记住在全局执行上下文中我们通过全局对象来代表全局上下文变量对象,在函数上下文中我们通过活动对象来代表函数上下文变量对象。下面我们来分别介绍它们。

全局上下文中的变量对象

相信大家都已经猜到了,全局上下文中的变量对象就是全局对象,因为它们拥有的特征太像了。

我们先来看一下全局对象的定义:

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。全局对象不是任何对象的属性,所以它没有名称。

在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。但通常不必用这种方式引用全局对象,因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

我们再来看一下全局上下文中的变量对象的特点:

  • 全局上下文中的变量对象在执行所有代码之前创建。
  • 全局上下文中的变量对象一直存在,直到程序结束。
  • 全局上下文中的变量对象保存了全局上下文中所有的变量和函数声明
  • 全局变量对象位于作用域链的顶端

其实对照起来一看,我们就会明白为什么会说全局上下文中的变量对象就是全局对象了。但全局对象除了含有变量对象的特点外,它还做了其他事,那就是在初始化时会将 Math 、 String 、Date、parseInt 等作为自身属性,这也是为什么我们可以访问使用这些函数和属性的原因。

讲了这么多,其实我们只要记住全局上下文中的变量对象就是全局对象就行了。

函数上下文中的变量对象

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object ,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象,它包括如下属性:

  • callee — 指向当前函数的引用
  • length — 真正传递的参数个数
  • properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。

注意,arguments 代表的是真正传入函数的参数列表(不受参数个数限制),和函数参数是分开的。

变量对象的两个阶段

我们知道一段可执行代码分为解析和执行两个阶段,变量对象的创建和赋值分别对应了这两个阶段。

代码解析阶段

在代码解析阶段,会根据执行上下文中的变量声明和函数声明来创建变量对象,具体创建方式已经在定义部分讲过了。注意此时除了函数上下文可以根据传入的参数对 arguments 属性赋值以外,其他声明的属性的值都为 undefined ,即未定义。

代码执行阶段

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值。

定义部分的例子,代码执行完,这时候的 AO 是:

1
2
3
4
5
6
7
8
9
10
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d"
}

参考文章

《JavaScript 论代码执行上下文》
《深入理解JavaScript系列(12):变量对象(Variable Object)》
《JavaScript深入之变量对象》

写在最后

变量对象这一个属性算是基本总结完了,后面还会有执行上下文另外两个属性的总结。其实在写文章前我认为我已经弄懂变量对象了,但真正总结的时候才发现,存在疑惑的地方还有很多,写的很纠结……不过很多东西还是慢慢来吧。

本篇文章纯属于个人的学习总结,如果文章中出现错误或不严谨的地方,希望大家能够指出,谢谢!

坚持原创技术分享,您的支持将鼓励我继续创作!