欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Javascript基础系列之变量对象

程序员文章站 2022-04-04 22:55:02
...

前言

本文翻译自variable-object

概要

在程序中我们免不了要声明函数变量去创建应用程序。然后,解析器是怎样以及去哪里找到这些数据(函数、变量)?当我们引用一个变量时,在解析器内部又发生了什么?

带着这样的问题,看下面代码

var a = 10; // variable of the global context
 
(function () {
  var b = 20; // local variable of the function context
})();
  
alert(a); // 10
alert(b); // "b" is not defined

同样的,大家都知道基于当前版本,独立作用域只能通过函数代码才能创建。它和C/C++不同,在ECMAScript中for循环不会创建一个局部上下文

数据(函数/变量)声明

如果变量与执行上下文相关,那么它就应该知道数据储存在哪里以及如何访问这些数据,这种机制被称为变量对象(variable object

变量对象(variable object, VO)是一个与某个执行上下文相关的特殊对象,并且存储了以下数据

  • 变量
  • 函数声明
  • 函数形参

notice: 在 ES5 中,变量对象和活动对象并入了词法环境模型(lexical environments model),详细的描述请看这里

简单的举个例子,可以使用ECMAScript 的对象来表示变量对象

VO = {}

正如我们所有,VO是执行上下文的一个属性

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

只有全局执行上下文中的变量对象才可以通过VO访问(因为全局对象就是VO对象本身),在其他上下文中是不能直接访问VO对象的,因为它只是内部机制的一个实现(抽象的)

当我们声明一个变量或者函数时,等于在VO对象上添加了一个对应的属性键值对

例如:

var a = 10;

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

test(30);

对应的变量对象如下:

VO(globalContext) = {
    a:10,
    test:<reference to function>
}

VO(test functionContext) = {
    x:30,
    b:20
}

但是,在实现层面上(和规范中)变量对象只是一个抽象概念。从本质上说,在实际执行上下文中,VO 可能完全不叫 VO,而且其初始结构也可能完全不同

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

对所有类型执行上下文,变量对象的一些操作(如变量对象)和行为都是相同的。从这个角度来看,把变量对象表示为抽象对象概念更多合适。而在函数执行上下文中也可以给变量对象定义相关的额外细节

AbstractVO (变量实例化过程中的通用行为)

  ║
  ╠══> GlobalContextVO
  ║        (VO === this === global)
  ║
  ╚══> FunctionContextVO
           (VO === AO, <arguments> object and <formal parameters> are added)

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

在这,非常有必要给变量对象一个定义

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

全局对象建立时候,像MathStringDateparseInt等属性会被初始化,同样也能添加其他对象作为属性,其中包括可以引用全局对象自身的属性。比如,在BOM中,window属性就是引用全局对象自身

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

在引用全局对象属性时候,前缀通常是可以省略的,因为全局对象是不能通过名字直接访问的。然而,我们还是可以通过this直接访问全局对象,也可以通过全局对象属性来访问到全局对象,例如,DOM中的window属性

String(10); // 等同于 global.String(10);

// 带前缀
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20;   // global.b = 20;

因此,全局执行上下文中的变量对象就是全局对象自身

VO(globalContext)  == VO

函数上下文中的变量对象

函数执行上下文中,变量对象(VO)是不可以直接访问的,此时活动对象(AO)扮演着VO的角色

VO(fucntionContext)  == AO

当进入函数执行上下文的时候,活动对象被创建,同时伴随着 arguments 属性的初始化,该属性是 Arguments 对象的值:

AO = {
  arguments: 
};

arguments 对象是活动对象(AO)中的一个属性,包含以下属性

  • callee -- 当前函数引用
  • length - 实参数量
  • properties-indexes(字符串类型的整数),属性的值就是函数的参数值(按参数列表从左到右排列)。properties-indexes 的元素的个数等于 arguments.length,properties-indexes 的值和实际传递进来的参数之间是共享的

例如:

function foo(x, y, z) {
  
  // 形参个数
  alert(foo.length); // 3
 
  // 实参个数
  alert(arguments.length); // 2
 
  alert(arguments.callee === foo); // true
  // 参数共享
  alert(x === arguments[0]); // true
  alert(x); // 10
  
  arguments[0] = 20;
  alert(x); // 20
  
  x = 30;
  alert(arguments[0]); // 30
  
  z = 40;
  alert(arguments[2]); // undefined
  
  arguments[2] = 50;
  alert(z); // 40
  
}
  
foo(10, 20);

处理上下文代码的几个阶段

到此,将是本文最核心的部分了。处理执行上下文分为两个部分

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

变量对象的修改和两个阶段息息相关。这两个处理阶段是通用的行为,与上下文类型无关(不管是全局上下文还是函数上下文都是一致的)。

进入执行上下文

当进入执行上下文时(在代码执行前),VO就会被下列属性填充(在此前已经描述过了)

  • 函数所有的参数(如果是在函数执行上下文中)都对应变量对象中的一个属性。该属性由形参名和对应的实参值构成,如果没有传递实参,那么该属性值就为 undefined
  • 所有函数声明(FunctionDeclaration, FD) 每个函数声明都对应变量对象中的一个属性,这个属性由一个函数对象的名称和值构成,如果变量对象中存在相同的属性名,则完全替换该属性。
  • 所有变量声明都对应变量对象中的一个属性,该属性的键/值是变量名和 undefined,如果变量名与已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。

如下:

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

test(10); // call

当进入test的执行上下文,并传递了实参10,AO对象如下:

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

注意AO中并不包含X函数,因为X函数不是函数声明,而是一个函数表达式(FE),函数表达式不会影响AO

执行代码

此时,AO/VO属性已经填充完毕(尽管很多属性值还是undefined)

继续上个例子,到了执行阶段,AO/VO就会修改为如下形式

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

再次注意,函数表达式 _e 仍在内存中,它被保存在声明的变量 e 中。但函数表达式 x 却不在 AO/VO 中,如果尝试在其定义前或者定义后调用 x 函数,这时会发生“x未定义”的错误。未保存在变量中的函数表达式只能在其内部或通过递归才能被调用

另外一经典例子:

alert(x); // function

var x = 10;
alert(x); // 10

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

为什么第一次弹出的是 “function”?为何在 x 声明前就能访问到?为什么弹出的不是 “10” 或者 “20”?原因在于,根据规范,在进入上下文时,VO 中的 x 被填充为函数声明。同时,还有变量声明 x,但是,根据前面的规则,变量声明是在函数形参和函数声明之后,并且,变量声明不会影响已经存在的同名函数或形参,因此,进入上下文时,VO 如下:

VO = {};

VO['x'] = <引用了函数声明“x”>

// 发现var x = 10;
// 如果函数“x”还未定义
// 则 "x" 为 undefined, 但是,在我们的例子中
// 变量声明并不会影响同名的函数值

VO['x'] = <值不受影响,仍是函数>

随后在代码执行阶段

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