[译] JavaScript核心指南(JavaScript Core)

本文是对“ECMA-262-3 in detail”系列学习内容的概述与总结。如果你对ES3系列文章感兴趣,本文每一节内容均包含相应ES3系列章节的链接,以供阅读与获取更深入的解释。

本文预期读者:有经验的程序员,专业人士

让我们首先来考虑ECMAScript的基础—— 对象(object) 的概念。

一、对象(An Object)

ECMAScript是一门高度抽象的面向对象(object-oriented)语言。虽然ECMAScript包含 基本类型(primitives) , 但是在需要的时候它们都会被转换为对象.

对象是一个属性的集合(collection of properties),同时具有单一原型对象引用(single prototype object)。原型可以指向一个对象,也可以为 null

让我们来看一个基本的对象Foo。首先要说明的是:一个对象的原型会使用内部属性[[Prototype]]表示引用。在下图中我们将使用 “__<internal-property(内部属性)>__”来标记内部属性,比如以__proto__ 来表示原型属性(这是某些脚本引擎比如SpiderMonkey的对于原型概念的具体实现,尽管并非标准).

代码如下:

1 var foo = {
2     x: 10,
3     y: 20
4 };

foo对象含有两个明确的私有属性(x与y)和一个隐含的 __proto__ 属性(指向foo的原型):

图1.一个含有原型的基本对象

图 1. 一个含有原型的基本对象

为什么需要原型呢,让我们考虑 原型链 的概念来回答这个问题.

二、原型链(A Prototype Chain)

原型对象也只是简单的对象,并可能有自己的原型。如果一个原型有一个非空引用指向它自己的原型,它自己的原型也有非空引用指向自己的原型……诸如此类,这就是所谓的 原型链

原型链是由 有限(finite) 的对象链接而成,可以 实现 继承(inheritance) 与 共享 属性.

考虑下面这样一种情况:假设我们有两个差别很小的对象,那么在一个设计良好的系统中,显而易见地,我们将会 复用(reuse) 那些相似的功能/代码而不是在每个对象中重复实现。在基于类的系统中,这个 代码重用(code reuse) 风格称为基类继承(class-based inheritance— 你在A类中放入公共的功能(共性),提供给继承于A类的B和C类,同时B和C类保持自身的微小差别。

ECMAScript没有类的概念。但是代码复用的理念类似 (甚至在某些方面比基于类的语言更加灵活) 并通过原型链实现。这种类型的继承被称为 基于原型的继承(prototype based inheritance)。

类似于类”A”,”B”,”C”,在ECMAScript中创建对象类”a”,”b”,”c”,相应地, 对象“a” 拥有对象“b”和”c”的共同部分。同时对象“b”和”c”只包含它们自己的附加属性或方法。

01 var a = {
02     x: 10,
03     calculate: function (z) {
04         return this.x + this.y + z
05     }
06 };
07  
08 var b = {
09     y: 20,
10     __proto__: a
11 };
12  
13 var c = {
14     y: 30,
15     __proto__: a
16 };
17  
18 // 调用继承的方法
19  
20 b.calculate(30); // 60
21  
22 c.calculate(40); // 80

很容易,不是吗? 可以看到,对象“b”和“c”访问了定义于”a”对象中的”calculate”方法。而做到这一点正是通过原型链。规则很简单:如果在对象本身中没有找到某个属性或方法(即对象没有此私有属性),那么就尝试在原型链中查找此对象或方法。如果在原型中找不到此属性,就查找原型的原型并以此类推直到查找完整个原型链(在基于类的继承语言系统中,如果要访问一个继承的方法,实现原理完全相同,不同之处在于查找的是 类型链(class chain) )。 第一个被找到的具有同样名字的方法/属性会被访问,并被称为 继承属性(inherited property)。如果在整个原型链中找不到此方法/属性,则返回 undefined.

请注意,this 指针在继承的方法中指向原有对象,而非原型对象。在上面的例子中 “this.y” 取值于“b” and “c”,而不是“a”。但是“this.x”来自于“a”。

如果一个对象没有明确指定原型,那“__proto__”的默认值为Object.prototype。对象 “Object.prototype” 自身也有一个“__proto__”,它是整个原型链的 最后一环(final link)并且值为 null.

下图显示了对象“a”,“b”和“c”的对象继承层次结构:

Figure 2. A prototype chain.

图 2. 原型链

原型链通常将会在这样的情况下使用:对象拥有 相同或相似的状态结构(same or similar state structure) (即相同的属性集合)与 不同的状态值(different state values)。在这种情况下,我们可以使用 构造函数(Constructor) 在 特定模式(specified pattern) 下创建对象。

三、构造函数(Constructor)

除了创建对象,构造函数(constructor) 还做了另一件有用的事情—自动为创建的对象设置了 原型对象(prototype object) 。原型对象存放于 ConstructorFunction.prototype 属性中.

例如,我们重写之前例子,使用构造函数创建对象“b”和“c”,那么对象”a”则扮演了“Foo.prototype”这个角色:

01 // 一个构造函数
02 function Foo(y) {
03   //这个构造函数将会以特定模式创建对象:被创建的对象都会有"y"属性
04   this.y = y;
05 }
06  
07 // "Foo.prototype"存放了新创建对象的原型的引用
08 // 所以我们可以将之用于定义继承属性/方法
09  
10 // 继承属性"x"
11 Foo.prototype.x = 10;
12  
13 // 继承方法"calculate"
14 Foo.prototype.calculate = function (z) {
15   this.x + this.y + z;
16 };
17  
18 // 使用模式"Foo"创建对象"b"和“c”
19 var b = new Foo(20);
20 var c = new Foo(30);
21  
22 // 调用继承方法
23 b.calculate(30); // 60
24 c.calculate(40); // 80
25  
26 // 让我们看看是否使用了预期的属性
27  
28 console.log(
29  
30   b.__proto__ === Foo.prototype// true
31   c.__proto__ === Foo.prototype// true
32  
33   // "Foo.prototype"自动创建了一个特定的属性"constructor",指向构造函数本身
34   // 实例"b"和"c"可以通过授权找到它并用以检测自己的构造函数
35  
36   b.constructor === Foo, // true
37   c.constructor === Foo, // true
38   Foo.prototype.constructor === Foo // true
39  
40   b.calculate === b.__proto__.calculate, // true
41   b.__proto__.calculate === Foo.prototype.calculate // true
42  
43 );

上述代码可以表示如下关系:

Figure 3. A constructor and objects relationship.

图 3. 构造函数与对象之间的关系

上图表明每个对象都拥有原型。构造函数 “Foo” 同样有它自己的”__proto__”—— “Function.prototype”,“Function.prototype”的”__proto__”属性为 “Object.prototype”。需要重复说明的是,“Foo.prototype”只是“Foo”的隐含属性,指向对象 “b”和“c”的原型。

如果去考虑 类化(classification) 的概念(我们刚刚类化了一个分离的事物——Foo),构造函数与原型对象组合起来或许可以称之为“类”。事实上,Python的“first-class”动态类采取了完全同样的实现。从这个角度看来,Python的类相比较于ECMAScript的基于原型的继承只是Syntactic Suger[译注:这里可能是“语法不同”的意思].

这个问题完整和详细的解释可以在ES3系列的第七章找到。有两个部分 7.1章 面向对象编程.一般理论(Chapter 7.1. OOP. The general theory),描述了不同的面向对象的范式与风格(OOP paradigms and stylistics),以及与ECMAScript的比较, 7.2章 面向对象编程.ECMAScript实现(Chapter 7.2. OOP. ECMAScript implementation), 专门讲述了ECMAScript中的面向对象编程.

现在,我们知道了基本的对象概念,让我们看看ECMAScript中 运行时程序执行(runtime program execution) 是怎样实现的。这被称之为 执行上下文堆栈(execution context stack), 其中的每个元素可以抽象地表示为对象。ECMAScript中,对象的概念几乎无处不在。

四、执行上下文栈(Execution Context Stack)

ECMAScript代码由三种类型: 全局(global) 代码, 函数(function) 代码和 求值(eval) 代码[译注:这里实在是没有很好的翻译,本意为字符串求值。orz…]。每一句代码都在它的上下文中被计算与执行。每一次函数调用,会进入函数的执行上下文并求出其代码类型;每一次eval 调用,都会进入 eval 执行上下文并执行代码。

请注意,由于每次函数调用都会产生具有不同状态的上下文,所以一个函数能可能产生无限数量的上下文集合。

1 function foo(bar) {}
2 // 调用同一个函数生成3个上下文
3 // 每一次调用产生的上下文状态不同(比如参数"b"的值)
4 foo(10);
5 foo(20);
6 foo(30);

一个执行上下文会激活其它上下文,比如一个函数调用另一个函数(或者在全局上下文中调用一个全局函数),然后循环往复。理论上来说,这种上下文状态会被实现为一个栈,被称为 执行上下文栈(execution context stack) 。

激活其它上下文的某个上下文被称为 调用者(caller) 。被激活的上下文被称为 被调用者(callee) 。被调用者同时也可能是调用者(比如一个在全局上下文中被调用的函数调用某些自身的内部方法)。

当调用一方激活(调用)被调用的一方时,调用函数会暂停(挂起)当前代码的执行并将控制流交给被调用者。此时被调用一方会被压入堆栈,称为一个 运行时(running (active)) 执行上下文。当被调用一方结束其运行时,它将控制权交回调用一方,然后对调用一方的上线文的求值会继续执行(可能激活其它上下文)直到其运行结束。被调用者会简单的进行 返回(return) 或以 异常(exception) 退出。被抛出的异常如果没有捕获,将会终止/退出(从栈中弹出)一个或多个上下文。

如下所示:所有的ECMAScript 程序运行时(program runtime) 可以表示为 执行上下文栈(execution context (EC) stack) ,其栈顶是当前被激活的上下文:

Figure 4. An execution context stack.

图 4. 执行上下文栈

程序开始运行时,会先进入栈底的(栈的第一个元素) 全局执行上下文(global execution context) ,然后全局代码进行初始化,创建所需的对象与方法。在全局上下文的执行过程中,代码会激活其它(已经创建)函数并进入函数各自的执行上下文,同时会向栈中压入新的元素。初始化完成后,运行时系统会等待事件的注入(比如用户鼠标点击),事件会激活函数并且进入新的执行上下文。

见图5,有一个函数上下文“EC1″和一个全局上下文“Global EC”,下图展现了从“Global EC”进入和退出“EC1″时栈的变化:

Figure 5. An execution context stack changes.

图 5. 执行上下文栈的变化

ECMAScript运行时系统就是这样管理代码的执行。

关于ECMAScript执行上下文栈的内容请查阅 第1章. 执行上下文(Chapter 1. Execution context)。

如上所述,栈中每一个执行上下文可以表示为一个对象。让我们看看上下文对象的结构以及执行其代码所需的 状态(state) 。

五、执行上下文(Execution Context)

执行上下文可以抽象为一个简单的对象。每个上下文包含一系列属性(我们称之为 上下文状态(context’s state) ) 用以跟踪相关代码的执行过程。下图展示了上下文的结构:

Figure 6. An execution context structure.

图 6. 上下文结构

除了这3个所需要的属性(变量对象(variable object),this指针(this value),作用域链(scope chain) ),执行上下文根据具体实现还可以具有任意额外属性。

接着,让我们仔细说明上下文的重要属性。

六、变量对象(Variable Object)

变量对象(variable object) 是与执行上下文相关的 数据作用域(scope of data) 。它是与上下文关联的特殊对象,用于存储被定义在上下文中的 变量(variables) 和 函数声明(function declarations) 

请注意,变量对象不包含 函数表达式(function expressions) (与 函数声明(function declarations) 比较)。

变量对象是一个抽象的概念。在不同的上下文中,它以不同的对象[译注:意思应为对象集合]来表示。举例来说,全局上下文中,变量对象是 全局对象本身(global object itself) (这就是我们能够通过属性名称引用全局对象的全局变量)。

让我们看看下面的例子:

01 var foo = 10;
02  
03 function bar() {} // 函数声明
04 (function baz() {}); // 函数表达式
05  
06 console.log(
07   this.foo == foo, // true
08   window.bar == bar // true
09 );
10  
11 console.log(baz); // 引用错误,baz没有被定义

全局上下文中的变量对象(VO)会有如下属性:

Figure 7. The global variable object.

图 7. 全局变量对象

如上所示,函数“baz”如果作为函数表达式则不被不被包含于变量对象。这就是在函数外部尝试访问产生 引应用错误(ReferenceError) 的原因。

请注意,ECMAScript和其他语言相比(比如C/C++),仅有函数能够创建新的作用域。在函数内部定义的变量与内部函数,在外部非直接可见并且不污染全局对象。

使用 eval 的时候,我们同样会使用一个新的(eval创建)执行上下文。eval会使用全局变量对象或调用者的变量对象(eval的调用来源)。

那函数以及自身的变量对象又是怎样的呢?在一个函数上下文中,变量对象被表示为 激活对象(activation object)

七、激活对象(Activation object)

当函数被调用者激活,一个特殊的对象——激活对象(activation object) 将被创建。它包含 形式参数(formal parameters) 与特殊 参数(arguments) 对象(具有索引属性的形参键值表(map)[译注:表现为数组])。激活对象在函数上下文中作为变量对象使用。

即:函数的变量对象保持不变,但除去存储变量与函数声明之外,还包含形参以及特殊对象arguments 

考虑下面的例子

1

你可能感兴趣的:(JavaScript)