OOP—ECMAScript实现详解

时间:2022-10-21 23:04:22

我们将从最基本的数据类型来分析,首先要了解的是ECMAScript用原始值( primitive values) 和对象

( objects) 来区分实体, 因此有些文章里说的“在JavaScript里, 一切都是对象”是错误的( 不完全对) , 原

始值就是我们这里要讨论的一些数据类型。

数据类型

大家都知道ECMAScript是可以动态转换类型的动态弱类型语言,即便如此,它还是有数据类型的。在标准中定义了9种数据类型,但只有6种是我们可以直接在ECMAScript程序里访问的。分别是:Number、String、Boolean、Object、Null、Undefined。

另外三种只能在实现级别访问:Reference、List、Completion。其中,Reference是用来解释delete、 typeof、 this这样的操作符, 并且包含一个基对象和一个属性名称;List描述的是参数列表的行为( 在new表达式和函数调用的时候) ;Completion是用来解释行为break、continue、 return和throw语句的。

原始值类型

在前面我们提到过的6种克制在ECMAScript程序中直接访问的数据类型中,有5种是原始值类型,分别是:

Number、String、Boolean、Null、Undefined。原始值类型例子:

var a = 10;

var b = 'string';

var c = true;

var d = null;

var e = undefined;

这些值是底层直接实现的,不是Object。它们没有构造函数,没有原型。

注:这些原始值和我们平时用的(Boolean、 String、 Number、 Object)虽然名字上相似, 但不是同一个

东西。 所以typeof(true)和typeof(Boolean)结果是不一样的, 因为typeof(Boolean)的结果是"function", 所以

函数Boolean、 String、 Number是有原型的 。

至于 typeof(null)返回"Object",规范中并没有作很多的解释,只是如此规定:对于Null值的typeof字符串返回值返回"Object"。

规范没有想解释这个, 但是Brendan Eich (JavaScript发明人)注意到null相对于undefined大多数都是用

于对象出现的地方, 例如设置一个对象为空引用。 但是有些文档里有些气人将之归结为bug, 而且将该bug

放在Brendan Eich也参与讨论的bug列表里, 结果就是任其自然, 还是把typeof null的结果设置为

object( 尽管262-3的标准是定义null的类型是Null, 262-5已经将标准修改为null的类型是object了)。

Object类型

Object类型是描述ECMAScript对象唯一一个数据类型。(不要和Object构造器混淆了,这里只讨论抽象类型) 那么什么是对象呢: Object is an unordered collection of key-value pairs.(对象是一个包含 key-value 对的无序集合)。其中对象的key称之为属性,属性是原始值和其他对象的容器。若函数的属性为一个函数我们则称之为方法。例如:

var obj = {

a:1,

b:{b1:false},

c:function(){

console.log('oop')

}

}

动态性

ECMAScript中对象是完全动态的,这意味着在程序执行期间,我们可以任意的添加、修改、删除对象的属性。例如:

var obj = {a:1};

//添加属性

obj.b = {b1:true};

//修改属性

obj.a = 'test';

//删除属性

delete obj.a;

有些属性不能被修改——( 只读属性、 已删除属性或不可配置的属性) 。 我们将稍后在属性特性里讲解。

另外, ECMAScript5规范规定, 静态对象不能扩展新的属性, 并且它的属性页不能删除或者修改。 他们是所谓的冻

结对象, 可以通过应用Object.freeze(o)方法得到。

var foo = {x:6};

//冻结foo

Object.freeze(foo);

console.log(Object.isFrozen(foo));//true

//不能修改

foo.x = 200;

console.log(foo.x);//

//不能添加

foo.y = false;

console.log(foo.y);//undefined

//不能删除

delete foo.x;//false

在ECMAScript5规范里, 也使用Object.preventExtensions(o)方法防止扩展, 或者使用Object.defineProperty(o)方

法来定义属性:

内置对象、原生对象宿主对象

有必要注意的是。规范区分了 内置对象、 元素对象及宿主对象。

内置对象和元素对象是被ECMAScript规范定义和实现的, 两者之间的差异微不足道。 所有ECMAScript实

现的对象都是原生对象( 其中一些是内置对象、 一些在程序执行的时候创建, 例如用户自定义对象) 。 内

置对象是原生对象的一个子集、 是在程序开始之前内置到ECMAScript里的( 例如, parseInt, Match等) 。

所有的宿主对象是由宿主环境提供的, 通常是浏览器, 并可能包括如window、 alert等。

注意, 宿主对象可能是ECMAScript自身实现的, 完全符合规范的语义。 从这点来说, 他们能称为“原生宿主”对象( 尽快很理论) , 不过规范没有定义“原生宿主”对象的概念。

var foo = {x : 10};

Object.defineProperty(foo, "y", {

value: 20,

writable: false, // 只读

configurable: false // 不可配置

});

// 不能修改

foo.y = 200;

// 不能删除

delete foo.y; // false

// 防治扩展

Object.preventExtensions(foo);

console.log(Object.isExtensible(foo)); // false

// 不能添加新属性

foo.z = 30;

console.log(foo); //{x: 10, y: 20}

Boolean、Number and String

此外,规范还定义了一些原生的特殊包装类:布尔对象、数字对象和字符串对象。

这些对象的创建, 是通过相应的内置构造器创建, 并且包含原生值作为其内部属性, 这些对象可以转换为

原始值, 反之亦然。

var c = new Boolean(true);

var d = new String('test');

var e = new Number(10);

// 转换成原始值

// 使用不带new关键字的函数

с = Boolean(c);

d = String(d);

e = Number(e);

// 重新转换成对象

с = Object(c);

d = Object(d);

e = Object(e);

此外, 也有对象是由特殊的内置构造函数创建: Function( 函数对象构造器) 、 Array( 数组构造器)

RegExp( 正则表达式构造器) 、 Math( 数学模块) 、 Date( 日期的构造器) 等等, 这些对象也是Object

对象类型的值, 他们彼此的区别是由内部属性管理的。

字面量Literal

对于三个对象的值:对象( object) ,数组( array) 和正则表达式( regular expression) , 他们分别有简写

的标示符称为:对象初始化器、 数组初始化器、 和正则表达式初始化器:

// 等价于new Array(1, 2, 3);

// 或者array = new Array();

// array[0] = 1;

// array[1] = 2;

// array[2] = 3;

var array = [1, 2, 3];

// 等价于

// var object = new Object();

// object.a = 1;

// object.b = 2;

// object.c = 3;

var object = {a: 1, b: 2, c: 3};

// 等价于new RegExp("^\d+$", "g")

var re = /^\d+$/g;

正则表达式字面量和RegExp对象

在第三版的规范里,正则表达式字面量和RegExp对象有如下问题:

RegExp字面量只在一句里存在, 并且再解析阶段创建, 但RegExp构造器创建的却是新对象, 所以这可能会导致出一些问题, 如lastIndex的值

在测试的时候结果是错误的:

for (var k = 0; k < 4; k++) {

var re = /ecma/g;

alert(re.lastIndex); // 0, 4, 0, 4

alert(re.test("ecmascript")); // true, false, true, false

} //

对比

for (var k = 0; k < 4; k++) {

var re = new RegExp("ecma", "g");

alert(re.lastIndex); // 0, 0, 0, 0

alert(re.test("ecmascript")); // true, true, true, true

}

注意:此问题在262-5中得到修正,不管是字面量还是通过RegExp构造器形式,都会创建新对象。

另外, ECMAScript5标准可以让我们创建没原型的对象( 使用Object.create(null)方法实现) 对, 从这个角度来

说, 这样的对象可以称之为哈希表:

var aHashTable = Object.create(null);

console.log(aHashTable.toString); // 未定义

对象转换

将对象转化成原始值可以用valueOf方法, 正如我们所说的, 当函数的构造函数调用做为function( 对于某

些类型的) , 但如果不用new关键字就是将对象转化成原始值, 就相当于隐式的valueOf方法调用:

var a = new Number(1);

var primitiveA = Number(a); // 隐式"valueOf"调用

var alsoPrimitiveA = a.valueOf(); // 显式调用

alert([

typeof a, // "object"

typeof primitiveA, // "number"

typeof alsoPrimitiveA // "number"

]);

这种方式允许对象参与各种操作, 例如:

var a = new Number(1);

var b = new Number(2);

alert(a + b); //

// 甚至

var c = {

x: 10,

y: 20,

valueOf: function () {

return this.x + this.y;

}

};

var d = {

x: 30,

y: 40,

// 和c的valueOf功能一样

valueOf: c.valueOf

};

alert(c + d); //

valueOf的默认值会根据根据对象的类型改变( 如果不被覆盖的话) , 对某些对象, 他返回的是this——例

如:Object.prototype.valueOf(), 还有计算型的值:Date.prototype.valueOf()返回的是日期时间:

var a = {};

alert(a.valueOf() === a); // true, "valueOf"返回this

var d = new Date();

alert(d.valueOf()); //current  time

alert(d.valueOf() === d.getTime()); // true

此外,对象还有一个更原始的代表性——字符串展示。 这个toString方法是可靠的, 它在某些操作上是自动

使用的:

var a = {

valueOf: function () {

return 100;

},

toString: function () {

return 'test';

}

};

// 这个操作里, toString方法自动调用

alert(a); // "test"

// 但是这里, 调用的却是valueOf()方法

alert(a + 10); // 110

// 但, 一旦valueOf删除以后

// toString又可以自动调用了

delete a.valueOf;

alert(a + 10); // "test10"

Object.prototype上定义的toString方法具有特殊意义, 它返回的我们下面将要讨论的内部[[Class]]属性值。

和转化成原始值( ToPrimitive) 相比, 将值转化成对象类型也有一个转化规范( ToObject) 。

一个显式方法是使用内置的Object构造函数作为function来调用ToObject( 有些类似通过new关键字也可

以) :

var n = Object(1); // [object Number]

var s = Object('test'); // [object String]

// 一些类似, 使用new操作符也可以

var b = new Object(true); // [object Boolean]

// 应用参数new Object的话创建的是简单对象

var o = new Object(); // [object Object]

// 如果参数是一个现有的对象

// 那创建的结果就是简单返回该对象

var a = [];

alert(a === new Object(a)); // true

alert(a === Object(a)); // true

关于调用内置构造函数, 适用还是不适用new操作符没有通用规则, 取决于构造函数。 例如Array或

Function当使用new操作符的构造函数或者不使用new操作符的简单函数使用产生相同的结果的:

var a = Array(1, 2, 3); // [object Array]

var b = new Array(1, 2, 3); // [object Array]

var c = [1, 2, 3]; // [object Array]

var d = Function(''); // [object Function]

var e = new Function(''); // [object Function]

属性的特性

所有的属性( property) 都可以有很多特性( attributes) 。

1. {Writable}——是否忽略向属性赋值的写操作尝, 但只读属性可以由宿主环境行为改变——也就是说不

是“恒定值” ;

2. {Enumerable}——设置属性是否能被for..in循环枚举

3. {Configurable}— 是否忽略delete操作符的行为( 即删不掉) ;

4. {Internal}——内部属性, 没有名字( 仅在实现层面使用) , ECMAScript里无法访问这样的属性。

内部属性和方法

对象也可以有内部属性( 实现层面的一部分) , 并且ECMAScript程序无法直接访问( 但是下面我们将看

到, 一些实现允许访问一些这样的属性) 。 这些属性通过嵌套的中括号[[ ]]进行访问。 我们来看其中的一

些, 这些属性的描述可以到规范里查阅到。

每个对象都应该实现如下内部属性和方法:

1. [[Prototype]]——对象的原型( 将在下面详细介绍)

2. [[Class]]——字符串对象的一种表示( 例如, Object Array , Function Object, Function等) ;用来区

分对象

3. [[Get]]——获得属性值的方法

4. [[Put]]——设置属性值的方法

5. [[CanPut]]——检查属性是否可写

6. [[HasProperty]]——检查对象是否已经拥有该属性

7. [[Delete]]——从对象删除该属性

8. [[DefaultValue]]返回对象对应的原始值( 调用valueOf方法, 某些对象可能会抛出TypeError异常) 。

通过Object.prototype.toString()方法可以间接得到内部属性[[Class]]的值, 该方法应该返回下列字符串: "

[object " + [[Class]] + "]" 。 例如:

var getClass = Object.prototype.toString;

getClass.call({}); // [object Object]

getClass.call([]); // [object Array]

getClass.call(new Number(1)); // [object Number]

// 等等

构造函数

在ECMAScript中的对象是通过所谓的构造函数来创建的。

Constructor is a function that creates and initializes the newly created object.(构造函数是一个函数, 用来创建并初始化新创建的对象。)

对象创建( 内存分配) 是由构造函数的内部方法[[Construct]]负责的。 该内部方法的行为是定义好的, 所有

的构造函数都是使用该方法来为新对象分配内存的。

而初始化是通过新建对象上下上调用该函数来管理的, 这是由构造函数的内部方法[[Call]]来负责任的。

注意, 用户代码只能在初始化阶段访问, 虽然在初始化阶段我们可以返回不同的对象( 忽略第一阶段创建

的this对象) :

function A() {

// 更新新创建的对象

this.x = 10;

// 但返回的是不同的对象

return [1, 2, 3];

} v

ar a = new A();

console.log(a.x, a); undefined, [1, 2, 3]

对象创建的算法

内部方法[[Construct]] 的行为可以描述成如下:

F.[Construct]:

O = new NativeObject();

// 属性[[Class]]被设置为"Object"

O.[[Class]] = "Object"

// 引用F.prototype的时候获取该对象g

var objectPrototype = F.prototype;

// 如果objectPrototype是对象, 就:

O.[[Prototype]] = __objectPrototype

// 否则:

O.[[Prototype]] = Object.prototype;

// 这里O.[[Prototype]]是Object对象的原型

// 新创建对象初始化的时候应用了F.[[Call]]

// 将this设置为新创建的对象O

// 参数和F里的initialParameters是一样的

R = F.[Call]; this === O;

// 这里R是[[Call]]的返回值

// 在JS里看, 像这样:

// R = F.apply(O, initialParameters);

// 如果R是对象

return R

// 否则

return O

请注意两个主要特点:

1. 首先, 新创建对象的原型是从当前时刻函数的prototype属性获取的( 这意味着同一个构造函数创建的

两个对象的原型可以不同,因为函数的prototype属性可以不同) 。

2. 其次, 正如我们上面提到的, 如果在对象初始化的时候, [[Call]]返回的是对象, 这恰恰是用于整个new

操作符的结果:

function A() {}

A.prototype.x = 10;

var a = new A(); alert(a.x); // 10 – 从原型上得到

// 设置.prototype属性为新对象

// 为什么显式声明.constructor属性将在下面说明

A.prototype = { constructor: A, y: 100 };

var b = new A(); // 对象"b"有了新属性 alert(b.x);

// undefined alert(b.y);

// 100 – 从原型上得到

// 但a对象的原型依然可以得到原来的结果 alert(a.x); // 10 - 从原型上得到 function B() { this.x = 10; return new

Array(); }

// 如果"B"构造函数没有返回( 或返回this) // 那么this对象就可以使用, 但是下面的情况返回的是array var

对象创建的算法

b = new B(); alert(b.x); // undefined alert(Object.prototype.toString.call(b)); // [object Array]

让我们来详细了解一下原型

原型

每个对象都有一个原型( 一些系统对象除外) 。 原型通信是通过内部的、 隐式的、 不可直接访问

[[Prototype]]原型属性来进行的, 原型可以是一个对象, 也可以是null值。

instanceof操作符的特性

我们是通过构造函数的prototype属性来显示引用原型的,这和instanceof操作符有关。该操作符是和原型链一起工作的,而不是构造函数,考虑到这一点,当检测对象的时候往往会有误解:

if (foo instanceof Foo) {
...
}

这不是用来检测对象foo是否是用Foo构造函数创建的,所有instanceof运算符只需要一个对象属性——foo.[[Prototype]],在原型链中从Foo.prototype开始检查其是否存在。instanceof运算符是通过构造函数里的内部方法[[HasInstance]]来激活的。

让我们来看看这个例子:

function A() {}
A.prototype.x = 10; var a = new A();
alert(a.x); // 10 alert(a instanceof A); // true // 如果设置原型为null
A.prototype = null; // ..."a"依然可以通过a.[[Prototype]]访问原型
alert(a.x); // 10 // 不过,instanceof操作符不能再正常使用了
// 因为它是从构造函数的prototype属性来实现的
alert(a instanceof A); // 错误,A.prototype不是对象

另一方面,可以由构造函数来创建对象,但如果对象的[[Prototype]]属性和构造函数的prototype属性的值设置的是一样的话,instanceof检查的时候会返回true:

function B() {}
var b = new B(); alert(b instanceof B); // true function C() {} var __proto = {
constructor: C
}; C.prototype = __proto;
b.__proto__ = __proto; alert(b instanceof C); // true
alert(b instanceof B); // false

原型可以存放方法并共享属性

大部分程序里使用原型是用来存储对象的方法、默认状态和共享对象的属性。

事实上,对象可以拥有自己的状态 ,但方法通常是一样的。 因此,为了内存优化,方法通常是在原型里定义的。 这意味着,这个构造函数创建的所有实例都可以共享找个方法。

function A(x) {
this.x = x || 100;
} A.prototype = (function () { // 初始化上下文
// 使用额外的对象 var _someSharedVar = 500; function _someHelper() {
alert('internal helper: ' + _someSharedVar);
} function method1() {
alert('method1: ' + this.x);
} function method2() {
alert('method2: ' + this.x);
_someHelper();
} // 原型自身
return {
constructor: A,
method1: method1,
method2: method2
}; })(); var a = new A(10);
var b = new A(20); a.method1(); // method1: 10
a.method2(); // method2: 10, internal helper: 500 b.method1(); // method1: 20
b.method2(); // method2: 20, internal helper: 500 // 2个对象使用的是原型里相同的方法
alert(a.method1 === b.method1); // true
alert(a.method2 === b.method2); // true

读写属性

正如我们提到,读取和写入属性值是通过内部的[[Get]]和[[Put]]方法。这些内部方法是通过属性访问器激活的:点标记法或者索引标记法:

// 写入
foo.bar = 10; // 调用了[[Put]] console.log(foo.bar); // 10, 调用了[[Get]]
console.log(foo['bar']); // 效果一样

下面,我们来看看伪代码实现:

[[Get]]方法

[Get]]也会从原型链中查询属性,所以通过对象也可以访问原型中的属性。

O.[[Get]](P):

// 如果是自己的属性,就返回
if (O.hasOwnProperty(P)) {
return O.P;
} // 否则,继续分析原型
var __proto = O.[[Prototype]]; // 如果原型是null,返回undefined
// 这是可能的:最顶层Object.prototype.[[Prototype]]是null
if (__proto === null) {
return undefined;
} // 否则,对原型链递归调用[[Get]],在各层的原型中查找属性
// 直到原型为null
return __proto.[[Get]](P)

请注意,因为[[Get]]在如下情况也会返回undefined:

if (window.someObject) {
...
}

这里,在window里没有找到someObject属性,然后会在原型里找,原型的原型里找,以此类推,如果都找不到,按照定义就返回undefined。

注意:in操作符也可以负责查找属性(也会查找原型链):

if ('someObject' in window) {
...
}

这有助于避免一些特殊问题:比如即便someObject存在,在someObject等于false的时候,第一轮检测就通不过。

[[PUT]]方法

[[Put]]方法可以创建、更新对象自身的属性,并且掩盖原型里的同名属性。

O.[[Put]](P, V):

// 如果不能给属性写值,就退出
if (!O.[[CanPut]](P)) {
return;
} // 如果对象没有自身的属性,就创建它
// 所有的attributes特性都是false
if (!O.hasOwnProperty(P)) {
createNewProperty(O, P, attributes: {
ReadOnly: false,
DontEnum: false,
DontDelete: false,
Internal: false
});
} // 如果属性存在就设置值,但不改变attributes特性
O.P = V return;

例如:

Object.prototype.x = 100;

var foo = {};
console.log(foo.x); // 100, 继承属性 foo.x = 10; // [[Put]]
console.log(foo.x); // 10, 自身属性 delete foo.x;
console.log(foo.x); // 重新是100,继承属性

请注意,不能掩盖原型里的只读属性,赋值结果将忽略,这是由内部方法[[CanPut]]控制的。

// 例如,属性length是只读的,我们来掩盖一下length试试

function SuperString() {
/* nothing */
} SuperString.prototype = new String("abc"); var foo = new SuperString(); console.log(foo.length); // 3, "abc"的长度 // 尝试掩盖
foo.length = 5;
console.log(foo.length); // 依然是3

在ECMAScript5的严格模式下,如果掩盖只读属性的话,会保存TypeError错误。

属性访问器

内部方法[[Get]]和[[Put]]在ECMAScript里是通过点符号或者索引法来激活的,如果属性标示符是合法的名字的话,可以通过“.”来访问,而索引方运行动态定义名称。

var a = {testProperty: 10};

alert(a.testProperty); // 10, 点
alert(a['testProperty']); // 10, 索引 var propertyName = 'Property';
alert(a['test' + propertyName]); // 10, 动态属性通过索引的方式

这里有一个非常重要的特性——属性访问器总是使用ToObject规范来对待“.”左边的值。这种隐式转化和这句“在JavaScript中一切都是对象”有关系,(然而,当我们已经知道了,JavaScript里不是所有的值都是对象)。

如果对原始值进行属性访问器取值,访问之前会先对原始值进行对象包装(包括原始值),然后通过包装的对象进行访问属性,属性访问以后,包装对象就会被删除。

例如:

var a = 10; // 原始值

// 但是可以访问方法(就像对象一样)
alert(a.toString()); // "10" // 此外,我们可以在a上创建一个心属性
a.test = 100; // 好像是没问题的 // 但,[[Get]]方法没有返回该属性的值,返回的却是undefined
alert(a.test); // undefined

那么,为什么整个例子里的原始值可以访问toString方法,而不能访问新创建的test属性呢?

答案很简单:

首先,正如我们所说,使用属性访问器以后,它已经不是原始值了,而是一个包装过的中间对象(整个例子是使用new Number(a)),而toString方法这时候是通过原型链查找到的:

// 执行a.toString()的原理:

1. wrapper = new Number(a);
2. wrapper.toString(); // "10"
3. delete wrapper;

接下来,[[Put]]方法创建新属性时候,也是通过包装装的对象进行的:

// 执行a.test = 100的原理:

1. wrapper = new Number(a);
2. wrapper.test = 100;
3. delete wrapper;

我们看到,在第3步的时候,包装的对象以及删除了,随着新创建的属性页被删除了——删除包装对象本身。

然后使用[[Get]]获取test值的时候,再一次创建了包装对象,但这时候包装的对象已经没有test属性了,所以返回的是undefined:

// 执行a.test的原理:

1. wrapper = new Number(a);
2. wrapper.test; // undefined

这种方式解释了原始值的读取方式,另外,任何原始值如果经常用在访问属性的话,时间效率考虑,都是直接用一个对象替代它;与此相反,如果不经常访问,或者只是用于计算的话,到可以保留这种形式。

继承

我们知道,ECMAScript是使用基于原型的委托式继承。链和原型在原型链里已经提到过了。其实,所有委托的实现和原型链的查找分析都浓缩到[[Get]]方法了。

如果你完全理解[[Get]]方法,那JavaScript中的继承这个问题将不解自答了。

经常在论坛上谈论JavaScript中的继承时,我都是用一行代码来展示,事实上,我们不需要创建任何对象或函数,因为该语言已经是基于继承的了,代码如下:

alert(1..toString()); // "1"

我们已经知道了[[Get]]方法和属性访问器的原理了,我们来看看都发生了什么:

  1. 首先,从原始值1,通过new Number(1)创建包装对象
  2. 然后toString方法是从这个包装对象上继承得到的

为什么是继承的? 因为在ECMAScript中的对象可以有自己的属性,包装对象在这种情况下没有toString方法。 因此它是从原理里继承的,即Number.prototype。

注意有个微妙的地方,在上面的例子中的两个点不是一个错误。第一点是代表小数部分,第二个才是一个属性访问器:

1.toString(); // 语法错误!

(1).toString(); // OK

1..toString(); // OK

1['toString'](); // OK

原型链

让我们展示如何为用户定义对象创建原型链,非常简单:

function A() {
alert('A.[[Call]] activated');
this.x = 10;
}
A.prototype.y = 20; var a = new A();
alert([a.x, a.y]); // 10 (自身), 20 (继承) function B() {} // 最近的原型链方式就是设置对象的原型为另外一个新对象
B.prototype = new A(); // 修复原型的constructor属性,否则的话是A了
B.prototype.constructor = B; var b = new B();
alert([b.x, b.y]); // 10, 20, 2个都是继承的 // [[Get]] b.x:
// b.x (no) -->
// b.[[Prototype]].x (yes) - 10 // [[Get]] b.y
// b.y (no) -->
// b.[[Prototype]].y (no) -->
// b.[[Prototype]].[[Prototype]].y (yes) - 20 // where b.[[Prototype]] === B.prototype,
// and b.[[Prototype]].[[Prototype]] === A.prototype

这种方法有两个特性:

首先,B.prototype将包含x属性。乍一看这可能不对,你可能会想x属性是在A里定义的并且B构造函数也是这样期望的。尽管原型继承正常情况是没问题的,但B构造函数有时候可能不需要x属性,与基于class的继承相比,所有的属性都复制到后代子类里了。

尽管如此,如果有需要(模拟基于类的继承)将x属性赋给B构造函数创建的对象上,有一些方法,我们后来来展示其中一种方式。

其次,这不是一个特征而是缺点——子类原型创建的时候,构造函数的代码也执行了,我们可以看到消息"A.[[Call]] activated"显示了两次——当用A构造函数创建对象赋给B.prototype属性的时候,另外一场是a对象创建自身的时候!

下面的例子比较关键,在父类的构造函数抛出的异常:可能实际对象创建的时候需要检查吧,但很明显,同样的case,也就是就是使用这些父对象作为原型的时候就会出错。

function A(param) {
if (!param) {
throw 'Param required';
}
this.param = param;
}
A.prototype.x = 10; var a = new A(20);
alert([a.x, a.param]); // 10, 20 function B() {}
B.prototype = new A(); // Error

此外,在父类的构造函数有太多代码的话也是一种缺点。

解决这些“功能”和问题,程序员使用原型链的标准模式(下面展示),主要目的就是在中间包装构造函数的创建,这些包装构造函数的链里包含需要的原型。

function A() {
alert('A.[[Call]] activated');
this.x = 10;
}
A.prototype.y = 20; var a = new A();
alert([a.x, a.y]); // 10 (自身), 20 (集成) function B() {
// 或者使用A.apply(this, arguments)
B.superproto.constructor.apply(this, arguments);
} // 继承:通过空的中间构造函数将原型连在一起
var F = function () {};
F.prototype = A.prototype; // 引用
B.prototype = new F();
B.superproto = A.prototype; // 显示引用到另外一个原型上, "sugar" // 修复原型的constructor属性,否则的就是A了
B.prototype.constructor = B; var b = new B();
alert([b.x, b.y]); // 10 (自身), 20 (集成)

注意,我们在b实例上创建了自己的x属性,通过B.superproto.constructor调用父构造函数来引用新创建对象的上下文。

我们也修复了父构造函数在创建子原型的时候不需要的调用,此时,消息"A.[[Call]] activated"在需要的时候才会显示。

为了在原型链里重复相同的行为(中间构造函数创建,设置superproto,恢复原始构造函数),下面的模板可以封装成一个非常方面的工具函数,其目的是连接原型的时候不是根据构造函数的实际名称。

function inherit(child, parent) {
var F = function () {};
F.prototype = parent.prototype
child.prototype = new F();
child.prototype.constructor = child;
child.superproto = parent.prototype;
return child;
}

因此,继承:

function A() {}
A.prototype.x = 10; function B() {}
inherit(B, A); // 连接原型 var b = new B();
alert(b.x); // 10, 在A.prototype查找到

也有很多语法形式(包装而成),但所有的语法行都是为了减少上述代码里的行为。

例如,如果我们把中间的构造函数放到外面,就可以优化前面的代码(因此,只有一个函数被创建),然后重用它:

var inherit = (function(){
function F() {}
return function (child, parent) {
F.prototype = parent.prototype;
child.prototype = new F;
child.prototype.constructor = child;
child.superproto = parent.prototype;
return child;
};
})();

由于对象的真实原型是[[Prototype]]属性,这意味着F.prototype可以很容易修改和重用,因为通过new F创建的child.prototype可以从child.prototype的当前值里获取[[Prototype]]:

function A() {}
A.prototype.x = 10; function B() {}
inherit(B, A); B.prototype.y = 20; B.prototype.foo = function () {
alert("B#foo");
}; var b = new B();
alert(b.x); // 10, 在A.prototype里查到 function C() {}
inherit(C, B); // 使用"superproto"语法糖
// 调用父原型的同名方法 C.ptototype.foo = function () {
C.superproto.foo.call(this);
alert("C#foo");
}; var c = new C();
alert([c.x, c.y]); // 10, 20 c.foo(); // B#foo, C#foo

此文章大部分内容来自汤姆大叔的深入理解Javascript系列,链接:http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html,本文在原文的基础上做了一些勘正,剔除了一些冗余的内容。感谢原文作者!