四、JavaScript之变量对象

在前面的文章《JavaScript之执行上下文栈》中讲到,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

执行上下文的组成代码示例:

1
2
3
4
5
const ExecutionContextObj = {
VO: window, // 变量对象
Scope: {}, // 作用域链
this: window
};

本文主要记录创建变量对象的过程。

变量对象

变量对象是与执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明。
变量对象式一个抽象的概念,在不同的上下文中,表示不同的对象:

全局上下文

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

  • 全局执行上下文中,变量对象就是全局对象。
  • 在顶层js代码中,this指向全局对象,全局变量会作为该对象的属性来被查询。在浏览器中,window就是全局对象。

这样我们就能理解为什么,在全局上下文中声明一个变量时,我们能够通过全局对象的一个​​属性来间接引用它(例如,当变量的名称事先不知道时):

1
2
3
4
5
6
7
8
9
var a = 'test';

console.log(a); // "test"

console.log(window['a']); // "test"
console.log(a === this.a); // true

var aKey = 'a';
console.log(window[aKey]); // "test"

函数上下文

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

  • 函数上下文中,变量对象VO就是活动对象AO。
  • 初始化时,带有arguments属性。

Arguments 对象(简称 ArgO)是一个位于函数上下文的激活对象中的对象,包含以下属性:

callee - 链接到正在执行的函数;
length -实际传递的参数数量;
properties-indexes(数字,缩减为字符串),其中的值是函数的形参(在参数列表中从左到右)。这些索引属性的数量 == arguments.length。arguments 对象的 index 属性值和存在的形参是可以互换的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function foo(x, y, z) {

// 描述函数参数的数量(x, y, z)
console.log(foo.length); // 3

// 实际传输参数的数量,只有(x, y)
console.log(arguments.length); // 2

// 函数本身的引用
console.log(arguments.callee === foo); // true

// arguments对象的index属性值和存在的形参是可以互换
console.log(x === arguments[0]); // true
console.log(x); // 10
arguments[0] = 20;
console.log(x); // 20
x = 30;
console.log(arguments[0]); // 30

// 然而,对于未指定的z参数,是无法和arguments的值进行互换的
z = 40;
console.log(arguments[2]); // undefined

arguments[2] = 50;
console.log(z); // 40
}

foo(10, 20);

执行过程

执行上下文的代码会分成两个阶段进行处理:分析(进入执行上下文)和执行(代码执行):

进入执行上下文

当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:

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

举个例子:

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

}

foo(1);

在进入执行上下文后,这时候的 AO 是:

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

请注意,AO 中不包含“x”函数。这是因为,“x”是不是一个函数声明,而是一个FunctionExpression(函数表达式)是不影响VO。然而,函数“_e”也是一个表达式函数,但是,正如我们将在下面看到的,通过将对其的引用分配给变量“e”,它可以通过“e”使用。您其他帖子中阅读 FunctionDeclaration 和 FunctionExpression 之间的区别。

代码执行

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

还是上面的例子,当代码执行完后,这时候的 AO 是:

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

再次注意 FunctionExpression_e仅以变量为代价保留在内存中e。FunctionExpressionx没有进入 AO/VO:也就是说,如果你试图在代码中x声明之前或之后调用一个函数,就会出现“x未定义”错误。未保存的 FunctionExpression 只能与声明一起调用,或递归调用。

来看一个示例:

1
2
3
4
5
6
7
8
9
10
console.log(x); // function

var x = 10;
console.log(x); // 10

x = 20;

function x() {}

console.log(x); // 20

为什么第一个输出中的“x”是一个函数,甚至在申明之前可用?为什么不是 10 或 20?因为,根据规则——VO在进入上下文时填充了函数声明,在同一个地方,进入时声明了变量“x”,但VO中的变量的优先级低于函数声明,因此,在进入时,VO填充会以如下方式发生:

1
2
3
4
5
6
7
8
VO = {};

VO['x'] = reference to function x(){}

// 发现 var x = 10;
// 由于之前已经填充了函数声明,变量‘x’不会覆盖函数的值

VO['x'] = reference to function x(){}

但是已经在执行代码时,VO被修改成这样:

1
2
VO['x'] = 10;
VO['x'] = 20;

再看一个示例:

1
2
3
4
5
6
7
8
if (true) {
var a = 1;
} else {
var b = 2;
}

console.log(a); // 1
console.log(b); // undefined, 而不是 "b is not defined"

在这个例子中,我们看到变量在进入上下文时就会进入 VO(例如,永远不会执行 else 块,但是,变量“b”仍然存在于 VO 中)

关于变量

在JavaScript中,使用var申明和不适用var申明是有区别,请记住:变量仅使用 var 关键字声明。
比如:

1
2
3
4
console.log(a); // undefined
console.log(b); // b is not defined
b = 20;
var a = 10;

这里我们看到”b is not defined”,因为 这不是一个变量,“b”只会在代码执行时创建;
关于变量的另一个重点,与简单属性不同,变量接收{DontDelete}属性,这意味着无法使用delete运算符删除:

1
2
3
4
5
6
7
8
9
a = 10;
console.log(window.a); // 10
console.log(delete a); // true
console.log(window.a); // undefined

var b = 20;
console.log(window.b); // 20
console.log(delete b); // false
console.log(window.b); // 任然是20

到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:

  • 全局上下文的变量对象初始化是全局对象

  • 函数上下文的变量对象初始化只包括 Arguments 对象

  • 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

  • 在代码执行阶段,会再次修改变量对象的属性值

  • 在进入执行上下文时,var声明的变量才会被添加并赋初始值

参考文献

http://dmitrysoshnikov.com/ecmascript/ru-chapter-2-variable-object/