本文是对“ECMA-262-3 in detail”系列学习内容的概述与总结。如果你对ES3系列文章感兴趣,本文每一节内容均包含相应ES3系列章节的链接,以供阅读与获取更深入的解释。
本文预期读者:有经验的程序员,专业人士
让我们首先来考虑ECMAScript的基础—— 对象(object) 的概念。
ECMAScript是一门高度抽象的面向对象(object-oriented)语言。虽然ECMAScript包含 基本类型(primitives) , 但是在需要的时候它们都会被转换为对象.
对象是一个属性的集合(collection of properties),同时具有单一原型对象引用(single prototype object)。原型可以指向一个对象,也可以为 null。
让我们来看一个基本的对象Foo。首先要说明的是:一个对象的原型会使用内部属性[[Prototype]]表示引用。在下图中我们将使用 “__<internal-property(内部属性)>__”来标记内部属性,比如以__proto__ 来表示原型属性(这是某些脚本引擎比如SpiderMonkey的对于原型概念的具体实现,尽管并非标准).
代码如下:
foo对象含有两个明确的私有属性(x与y)和一个隐含的 __proto__ 属性(指向foo的原型):
图 1. 一个含有原型的基本对象
为什么需要原型呢,让我们考虑 原型链 的概念来回答这个问题.
原型对象也只是简单的对象,并可能有自己的原型。如果一个原型有一个非空引用指向它自己的原型,它自己的原型也有非空引用指向自己的原型……诸如此类,这就是所谓的 原型链。
原型链是由 有限(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”只包含它们自己的附加属性或方法。
03 |
calculate: function (z) { |
04 |
return this .x + this .y + z |
20 |
b.calculate(30); // 60 |
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”的对象继承层次结构:
图 2. 原型链
原型链通常将会在这样的情况下使用:对象拥有 相同或相似的状态结构(same or similar state structure) (即相同的属性集合)与 不同的状态值(different state values)。在这种情况下,我们可以使用 构造函数(Constructor) 在 特定模式(specified pattern) 下创建对象。
除了创建对象,构造函数(constructor) 还做了另一件有用的事情—自动为创建的对象设置了 原型对象(prototype object) 。原型对象存放于 ConstructorFunction.prototype 属性中.
例如,我们重写之前例子,使用构造函数创建对象“b”和“c”,那么对象”a”则扮演了“Foo.prototype”这个角色:
03 |
//这个构造函数将会以特定模式创建对象:被创建的对象都会有"y"属性 |
07 |
// "Foo.prototype"存放了新创建对象的原型的引用 |
08 |
// 所以我们可以将之用于定义继承属性/方法 |
14 |
Foo. prototype .calculate = function (z) { |
18 |
// 使用模式"Foo"创建对象"b"和“c” |
23 |
b.calculate(30); // 60 |
24 |
c.calculate(40); // 80 |
30 |
b.__proto__ === Foo. prototype , // true |
31 |
c.__proto__ === Foo. prototype , // true |
33 |
// "Foo.prototype"自动创建了一个特定的属性"constructor",指向构造函数本身 |
34 |
// 实例"b"和"c"可以通过授权找到它并用以检测自己的构造函数 |
36 |
b. constructor === Foo, // true |
37 |
c. constructor === Foo, // true |
38 |
Foo. prototype . constructor === Foo // true |
40 |
b.calculate === b.__proto__.calculate, // true |
41 |
b.__proto__.calculate === Foo. prototype .calculate // true |
上述代码可以表示如下关系:
图 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中,对象的概念几乎无处不在。
ECMAScript代码由三种类型: 全局(global) 代码, 函数(function) 代码和 求值(eval) 代码[译注:这里实在是没有很好的翻译,本意为字符串求值。orz…]。每一句代码都在它的上下文中被计算与执行。每一次函数调用,会进入函数的执行上下文并求出其代码类型;每一次eval 调用,都会进入 eval 执行上下文并执行代码。
请注意,由于每次函数调用都会产生具有不同状态的上下文,所以一个函数能可能产生无限数量的上下文集合。
3 |
// 每一次调用产生的上下文状态不同(比如参数"b"的值) |
一个执行上下文会激活其它上下文,比如一个函数调用另一个函数(或者在全局上下文中调用一个全局函数),然后循环往复。理论上来说,这种上下文状态会被实现为一个栈,被称为 执行上下文栈(execution context stack) 。
激活其它上下文的某个上下文被称为 调用者(caller) 。被激活的上下文被称为 被调用者(callee) 。被调用者同时也可能是调用者(比如一个在全局上下文中被调用的函数调用某些自身的内部方法)。
当调用一方激活(调用)被调用的一方时,调用函数会暂停(挂起)当前代码的执行并将控制流交给被调用者。此时被调用一方会被压入堆栈,称为一个 运行时(running (active)) 执行上下文。当被调用一方结束其运行时,它将控制权交回调用一方,然后对调用一方的上线文的求值会继续执行(可能激活其它上下文)直到其运行结束。被调用者会简单的进行 返回(return) 或以 异常(exception) 退出。被抛出的异常如果没有捕获,将会终止/退出(从栈中弹出)一个或多个上下文。
如下所示:所有的ECMAScript 程序运行时(program runtime) 可以表示为 执行上下文栈(execution context (EC) stack) ,其栈顶是当前被激活的上下文:
图 4. 执行上下文栈
程序开始运行时,会先进入栈底的(栈的第一个元素) 全局执行上下文(global execution context) ,然后全局代码进行初始化,创建所需的对象与方法。在全局上下文的执行过程中,代码会激活其它(已经创建)函数并进入函数各自的执行上下文,同时会向栈中压入新的元素。初始化完成后,运行时系统会等待事件的注入(比如用户鼠标点击),事件会激活函数并且进入新的执行上下文。
见图5,有一个函数上下文“EC1″和一个全局上下文“Global EC”,下图展现了从“Global EC”进入和退出“EC1″时栈的变化:
图 5. 执行上下文栈的变化
ECMAScript运行时系统就是这样管理代码的执行。
关于ECMAScript执行上下文栈的内容请查阅 第1章. 执行上下文(Chapter 1. Execution context)。
如上所述,栈中每一个执行上下文可以表示为一个对象。让我们看看上下文对象的结构以及执行其代码所需的 状态(state) 。
执行上下文可以抽象为一个简单的对象。每个上下文包含一系列属性(我们称之为 上下文状态(context’s state) ) 用以跟踪相关代码的执行过程。下图展示了上下文的结构:
图 6. 上下文结构
除了这3个所需要的属性(变量对象(variable object),this指针(this value),作用域链(scope chain) ),执行上下文根据具体实现还可以具有任意额外属性。
接着,让我们仔细说明上下文的重要属性。
变量对象(variable object) 是与执行上下文相关的 数据作用域(scope of data) 。它是与上下文关联的特殊对象,用于存储被定义在上下文中的 变量(variables) 和 函数声明(function declarations) 。
请注意,变量对象不包含 函数表达式(function expressions) (与 函数声明(function declarations) 比较)。
变量对象是一个抽象的概念。在不同的上下文中,它以不同的对象[译注:意思应为对象集合]来表示。举例来说,全局上下文中,变量对象是 全局对象本身(global object itself) (这就是我们能够通过属性名称引用全局对象的全局变量)。
让我们看看下面的例子:
03 |
function bar() {} // 函数声明 |
04 |
( function baz() {}); // 函数表达式 |
07 |
this .foo == foo, // true |
08 |
window .bar == bar // true |
11 |
console. log (baz); // 引用错误,baz没有被定义 |
全局上下文中的变量对象(VO)会有如下属性:
图 7. 全局变量对象
如上所示,函数“baz”如果作为函数表达式则不被不被包含于变量对象。这就是在函数外部尝试访问产生 引应用错误(ReferenceError) 的原因。
请注意,ECMAScript和其他语言相比(比如C/C++),仅有函数能够创建新的作用域。在函数内部定义的变量与内部函数,在外部非直接可见并且不污染全局对象。
使用 eval 的时候,我们同样会使用一个新的(eval创建)执行上下文。eval会使用全局变量对象或调用者的变量对象(eval的调用来源)。
那函数以及自身的变量对象又是怎样的呢?在一个函数上下文中,变量对象被表示为 激活对象(activation object)。
当函数被调用者激活,一个特殊的对象——激活对象(activation object) 将被创建。它包含 形式参数(formal parameters) 与特殊 参数(arguments) 对象(具有索引属性的形参键值表(map)[译注:表现为数组])。激活对象在函数上下文中作为变量对象使用。
即:函数的变量对象保持不变,但除去存储变量与函数声明之外,还包含形参以及特殊对象arguments 。
考虑下面的例子