本文内容和例子 参考自 《javascript高级程序设计》

之前写过一篇文章:

javascript的继承主要是依靠原型链实现的.

一、原型链

使用原型模式构建的自定义对象,原型属性原型方法是在存在于原型对象中,也就是 Object.prototype 里面.

因此,是时候又祭出这张图:(图片来自《javascript高级程序设计》)

原型1.jpg

可以看到,Person 的 prototype 指向的是 Person 的原型对象,而原型对象中的 constructor 又指向了 Person 的构造方法。

而实例中的 prototype (原型)也指向原型对象。

在学其他语言的时候,都了解过,所谓的继承,无非是把另一个的一些属性和方法,拿过来自己用, 有的可能需要修改一下(重载父类方法),有的可能直接用。

而javascript给出的方案就是:

  • 将一个对象的 prototype 属性指向另一个对象的原型对象

往往在实现上面的做法的时候,就是将当前对象的 prototype 赋值为另一个对象的实例,由于实例本身的prototype 已经指向了原型对象<因此,当前对象的 prototype 也随即指向了另一个对象的原型对象,从而拥有了另一个对象的原型方法和原型属性/red>

多层继承(实现上面的方式)最终会形成一个链,称为原型链

原型链1.jpg

二、利用原型链实现继承

上面说了原型链的产生的核心思想,通过上述的方式,能够很方便的实现对象的继承。

一个简单的继承(使用原型链):

    function A(){
        this.name = "aaaptbird";
        this.age = 20;
    }
    // 为A指定sayName方法
    A.prototype.sayName = function(){
        console.log("A" + this.name);
    }
    // 对象B
    function B(){
        this.name="bbb";
    }
    // 将对象B的prototype赋值为A的实例 完成原型链
    B.prototype = new A();
    // 为B指定原型方法 sayBName
    B.prototype.sayBName = function(){
        console.log("B" + this.name)
    }
    // 实例化 B
    var bObj = new B();
    console.log(bObj.name); // bbb
    console.log(bObj.age); // 20
    bObj.sayName(); // Abbb
    bObj.sayBName(); // Bbbb

上面的继承中,对象B继承对象A:

  • 对象B有自己的 name 属性,姑且可以认为,[ 重写了A的 name 属性 ] (存在错误)
  • 对象B的 prototype 赋值为A的实例,完成了原型链
  • 对象B能够访问A的age属性,以及sayName方法,不过name会自动访问B的新name = "bbb"

上面第一点(重写A的name属性)是有问题的,当 console.log(bObj) 之后就会发现问题:

4-1.jpg

上图能够发现几个问题:

  1. 发现了两个name,分别是B和A的name属性,实际上B并没有重写A的name属性,只是通过实例属性屏蔽了原型属性
  2. A的实例属性B的原型属性和原型方法都在B的 prototype 中

    • sayBname 是B的原型方法
    • age 是A的实例属性
    • name="aaa" 是A的实例属性
    • 由此可见,在原型链中,A的实例属性和方法是放在B的原型对象中的
  3. A的原型方法 sayName() 是放在B的 prototype 的 prototype 中的

出现两个name并且最终都是 bbb 的原因:


在B对象中,创建了实例属性name,之后在继承的A的name属性是放在了原型对象中,

根据 javascript 的作用域链查找变量的方式,就首先在实例属性中查询(我个人理解为内层),如果查不到会去查原型对象(我个人理解为外层),因此会首先拿到B的实例属性name=bbb

  • 关键是作用域链

二、原型链继承的问题

1、引用类型变量共享问题

这个问题是原型模式的通病,如果需要保证数字等应用类型的唯一性并且需要变动,则原型链实现的继承也会存在该问题。

具体可以看 - http://www.ptbird.cn/javascript-prototype-extend.html 的详细内容

要解决原型链引起的引用类型变量共享的问题,可以使用借用构造函数来解决:

这里的借用构造函数在子类的构造函数里面,借调超类的构造函数

使用 call 或者 apply 能够在将来新创建对象的时候执行构造函数:

    // 对象B
    function B(){
        this.name="bbb";
        // 继承A
        A.call(this);
    }

B在继承A的时候,在B的构造函数中,通过 call 调用A的构造函数。

这样子在实例化B的时候,都会重新调用一次A的构造函数,所以每个B对象都有一个属于自己的 colors 副本。从而解决了引用对象共享的问题

2、无法向超类传递参数

这个问题也是原型模式的通病,通过原型模式(或者说纯粹的原型链)实现的继承,父类的一些属性和方法是固定的,就比如说上面的age和name,除非在子类中在重新声明一个实例属性和实例方法。

不过这样子做就失去了继承的意义。(某种程度上)

向超类传递参数依旧可以通过 借用构造函数

    function A(name){
        this.name = name;
    }
    function B(name,age){
        A.call(this,name);
        this.age = age;
    } 
    var b = new B("BBB",20);
    console.log(b.name);
    console.log(b.age); 
    console.log(b);

通过上面能够发现,通过借用构造函数,将B()的参数应用到 A.call() 上面。

3、借用构造函数存在的问题

借用构造函数,虽然解决了参数传递引用共享的问题,但是应用共享也包括了函数,因此无法进行函数复用,因此也不是完美的解决方案。

三、组合继承

要解决上面的问题,本质其实是解决 引用属性的共享问题原型方法的复用

需要把 借用构造函数 和 原型继承 的优点结合起来:

    function A(name){
        this.name = name;
        this.numbers = [1,2,3,4];
    }
    A.prototype.sayName = function(){
        console.log(this.name);
    }
    function B(name,age){
        A.call(this,name);
        this.age = age;
    } 
    // 将B的原型对象指向 A
    B.prototype = new A();
    // 构造方法指向 B
    B.prototype.construtor = B;
    var b1 = new B("BB1",20);
    var b2 = new B("BB2",21);
    b1.sayName(); // BB1
    b2.sayName(); // BB2
    b1.numbers.pop();
    console.log(b1.numbers); // 1,2,3
    console.log(b2.numbers); // 1,2,3,4

上面的代码能够发现:

  • 将属性部分通过 借用构造函数 来实现继承,这样 numbers 在 b1 b2 会有引用的副本。
  • 将方法放在 原型对象 中,从而能够实现对方法的复用。

目前组合继承是使用最多的继承方式,但是依旧存在缺陷。

组合进程的缺点:

使用组合继承的方式实现继承,是需要调用两次A(超类)的构造函数的

四、原型式继承和寄生继承

1、原型式继承

原型式继承本质上还是原型链,不过只是表面上是不存在构造函数的。

原型式继承是需要有一个对象作为基础的

原型式实际上就是通过已经存在的一个 Object 来创建另外一个 Object

// 最开始的原型式继承方式
function newObject(obj){
        function NewObj(){};
        NewObj.prototype = obj;
        return new NewObj();
    }

最早的原型式继承的思想是在一个函数内容,创建一个临时对象的构造函数,然后通过原型链将传入的对象作为构造函数的原型,返回临时对象的一个实例

其实本质上就是对 传入的对象进行一次浅复制

ECMAScript5 里面通过 Object.create() 规范了原型式继承:

var A = {
    name : "AAA",
    numbers : [1,2,3]
};
var b = Object.create(A);
console.log(b.numbers); // 1,2,3
b.numbers.pop();
console.log(A.numbers);// 1,2

原型式继承不是为了解决原型链或者组合继承的问题,只是一种不同的思路。

2、寄生式继承

寄生式继承是在原型式继承的基础上进行增强对象。比如增加实例方法等

    function newObject(obj){
        function NewObj(){};
        NewObj.prototype = obj;
        return new NewObj
    }
    function newObjectIncrease(obj){
        // 通过原型式继承得到一个tmpObj
        var tmpObj = newObject(obj);
        // 通过 tmpObj 增强对象
        tmpObj.sayName = function(){
            console.log('name is postbird');
        }
        return tmpObj;
    }

寄生式继承一个很大的问题就是 无法做到方法的复用 (和构造函数一样)

五、寄生组合式继承

寄生组合其实就是结合了寄生式继承组合继承两种方式。

能够做到,继承方法的时候,通过原型继承,而继承属性的时候通过构造函数继承,同时避免了调用两次超类的构造函数

一个完整的示例:

    // 原型式继承方案
    function newObject(obj){
        function NewObj(){};
        NewObj.prototype = obj;
        return new NewObj;
    }
    // 寄生组合继承方式
    function inheritPrototype(Sub,Super){
        // 通过原型式继承创建对象
        var tmpProto = newObject(Super.prototype);
        // 将tmpProto的构造方法指向Sub
        tmpProto.constructor = Sub;
        // 将Sub的原型指向tmpProto
        Sub.prototype = tmpProto;
    }
    // A 对象
    function A(name){
        this.name = name;
        this.numbers = [1,2,3];
    }
    // 为A指定sayName方法
    A.prototype.sayName = function(){
        console.log(this.name);
    }
    // 对象B
    function B(name,age){
        this.age = age;
        // 继承A 传递name参数
        A.call(this,name);
    }
    inheritPrototype(B,A);
    // 为对象B增强方法
    B.prototype.sayAge = function(){
        console.log(this.age);
    }
    var b1 = new B("B1",21);
    var b2 = new B("B2",22);
    b1.sayName(); // B1
    b2.sayName(); // B2
    b1.sayAge(); // 21
    b2.sayAge(); // 22
    b1.numbers.pop();
    console.log(b1.numbers); // 1,2
    console.log(b2.numbers); // 1,2,3

上面的函数中:

  • 第一个函数 function newObj(obj){} 实际上就是原型式继承。
  • 第二个函数 function inheritPrototype(Sub,Super){} 是寄生组合继承的关键所在:

通过上面的例子来说明一下:

  1. 在创建A对象之后,A对象有两个实例属性,name 和 numbers , 其中 numbers 是引用类型
  2. B对象是继承A对象的,也有自己的实例属性 age ,通过构造函数中的 A.call(this,name),将参数传递给了超类(A)。
  3. 按照组合继承的方式,我们需要指定B的 prototypenew A() ,也就是 B.prototype = new A() ,原来这里是需要再次调用A的构造函数用来继承A的原型对象的,同时需要将 B 的 constructor 指向 B 的构造函数
  4. 现在通过 function inheritPrototype(Sub,Super) 这个函数实现上面的过程:

    • 通过 newObject(obj) 将 A(Super) 的原型对象传递给 tmpProto ,此时tmpProto的原型对象和A的原型对象是一样的。 可以通过 console.log(tmpProto); 来比较(比较的时候需要将 inheritPrototype后面的代码注释掉
    • 之后,将 tmpProto 的构造方法赋值为 B(Sub)
    • 最后,将 Sub 的原型对象赋值为 tmpProto
  5. 通过 inheritPrototype 函数,最后能够将 B(Sub) 的原型对象赋值为 拥有 A(Super)的原型对象,但是构造方法是 B(Sub)的新的对象

我这部分搞了挺长时间才搞的清楚,上面和下面是个人的理解。

寄生式组合继承相比于组合继承中,去掉的代码部分是 B.prototype = new A(); B.prototype.constructor = B;

在这个过程中,通过寄生式继承(原型式继承),用一个新的对象 tmpProto , tmpProto 的原型对象就是A的原型对象,但是tmpProto的构造函数指向了B。

因此tmpProto是一个新的拥有A的原型对象,B的构造函数的对象。将B的原型对象赋值为tmpProto就是上面去掉的两行代码的作用。不过,省去了一次调用A的构造对象的麻烦.

当然,需要注意的是,在上面的过程中,增强B对象方法的操作B.prototype.sayAge = function(){}一定是要放在 inheritPrototype() 函数后面操作。否则当增强了B的方法 sayAage() 之后,会在 inheritPrototype() 中被覆盖掉。

inheritPrototype() 函数中很关键的一点是 var tmpProto = newObj(A.prototype); 传入的参数是 A.prototype 而不是 A , 因为只有这样子, B.prototype = tmpProto; 才能正确的继承A,传入A,继承的就不是A,而是 tmpProto.