日期:2014-05-16  浏览次数:20471 次

深入浅出 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 对象的建立顺序是解释原型继承的关键. 援引帖子:
引用

先了解下new运算符,如下:
var a1 = new A;
var a2 = new A;
  这是通过构造函数来创建对象的方式,那么创建对象为什么要这样创建而不是直接var a1 = {};呢?这就涉及new的具体步骤了,这里的new操作可以分成三步(以a1的创建为例):
  1、新建一个对象并赋值给变量a1:var a1 = {};
  2、把这个对象的[[Prototype]]属性指向函数A的原型对象:a1.[[Prototype]] = A.prototype
  3、调用函数A,同时把this指向1中创建的对象a1,对对象进行初始化:A.apply(a1,arguments)

按照上面所说, 对象建立是分为建立和初始化两个步骤的, 而初始化是通过 constructor 来完成的. 每个"类"类型就具有这样的结构
// json 只是用来表示结构, 不是说"类"真的就是一个json
var 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.prototype
F.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(supe