Variable Object in JavaScript

in #variable6 years ago

[JavaScript] 变量对象 Variable Object

介绍

本文中,我们将分析与ECMAScript执行上下文相关的概念变量对象 Variable Object

先举个栗子🌰,为什么a b x表现大不相同,当引用一个函数或者变量时,解释器是如何以及从哪里找到它们的呢

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

alert(x); // function x() {}
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20

定义

然变量和执行上下文有关,那它应该知道数据存放在哪以及如何获取,这种机制称为变量对象

变量对象(缩写VO)是与执行上下文相关的特殊对象,它存储在上下文中声明的:

  • 变量(var,VariableDeclaration)
  • 函数声明(FunctionDeclaration,简写为FD)
  • 函数形式参数

示意地示例,可以用ECMAScript的对象来表示变量对象:

VO = {};

变量对象同时也是执行上下文EC的一个属性:

activeExecutionContext = {
  VO: {
    // context data (var, FD, function arguments)
  }
};

对变量的间接引用(通过VO的属性名)只允许发生在全局上下文中的变量对象上(全局对象本身就是变量对象), 对于其他的上下文,是无法直接引用VO的,因为VO是实现层机制

声明新的变量或函数的过程其实就是用变量或函数的名称和值在VO中创建新的属性的过程:

var a = 10;
function test(x) {
  var b = 20;
};
test(30);

上面👆代码对应的变量对象如下:

// Variable object of the global context
VO(globalContext) = {
  a: 10,
  test: <reference to function>
};

// Variable object of the "test" function context
VO(test functionContext) = {
  x: 30,
  b: 20
};

但是,但是,在实现层(标准中定义的),变量对象只是一个抽象概念

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

变量对象上的一些操作(比如:变量的初始化)和行为对于所有的执行上下文类型来说都一样,从这点来说,将变量对象表示成抽象的概念更加便捷

AbstractVO (generic behavior of the variable instantiation process)
 
  ║
  ╠══> GlobalContextVO
  ║        (VO === this === global)
  ║
  ╚══> FunctionContextVO
           (VO === AO, <arguments> object and <formal parameters> are added)

全局上下文中的变量对象

首先对全局对象(Global object)作个定义

全局对象是一个在进入任何执行上下文前就创建出来的对象,此对象以单例形式存在,它的属性在程序的任何地方都可以直接访问,它的生命周期随着程序的结束而终止

全局对象在创建时,Math,String,Date,parseInt等属性也会被初始化,同时,其中一些对象会指向全局对象本身,如BOM中,全局对象上的window属性就指向全局对象

global = {
  Math: <...>,
  String: <...>
  ...
  ...
  window: global
};

在引用全局对象的属性时,前缀通常可以省略,因为全局对象是不能通过名字直接访问的,然而,通过全局对象上的this值,以及通过BOM中的window对象这样递归引用的方式都可以访问到全局对象

String(10); // means global.String(10);
// with prefixes
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;

回到全局上下文的变量对象上,这里变量对象就是全局对象本身

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是不能直接访问的,它主要扮演活跃对象(activation object)(简称AO)的角色

VO(functionContext) === AO;

活动对象在进入函数上下文时创建,并通过属性arguments进行初始化,其值就是Arguments对象

AO = {
  arguments: <ArgO>
};

Arguments对象是活动对象的属性,它包含的属性如下:

  • callee 对当前函数的引用
  • length 实际参数的数量
  • properties-indexes(整数,转换为字符串)其值是函数参数的值(在参数列表中从左到右),properties-indexes的个数 == arguments.length,arguments对象的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);

处理上下文代码的阶段

处理执行上下文代码分为两个阶段:

  • 进入执行上下文
  • 执行代码

对变量对象的修改和这两个阶段密切相关

要注意的是,这两个处理阶段的行为是通用的,与上下文类型无关(不管是全局上下文还是函数上下文)

进入执行上下文

一旦进入执行上下文(但是在执行代码之前),VO就会被填充如下的一些属性:

  • 函数的形参(当进入函数执行上下文时)
    变量对象的属性,其属性名就是形参的名字,其值就是实参的值,对于没有传递的参数,其值为undefined
  • 函数声明(FunctionDeclaration,FD)
    变量对象的属性,其属性名和值就是函数对象的名称和值,如果变量对象已经包含具有相同名称的属性,则替换它的值
  • 变量声明(var,VariableDeclaration)
    变量对象的属性,其属性名即为变量名,其值为undefined,如果变量名和已经声明的函数名或函数参数名相同,则不会影响已经存在的属性

看个栗子🌰:

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

当以10为参数进入test函数上下文时,对应的AO如下:

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

注意,上面的AO并不包含函数x,因为这里的x不是函数声明而是函数表达式(FunctionExpression,简称FE),函数表达式不会影响VO, 尽管函数_e也是函数表达式,然而,由于它被赋值给变量e,因此它可以通过e来访问到

至此,处理上下文代码的第一阶段介绍完了,接下来介绍第二阶段:执行代码阶段

执行代码

此时AO/VO的属性已经填充好,(尽管大部分属性都还没有赋予真正的值,都只是初始化时的undefined)
以上一例子为例,到了执行代码阶段,AO/VO会修改为如下形式:

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

再次强调,这里函数表达式_e仍在内存中,因为它被保存在声明的变量e中,而同样是函数表达式的x却不在AO/VO中,如果尝试在定义前或者定义后调用x函数,这时会发生x is not defined错误,未保存的函数表达式只有在定义或递归时才能调用

一个更加典型的例子:

alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {}
alert(x); // 20

上例中,为何x打印出来是函数呢,为何在声明前就可以访问到,为何10或者20不是这样呢,原因在于,根据规则,在进入上下文时,VO会被函数声明填充,同时还有变量声明x,但是,变量声明是在函数声明和函数形参之后,并且变量声明不会与已经存在的同名的函数声明和函数形参冲突, 因此在进入上下文的阶段,VO填充为如下形式:

VO = {};

VO['x'] = <reference to FunctionDeclaration "x">

// found var x = 10;
// if function "x" would not be already defined 
// then "x" be undefined, but in our case
// variable declaration does not disturb
// the value of the function with the same name

VO['x'] = <the value is not disturbed, still function>

随后在执行代码阶段,VO被修改为如下:

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

在如下例子中,再次看到,在进入上下文阶段,变量存储在VO中(因此,尽管else代码块永远都不会执行,而b却仍然在VO中)

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

关于变量

一些JavaScript文章甚至是JavaScript书籍经常会说:声明全局变量的方式有两种,一种是使用var关键字(在全局上下文中),另外一种是不用var关键字(在任何位置),这样的描述是错误的,记住:

使用var关键字是声明变量的唯一方式

像下面👇的赋值语句:

a = 10;

仅仅是在全局对象上创建了新的属性(而不是变量),不是变量并不意味着它无法改变,而是按照ECMAScript中变量的概念不是变量

不同之处如下:

alert(a); // undefined
alert(b); // "b" is not defined

b = 10;
var a = 20;

所有这些都取决于VO和其修改的阶段(进入上下文阶段和代码执行阶段)

进入上下文阶段:

VO = {
  a: undefined
};

这个阶段并没有任何b,因为它不是变量,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

关于变量还有一点非常重要:与简单属性不同,变量是不能删除的{DontDelete},这意味着要想通过delete操作符来删除一个变量是不可能的

a = 10;
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
var b = 20;
alert(window.b); // 20
alert(delete b); // false
alert(window.b); // still 20

注意,在ES5中{DontDelete}被重命名为[[Configurable]],并且可以通过Object.defineProperty方法手动管理

然而,有一个执行上下文不会被这个规则影响到,这就是eval上下文,其中没有为变量设置{DontDelete}属性

eval('var a = 10;');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined

引用

ECMA-262-3 in detail. Chapter 2. Variable object.