[JavaScript]ECMA-262-3 深入解析.第二章.变量对象

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

介绍

我们在创建应用程序的时候,总免不了要声明变量和函数。那么,当我们需要使用这些东西的时候,解释器(interpreter)是怎么样、从哪里找到我们的数据(函数,变量)的,这个过程究竟发生了什么呢?

大部分ECMAScript程序员应该都知道变量与 执行上下文 密切相关:

1
2
3
4
5
6
7
8
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

同样,很多程序员也知道,基于当前版本的规范,独立作用域只能通过“函数(function)”代码类型的执行上下文创建。那么,想对于C/C++举例来说,ECMAScript里, for 循环并不能创建一个局部的上下文。(译者注:就是局部作用域):

1
2
3
4
5
for  ( var  k in  {a: 1, b: 2}) {
   alert(k);
}
  
alert(k); // variable "k" still in scope even the loop is finished

下面我们具体来看一看,当我们声明数据时候的内部细节。

数据声明

如果变量与执行上下文相关,那么它自己应该知道它的数据存储在哪里和如何访问。这种机制被称作 变量对象(variable object).

变量对象 (缩写为VO)就是与执行上下文相关的对象(译者注:这个“对象”的意思就是指某个东西),它存储下列内容:
  • 变量 (var, VariableDeclaration);
  • 函数声明 (FunctionDeclaration, 缩写为FD);
  • 以及函数的形参

以上均在上下文中声明。

简单举例如下,一个变量对象完全有可能用正常的ECMAScript对象的形式来表现:

1
VO = {};

正如我们之前所说, VO就是执行上下文的属性(property):

1
2
3
4
5
activeExecutionContext = {
   VO: {
     // context data (var, FD, function arguments)
   }
};

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

当我们声明一个变量或一个函数的时候,同时还用变量的名称和值,在VO里创建了一个新的属性。

例如:

1
2
3
4
5
6
7
var  a = 10;
 
function  test(x) {
   var  b = 20;
};
 
test(30);

对应的变量对象是:

1
2
3
4
5
6
7
8
9
10
11
// Variable object of the global context
VO(globalContext) = {
   a: 10,
   test:
};
 
// Variable object of the "test" function context
VO(test functionContext) = {
   x: 30,
   b: 20
};

在具体实现层面(和在规范中)变量对象只是一个抽象的事物。(译者注:这句话翻译的总感觉不太顺溜,欢迎您提供更好的译文。)从本质上说,在不同的具体执行上下文中,VO的名称和初始结构都不同。

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

对于所有类型的执行上下文来说,变量对象的一些操作(如变量初始化)和行为都是共通的。从这个角度来看,把变量对象作为抽象的基本事物来理解更容易。而在函数上下文里同样可以通过变量对象定义一些相关的额外细节。

下面,我们详细展开探讨;

全局上下文中的变量对象

这里有必要先给全局对象(Global object)一个明确的定义:

全局对象(Global object) 是在进入任何执行上下文之前就已经创建的对象;这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。

初始创建阶段,全局对象通过Math,String,Date,parseInt等属性初始化,同样也可以附加其它对象作为属性,其中包括可以引用全局对象自身的对象。例如,在DOM中,全局对象的window属性就是引用全局对象自身的属性(当然,并不是所有的具体实现都是这样):

1
2
3
4
5
6
7
global = {
   Math: <...>,
   String: <...>
   ...
   ...
   window: global
};

因为全局对象是不能通过名称直接访问的,所以当访问全局对象的属性时,通常忽略前缀。尽管如此,通过全局上下文的this还是有可能直接访问到全局对象的,同样也可以通过引用自身的属性来访问,例如,DOM中的window。综上所述,代码可以简写为:

1
2
3
4
5
String(10); // means global.String(10);
 
// with prefixes
window.a = 10; // === global.window.a = 10 === global.a = 10;
this .b = 20; // global.b = 20;

因此,全局上下文中的变量对象就是全局对象自身(global object itself):

1
VO(globalContext) === global;

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

1
2
3
4
5
6
7
8
9
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的角色。

1
VO(functionContext) === AO;

激活对象 是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。grguments属性的值是Arguments object

1
2
3
4
<pre class = "brush:javascript" >AO = {
   arguments: <ArgO>
};
</pre>

Arguments objects 是函数上下文里的激活对象中的内部对象,它包括下列属性:

  • callee — 指向当前函数的引用;
  • length — 真正传递的参数的个数;
  • properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。(译者注:共享与不共享的区别可以对比理解为引用传递与值传递的区别)

例如:

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
function  foo(x, y, z) {
 
   alert(arguments.length); // 2 – quantity of passed arguments
   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
 
   // 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);

最后一个例子的场景,在当前版本的Google Chrome浏览器里有一个bug  — 即使没有传递参数z,zarguments[2]仍然是共享的。(译者注:我试验了一下,在Chrome Ver4.1.249.1059版本,该bug仍然存在)

分阶段处理上下文代码

现在我们终于触及到本文的核心内容。执行上下文的代码被分成两个基本的阶段来处理:

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

变量对象的变化与这两个阶段紧密相关。

进入执行上下文

当进入执行上下文(代码执行之前)时,VO已被下列属性填充满(这些都已经在前文描述过):

  • 函数的所有形式参数(如果我们是在函数执行上下文中)
  • — 变量对象的一个属性,这个属性由一个形式参数的名称和值组成;如果没有对应传递实际参数,那么这个属性就由形式参数的名称和undefined值组成;

  • 所有函数声明(FunctionDeclaration, FD)
  • —变量对象的一个属性,这个属性由一个函数对象(function-object)的名称和值组成;如果变量对象已经存在相同名称的属性,则完全替换这个属性。

  • 所有变量声明(var, VariableDeclaration)
  • —变量对象的一个属性,这个属性由变量名称和undefined值组成;如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

让我们看一个例子:

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

进入“test”函数的上下文时(传递参数10),AO如下:

1
2
3
4
5
6
7
AO(test) = {
   a: 10,
   b: undefined,
   c: undefined,
   d: <reference to FunctionDeclaration "d" >
   e: undefined
};

注意,AO里并不包含函数“x”。这是因为“x” 是一个函数表达式(FunctionExpression, 缩写为 FE) 而不是函数声明,函数表达式不会影响VO(译者注:这里的VO指的就是AO)。 不管怎样,函数“_e” 同样也是函数表达式,但是就像我们下面将看到的那样,因为它分配给了变量 “e”,所以它变成可以通过名称“e”来访问。FunctionDeclaration 与 FunctionExpression 的不同,将在 Chapter 5. Functions进行详细的探讨。

这之后,将进入处理上下文代码的第二个阶段 — 执行代码。

执行代码

这一刻,AO/VO 已经被属性(不过,并不是所有的属性都有值,大部分属性的值还是系统默认的初始值undefined )填满。

还是前面那个例子, AO/VO 在代码解释期间被修改如下:

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

再次注意,因为FunctionExpression“_e”保存到了已声明的变量“e”上,所以它仍然存在于内存中(译者注:就是还在AO/VO中的意思)。而FunctionExpression。未保存的函数表达式只有在它自己的定义或递归中才能被调用。 “x” 并不存在于AO/VO中。即,如果我们想尝试调用“x”函数,不管在函数定义之前还是之后,都会出现一个错误“x is not defined”

另一个经典例子:

1
2
3
4
5
6
7
8
9
10
alert(x); // function
 
var  x = 10;
alert(x); // 10
 
x = 20;
 
function  x() {};
 
alert(x); // 20

为什么第一个alert “x” 的返回值是function,而且它还是在“x” 声明之前访问的“x” 的?为什么不是10或20呢?因为,根据规范 — 当进入上下文时,往VO里填入函数声明;在相同的阶段,还有一个变量声明“x”,那么正如我们在上一个阶段所说,变量声明在顺序上跟在函数声明和形式参数声明之后,而且,在这个阶段(译者注:这个阶段是指进入执行上下文阶段),变量声明不会干扰VO中已经存在的同名函数声明或形式参数声明,因此,在进入上下文时,VO的结构如下:

1
2
3
4
5
6
7
8
9
10
11
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做如下修改:

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

我们可以在第二、三个alert看到这个效果。

在下面的例子里我们可以再次看到,变量是在进入上下文阶段放入VO中的。(因为,虽然else部分代码永远不会执行,但是不管怎样,变量“b”仍然存在于VO中。)(译者注:变量b虽然存在于VO中,但是变量b的值永远是undefined)

1
2
3
4
5
6
7
8
if  ( true ) {
   var  a = 1;
} else  {
   var  b = 2;
}
 
alert(a); // 1
alert(b); // undefined, but not "b is not defined"

关于变量

通常,各类文章和JavaScript相关的书籍都声称:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。请记住,这绝对是谣传:

任何时候,变量只能通过使用var关键字才能声明。

那么像下面这样分配:

1
a = 10;

这仅是给全局对象创建了一个新属性(但是它不是变量)。“不是变量”的意思并不是说它不能被改变,而是指它不符合ECMAScript规范中的变量概念,所以它“不是变量”(它之所以能成为全局对象的属性,完全是因为VO(globalContext) === global,大家还记得这个吧?)。

让我们通过下面的实例看看具体的区别吧:

1
2
3
4
5
alert(a); // undefined
alert(b); // "b" is not defined
 
b = 10;
var  a = 20;

所有根源仍然是VO和它的修改阶段(进入上下文 阶段和执行代码 阶段):

进入上下文阶段:

1
2
3
VO = {
   a: undefined
};

我们可以看到,因为“b”不是一个变量,所以在这个阶段根本就没有“b”,“b”将只在执行代码阶段才会出现(但是在我们这个例子里,还没有到那就已经出错了)。

让我们改变一下例子代码:

1
2
3
4
5
6
7
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

关于变量,还有一个重要的知识点。变量相对于简单属性来说,变量有一个特性(attribute):{DontDelete},这个特性的含义就是不同通过delete操作符直接删除变量属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
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}特性。

1
2
3
4
5
6
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):

1
2
3
4
5
6
7
8
9
10
11
var  global = this ;
var  a = 10;
 
function  foo() {}
 
alert(foo.__parent__); // global
 
var  VO = foo.__parent__;
 
alert(VO.a); // 10
alert(VO === global); // true

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

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

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

例如 (Rhino):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
 
})();

结论

在这篇文章里,我们进一步深入学习了跟执行上下文相关的对象。我希望这些知识对您来说能有所帮助,能解决一些您曾经遇到的问题或困惑。按照计划,在后续的章节中,我们将探讨Scope chainIdentifier resolution ,Closures

如果您有问题,我很高兴在下面评论中解答。

其他参考

 

英文地址 : ECMA-262-3 in detail.Chapter 2.Variable object
中文地址 [JavaScript]ECMA-262-3 深入解析.第二章.变量对象


本文转自Justin博客园博客,原文链接:http://www.cnblogs.com/justinw/archive/2010/04/23/1718733.html,如需转载请自行联系原作者

相关文章
|
3天前
|
数据采集 前端开发 JavaScript
金融数据分析:解析JavaScript渲染的隐藏表格
本文详解了如何使用Python与Selenium结合代理IP技术,从金融网站(如东方财富网)抓取由JavaScript渲染的隐藏表格数据。内容涵盖环境搭建、代理配置、模拟用户行为、数据解析与分析等关键步骤。通过设置Cookie和User-Agent,突破反爬机制;借助Selenium等待页面渲染,精准定位动态数据。同时,提供了常见错误解决方案及延伸练习,帮助读者掌握金融数据采集的核心技能,为投资决策提供支持。注意规避动态加载、代理验证及元素定位等潜在陷阱,确保数据抓取高效稳定。
33 17
|
6天前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
6天前
|
存储 JavaScript 前端开发
全网最全情景,深入浅出解析JavaScript数组去重:数值与引用类型的全面攻略
如果是基础类型数组,优先选择 Set。 对于引用类型数组,根据需求选择 Map 或 JSON.stringify()。 其余情况根据实际需求进行混合调用,就能更好的实现数组去重。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
19天前
|
JavaScript 前端开发 开发者
JavaScript基础——JavaScript变量名称命名规范
JavaScript变量命名规范是编写高质量代码的重要部分。通过遵循基本规则、使用常见命名约定并应用最佳实践,可以提高代码的可读性和可维护性。希望本文能帮助开发者在日常编程中更好地理解和应用JavaScript变量命名规范,从而编写出更清晰、更可靠的代码。
43 11
|
1月前
|
存储 Linux iOS开发
Python入门:2.注释与变量的全面解析
在学习Python编程的过程中,注释和变量是必须掌握的两个基础概念。注释帮助我们理解代码的意图,而变量则是用于存储和操作数据的核心工具。熟练掌握这两者,不仅能提高代码的可读性和维护性,还能为后续学习复杂编程概念打下坚实的基础。
Python入门:2.注释与变量的全面解析
|
5天前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
2月前
|
JavaScript 前端开发 容器
盘点JavaScript中所有声明变量的方式及特性
本文详细介绍了JavaScript中变量定义的多种方式,包括传统的`var`、`let`和`const`,以及通过`this`、`window`、`top`等对象定义变量的方法。每种方式都有其独特的语法和特性,并附有代码示例说明。推荐使用`let`和`const`以避免作用域和提升问题,谨慎使用`window`和`top`定义全局变量,不建议使用隐式全局变量。掌握这些定义方式有助于编写更健壮的JS代码。
54 11
|
4月前
|
SQL 存储 Oracle
南大通用GBase 8s数据库游标变量解析:提升数据库操作效率
南大通用GBase 8s 数据库游标变量解析:提升数据库操作效率
|
7天前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
46 29
|
3天前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
20 3

热门文章

最新文章

推荐镜像

更多