前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ECMA-262-3 详解:2、变量对象

ECMA-262-3 详解:2、变量对象

作者头像
踏浪
修改2020-06-22 17:19:00
5830
修改2020-06-22 17:19:00
举报
文章被收录于专栏:踏浪的文章踏浪的文章

从来没有深入了解ECMA,网上找了一下,发现早在2010年就有大佬 Dmitry Soshnikov 总结了ECMA中的核心内容,我这里只是翻译记录,加深自己的印象。文章原文来自 ECMA-262-3 in detail. Chapter 2. Variable object.

介绍

在我们创建应用程序的时候,总是避免不了会进行函数和变量的声明。但是,解释器(interpreter)是怎么找到我们的数据(函数和变量)的呢?又是在哪里找到的呢?我们引用我们需要的对象的时候又发生了什么呢?

大部分程序员都知道变量与执行上下文紧密相关。

代码语言:javascript
复制
var a = 10; // variable of global context
(function foo() {	var b = 20; // local variable of the function context})();
alert(a); // 10alert(b); // b is not defined

同样,许多程序员也都知道,在当前的版本规范中,只有函数(function)代码的执行上下文才可以创建独立的作用域。与C++等语言相反,在ECMAScript中,for循环 没有 创建一个独立的作用域。这就是为什么,下面的代码, i 始终是5

代码语言:javascript
复制
var obj = {};for (var i = 0; i < 5; i++) {	obj[i] = function() {console.log(i)}}
for (var k in obj) { obj[k]() }

我们来详细了解一下声明数据的时候都发生了什么。

数据声明

如果变量与执行上下文相关,那么,他就知道它的数据存放在哪里以及如何获取。这种机制称为 变量对象(variable object)

一个变量对象(缩写形式 - VO)是包含执行上下文的特殊对象,并且保存着:

  • variables( var, 变量声明 )
  • function declarations(函数声明,缩写形式为FD)
  • 函数形参

以上内容均在上下文中声明。

Notice:在ES5中,变量对象的概念已经被词汇环境模型所取代。

举例来说,可以将变量对象表示为普通的ESMAScript对象:

代码语言:javascript
复制
VO = {};

正如我们所说,变量对象是执行上下文的一个属性,则:

代码语言:javascript
复制
activeExecutionContext = {	VO: {		// context data(var, FD, function arguments)	}}

只有全局上下文中的变量对象可以通过VO的属性名称间接访问、使用(其中全局变量自身就是变量对象)。对于其他的上下文,直接访问VO是不可能的,因为它(VO)纯粹是实现机制(内部的事情)。

当我们声明变量或者函数的时候,除了使用变量名和值创建VO的新属性外,没有其他的事情了。

例如:

代码语言:javascript
复制
var a = 10;function test(x) {	var b = 20;};test(30);

对应的变量对象是:

代码语言:javascript
复制
// 全局的变量对象VO(globalContext) = {	a: 10,	test: <test> fn}
// 函数 test 中的变量对象VO(test functionContext) = {	x: 30,	b: 20}

但是在具体的实现层级(和规范中),变量对象只是抽象的事物(实际上是不存在的)。从根本上来说,在不同的具体执行上下文中,VO的名称和初始结构都是不同的。

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

变量对象的某些操作(例如:变量实例化)和表现对于所有的执行上下文类型都成很普通的。从这个角度出发,将变量对象当作为一个抽象的基础物质更容易理解。函数上下文还可以定义域变量对象相关的其他详细信息。

代码语言:javascript
复制
AbstractVO (变量实例化过程的一般行为)   ║  ╠══> GlobalContextVO  ║        (VO === this === global)  ║  ╚══> FunctionContextVO           (VO === AO, <arguments> object and <formal parameters> are added)

我们来详细分析一下:

全局上下文中的VO

首先,要给出Global对象的定义:

全局对象是在进入任何执行上下文之前就被创建好的。这个对象只存在一份,他的属性可以在进程的任何地方访问,进程结束,全局对象的声明周期结束。

在创建时候,全局对象通过 MathStringDateparseInt 等属性进行初始化,还可以附加其他对象作为属性,其中也包括引用全局对象自身的对象。例如:在BOM(浏览器对象模型)中,全局对象的 window 属性就是指向全局的(当然,并不是所有的实现都是这样的)。

请看下面的这个例子:windos 是global的属性,但同时值是global。

代码语言:javascript
复制
global = {	Math: <...>,	String: <...>,	...,	window: global}

当引用全局对象属性的时候,通常是省略前缀的,因为全局对象不可以直接通过名称访问。但是,可以通过全局上下文中的 this 访问,也可以通过递归自己调用自己(例如BOM中的window)来访问。所以,代码可简写为:

代码语言:javascript
复制
String(10); // 等同于 global.String(10);
// 有前缀window.a = 10; // === global.window.a = 10 === global.a = 10;this.b = 20; // global.b = 20;

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

代码语言:javascript
复制
VO(globalContext) === global;

准确理解 全局上下文中的变量对象就是全局变量自身 是非常有必要的,基于这个事实,在全局上下文中声明一个变量的时候,我们才可以通过全局对象的属性访问到这个变量(例如:实现未知变量名时):

代码语言:javascript
复制
var a = new String('test');
alert(a); // 直接获取到,因为在VO中找到了:'text'
alert(window['a']); // 间接获取到,通过 global === VO: 'text'alert(a === this.a); // true this === window
var akey = 'a';alert(window[akey]); // 间接获取到,通过动态属性名 akey === 'a'

函数上下文中的变量对象

关于函数的执行上下文,VO是不能直接获取的。此时由活动对象(activation object)扮演VO的角色。

代码语言:javascript
复制
VO(functionContext) === AO;

活动对象在进入函数上下文的时候被创建,并且有一个属性名为 argumants ,属性值为 Argumants Object 的初始值:

代码语言:javascript
复制
AO = {	arguments: <ArgO>}

Arguments Object 是活动对象的属性,他包含以下属性:

  • callee:指向当前函数的引用
  • length:实际传递的参数的数量
  • properties-indexes(属性索引,字符串类型的整数):属性的值就是函数的参数值(按照参数列表从左往右排列)。属性索引的数量==arguments.length。属性索引对应的值和实际传进来的参数是 共享的
代码语言:javascript
复制
function foo(x, y, z) {    // 定义的函数的参数数量 (x, y, z)  alert(foo.length); // 3   // 实际传递参数的数量 (only x, y)  alert(arguments.length); // 2   // 函数自身的引用  alert(arguments.callee === foo); // true    // 参数共享   alert(x === arguments[0]); // true  alert(x); // 10    arguments[0] = 20;  alert(x); // 20  11549  x = 30;  alert(arguments[0]); // 30    // 但是,对于未传递参数的z,属性索引的值是不共享的    z = 40;  alert(arguments[2]); // undefined    arguments[2] = 50;  alert(z); // 40  }  foo(10, 20);

关于最后一个例子,在 chrome 的老版本中存在一个bug — 即,没有传递参数z,z 与 arguments[2] 的仍然是共享的。

处理上下文代码的阶段

现在,我们终于进入到本文的关键部分,处理上下文代码的过程被分为两个基本阶段:

  1. 进入执行上下文
  2. 代码运行

变量对象的修改与这两个阶段有着密切的关联。

注意:这两个阶段的处理是一般行为,与上下文类型无关。(对于全局和函数上下文都是公平的)。

进入执行上下文

当进入执行上下文(但是是在代码运行 之前),VO被下面这些属性填充(在前文已经描述过)(从上往下优先级依次降低)

  • 函数的每一个形参(如果我们是在函数执行上下文) — 变量对象的一个属性,这个属性由形参的名称与值组成;如果没有传递实际参数,那么这个属性就由形参形式的名称和 undefined 的值组成。
  • 每一个函数声明(FunctionDeclaration, FD) — 变量对象的一个属性,这个属性的名称是函数名,值是这个函数对象,如果这个变量对象已经拥有了相同名称的属性,那么完全替换这个属性。
  • 每一个变量声明(var, VariableDecalartion) — 变量对象的一个属性,这个属性的名称是变量名,值是 undefined 。如果这个变量名和已经声明的形参或者是函数名称相同,那么这个变量不会影响已经存在的属性。

来看一个例子:

代码语言:javascript
复制
function test(a, b) {	var c = 10;	function d() {}	var e = function _e() {}	(function x(){})}test(10);

当进入到 test 函数的上下文的时候,test函数接收了一个实参 10,AO对象如下:

代码语言:javascript
复制
AO(test) = {	a: 10,	b: undefined,	c: undefined,	d: <FD d>,	e: undefined}

注意,AO中并没有包含函数 x,这是因为 x 并不是一个函数声明而是一个函数表达式(FunctionExpression, 缩写形式:FE),不影响VO(即这里的AO)。

但是,函数 _e 也是一个函数表达式,就像接下来要看到的,它是被分配给了变量 e,它可以通过变量 e 来访问。关于函数声明( FunctionDeclaration )和函数表达式(FunctionExpression )的不同将会在Chapter 5. Functions中讲到。

代码执行

这个时候,AO/VO已经被各种属性填满了(但是,不是所有的属性都已经有具体的值了,他们中的大部分的初始值都还是 undefined )。

所有代码以及环境不变的情况下,上面的代码中,AO/VO在代码解释器间被修改为如下:

代码语言:javascript
复制
AO['c'] = 10;AO['e'] = <FE _e>;

再次注意,因为FE _e 是被保存在变量 e 中,所以,它仍然存在于内存(理解成AO/VO)中。但是FE x 不在了。如果我们在定义之前或者时候调用 x 函数,我们会得到一个错误:x is not defined 。没有保存到一个变量的函数表达式(FE)只能立即执行或者是递归调用。

另一个经典例子:

代码语言:javascript
复制
alert(x); // functionvar x = 10;alert(x); // 10x = 20;function x() {};alert(x); // 20

为什么第一次 alert x 的是一个函数,而且,还是在声明之前?为什么不是 10 或者 20 ?因为,根据规则 — 当进去执行上下文的时候,VO是由函数声明填充的。同时,在相同的阶段,进入执行上下文的时候,有一个 x 的变量声明,但是我们上面已经提到了,如果这个变量名和已经声明的形参或者是函数名称相同,那么这个变量不会影响已经存在的属性。因此,当进入执行上下文的时候。VO进行如下填充:

代码语言:javascript
复制
VO = {};VO['x'] = <FD X>;// 找到x的变量声明,但是x已经存在了,所以变量声明无效VO['x'] = <值没有被影响,仍然是 function x>

当到了函数执行阶段,VO进行如下填充:

代码语言:javascript
复制
VO['x'] = 10;VO['x'] = 20;

这就是我们在第二次 alert 和第三次 alert 看到的内容。

下面的例子中,我们看到,当进入执行上下文阶段的时候变量都被存放在了 VO 中(虽然else 语句块没有执行,但是, b 依然存在于 VO中)

代码语言:javascript
复制
if (true) {	var a = 1;} else {	var b = 2;}
alert(a); // 1alert(b); // undefined 不是 b is not defined.

关于变量

通常很多关于JavaScript的文章或者数据中都指出:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。根本就不是这样的请记住:

变量声明只能通过 var 关键字进行声明。

就像这样:

代码语言:javascript
复制
a = 10;

这仅仅只是在全局对象上创建了一个新的属性(而不是一个变量)。“不是变量”不是表示不能被修改,而是指ESMAScript规范中的“不是变量”。(由于 VO(globalContext) === global的原因,也会成为全局对象上的属性,还记得吗?)

让我们用代码来展示两者的不同

代码语言:javascript
复制
alert(a); // undefinedalert(b); // b is not defined
b = 10;var a = 20;

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

进入上下文:

代码语言:javascript
复制
VO = {	a: undefined};

我们可以看到,这里没有 b ,因为这不是一个变量。b 只会出现在在代码执行阶段(但是上面的例子中不会出现,因为出错了)。

来改一下这段代码:

代码语言:javascript
复制
alert(a); // undefined 我们知道为什么
b = 10;alert(b); // 10 创建 代码执行阶段
var a = 20;alert(a); // 20 代码执行阶段修改

关于变量还有一个很重要的观点。与简单属性相反,变量具有 DontDelete 属性(ES5中为 [[Configurable]]),意味着我们不能通过 delete 删除变量。

代码语言:javascript
复制
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

然而,有一个例外。在 eval 上下文中,声明的变量没有 {DontDelete} 属性:

代码语言:javascript
复制
eval('var a = 10;');alert(window.a); // 10 alert(delete a); // true alert(window.a); // undefined

使用一些调试工具(例如:Firebug)的控制台测试该实例时,请注意,Firebug同样是使用eval来执行控制台里你的代码。因此,变量属性同样没有 {DontDelete}特性,可以被删除。

特殊实现:__parent__属性(不重要了)

前面已经提到过,按照标准规范,活动对象是不能直接访问的。然而,一些具体的实现并没有按照这个标准,例如 SpiderMonkey 和Rhino。在这些实现中,函数具有特殊的属性 __parent__ ,通过这个属性可以访问到已经创建的活动对象(或者是全局变量对象)。

例如(SpiderMonkey, Rhino):

代码语言:javascript
复制
var global = this;var a = 10;function foo() {};alert(foo.__parent__); // globalvar VO = foo.__parent__;alert(VO.a); // 10alert(VO === global);

在上面的例子中我们可以看到,函数foo是在全局上下文中创建的,所以属性__parent__ 指向全局上下文的变量对象,即全局对象。(译者注:还记得这个吧:VO(globalContext) === global)

然而,在SpiderMonkey中用同样的方式访问激活对象是不可能的:在不同版本的SpiderMonkey中,内部函数的__parent__ 有时指向null ,有时指向全局对象。

在Rhino中,用同样的方式访问激活对象是完全可以的。

例如 (Rhino):

代码语言:javascript
复制
var global = this;var x = 10; (function foo() {   var y = 20;   // the activation object of the "foo" context  var AO = (function () {}).__parent__;   print(AO.y); // 20   // __parent__ of the current activation  // object is already the global object,  // i.e. the special chain of variable objects is formed,  // so-called, a scope chain  print(AO.__parent__ === global); // true   print(AO.__parent__.x); // 10 })();

阅读原文

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍
  • 数据声明
  • 不同执行上下文中的变量对象
  • 全局上下文中的VO
  • 函数上下文中的变量对象
  • 处理上下文代码的阶段
  • 进入执行上下文
  • 代码执行
  • 关于变量
  • 特殊实现:__parent__属性(不重要了)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档