读书人

深入显出 javascript prototype 继承

发布时间: 2013-03-26 09:54:34 作者: rapoo

深入浅出 javascript prototype 继承
最近又重温了 js 的继承关系, 看了几篇帖子, 终于被我悟道了, 在此分享一下.

如果您没有耐心阅读完本文章, 请直接跳转至最后一段, 复制代码.

http://www.51testing.com/html/90/n-814890-2.html
推荐大家先看看这篇帖子, 等云里雾里的时候再看我下面的解释就豁然开朗了.

关键词: function, prototype, __proto__

javascript 对象是原型继承的, 当访问实例对象的某个属性而没有找到的时候, js 引擎会追溯这个对象的 prototype 链条, 尝试从超类, 超类的超类... 找到想要的属性; 如果失败返回 undefined, 成功则返回超类 prototype 对象的属性引用. 需要额外注意的是, 这个属性是只读的, "实例"是不可能改变"原型"中的属性的. 这段话相信大家都耳熟能详, 但它却是暗藏玄机, 需要细细的去解读一番.

1 prototype 只有类才具备, 实例类型是不具备的, 例如:
function First() {
}
var f = new First();
console.log(f.prototype);
会输出 undefined, 怎么样是不是同你的直觉相悖呢? 如果你打开 chrome 浏览器的调试功能, 你会发现 f 这个"实例"就没有 prototype 这个属性, 只有 __proto__ . 但这完全不矛盾, 定义只说:属性查找的时候会追溯原型链, 但没有说 prototype 是对象自身的. 它只属于对象的"类".

2 __proto__ 只属于实例, 或者说当一个对象可以作为"实例"时就具有了 __proto__ 例如
function First() {
}
启用调试模式就发现 First 同时具有 prototype 和 __proto__, 这是因为, 如果定义
var f = new First();// First 作为类型使用, 所以它具有 prototype, 而 First 本身是函数类型 Function 的一个实例, 所以也具有 __proto__ 属性.

3 对象的建立顺序是解释原型继承的关键. 援引帖子:
// json 只是用来表示结构, 不是说"类"真的就是一个jsonvar someclass = { prototype : { constuctor : function used when init newly created object. __proto__ : point to parent }}

上面的三个步骤转化为伪码就是:

function A() {}var a = {};// empty object [1]a.__proto__ = A.prototype;// [2]A.call(a, arguments);//[3]

由于 js 的继承是实例继承, 所以 A.prototype 对象本身也是一个实例, 它自然具有 __proto__ 属性.

4 基于 1, 2, 3 我来解释下为什么继承的实现这么嗦:
function A() {     this.name = 'Jack';}function B() {}// 如果我们想让 B 继承 A 的属性 name, 那么只要var b = new B();b.__proto__ = A.prototype;

整件事情就极其简单的被搞定了. 可遗憾的是你不能这么做, __proto__ 这个指针只是 FF 和 chrome 浏览器实现提的, 在规范中它叫做 [[Prototype]], 是不可以被改变的, 所以以上代码只是 hack 行为, 自己想想就行了.

再次基于以上所有知识, 让我们想法来绕过去
function B() {}var b = new B();

这时 b 对象的结构是:
b:{    __proto__: {        constructor: function B() {...}        __proto__: Object    }}

b 的超类是 Object, 而我们需要的结构是:
b:{    __proto__: {        constructor: function B() {...}        __proto__: A    }}


有牛人(sorry 名字没记住)在 2006 就帮我们把这个死脑细胞的问题搞定了, 让我们一起来看看吧:
function A() {}function B() {}function F() {}F.prototype = A.prototype;var f = new F();B.prototype = f;B.prototype.constructor = B;


详细解释:
function A() {}function F() {}// 清晰起见, 我们设置var APROTO = A.prototypeF.prototype = A.prototype;// F.prototype = APROTO;var f = new F();// f.__proto__ = F.prototype = APROTO;function B() {}B.prototype = f;// js 默认会为每个类型创建一个 prototype 对象, // 当然我们也可以指定 prototype 对象, 像这样 B.prototype = f, // 则有, B.prototype = f = {__proto__ : APROTO} 这里尤其关键, 因为它折腾出了一个 __proto__ 指向 A 的对象 f.var b = new B();/* 根据实例建立的三个步骤 b.__proto__ = B.prototype = {__proto__ : APROTO}; b 的结构为:b:{    __proto__: {        __proto__: APROTO    }}而之前的目标结构为:b:{    __proto__: {        constructor: function B() {...}        __proto__: A    }}比较发现, 只要设置B.prototyp.constructor = B;目标和结果就完全一致了, 就实现了完美的继承.*/


5 更多细节:
// 清晰起见我们定义tool函数function Extends(superobj) {    function F() {    }    F.prototype = superobj;    return new F();}function A() {}// 如果要实现 B extends A, 则只要function B() {}B.prototype = Extends(A.prototype);B.prototype.constructor = B;var b = new B();// successful? 这里忽略了一件事情, 就是 A 的私有属性没有在 B 中被初始化, 也就是没有 A.call(b, arguments);// 让我们添加一点日志再次执行上面的代码function A() {    console.log('A.constructor');    this.name = 'Jack';}function Extends(superobj) {    function F() {        console.log('F.constructor');    }    F.prototype = superobj;    return new F();}function B() {    console.log('B.constructor');    this.company = 'top secret';}B.prototype = Extends(A.prototype);B.prototype.constructor = B;var b = new B();console.log(b.name);console.log(b.company);/*输出:F.constructorB.constructorundefinedtop secret  */// 这时继承结构正确, 但变量初始化不正确, 所以需要修改 Bfunction B() {    A.call(this, arguments);// <-------    console.log('B.constructor');    this.company = 'top secret';}// 再次执行var b = new B();console.log(b.name);console.log(b.company);/*输出:F.constructorA.constructorB.constructorJacktop secret   */


6 万事大吉. 让我们再来考虑如何把事情做的更优雅, 实际上在 ECMAScript 5th Edition 提供了函数 Object.create 能替代上面的 Extends. 基于这点, code 可以简化为:
function A() {};function B() {A.call(this, arguments)};B.prototype = Object.create(A.prototype);B.prototype.constructor = B;

Object.create 也支持额外的参数, 可以自行查阅相关资料.

以上所有痛苦是为了更优雅的解决问题, 而不单单只是为了解决问题. copy, paste 也要知其所以然.

读书人网 >JavaScript

热点推荐