首页 > 技术文章 > JS 中的 继承

a-lonely-wolf 2016-06-21 23:44 原文

许多 OO 语言(面向对象语言)都支持两种继承方式:接口继承和实现继承。接口继承只继承方法名,而实现继承则继承实际的方法。

JS只支持实现继承,主要是依靠原型链来实现的。

1、原型链

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,我们让原型对象等于另一个类型的实例,
此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针,如此层层递进,就构成了实例与原型的链条。

一种基本的模式:

function SuperType(){
        this.prototype = true;
    }
    SuperType.prototype.getSuperValue = function(){
        return this.prototype;
    }
    function SubType(){
        this.subprototype = false;
    }
    SubType.prototype = new SuperType();
    SubType.prototype.getSubValue = function(){
        return this.subprototype;
    }

    var instance = new SubType();
    alert(instance.getSuperValue());  //true

继承是通过创建 SuperType 的实例,并将该实例赋给SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。
原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了,关系图 如下:
上面的例子来说,调用
instance.getSuperValue()会经历三个搜索步骤: 1)搜索实例; 2)搜索 SubType.prototype;
3)搜索 SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。


默认原型都会包含一个内部指针,指向 Object.prototype。这也正是所有自定义类型都会继承 toString()、valueOf()等默认方法的根本原因。
这是一个完整的原型路径:

 

原型和实例的关系

使用 instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。

alert(instance  instanceof  Object)   //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType) //true

可以说 instance 是 Object、 SuperType 或 SubType 中任何一个类型的实例。

使用 isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,

alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true

谨慎的定义方法

给原型添加方法的代码一定要放在替换原型的语句之后,因为这个是对原型的重写,如果顺序放错了,就出问题了

 

4. 原型链的问题

原型链虽然很强大,可以用它来实现继承,最主要的问题来自包含引用类型值的原型。

function SuperType(){
        this.colors = ["red", "blue", "green"];
    }
    function SubType(){
    }
    //继承了 SuperType
    SubType.prototype = new SuperType();

    var instance1 = new SubType();
    var instance2 = new SubType();

    instance1.colors.push("black");
    alert(instance1.colors); //"red,blue,green,black"
    alert(instance2.colors); //"red,blue,green,black"
当intance1做修改时,就影响了instance2

另一个问题:在创建子类型的实例时,不能向超类型的构造函数中传递参数。

2、借用构造函数实现继承

   function Person(name,age){
        this.color = ["red","yellow","blue"];
        this.name = name;
        this.age = age;
    }
    function Student(name,age){
    //继承了Person Person.apply(
this,arguments); Person.call(this,name,age); } var s1 = new Student("Linda",25); var s2 = new Student("John",32); s1.color.push("black"); alert(s1.color+s1.name); //"red","yellow","blue","black" alert(s2.color+s2.name); //"red","yellow","blue"

借用构造函数有一个很大的优势:  即可以在子类型构造函数中向超类型构造函数传递参数。
借用构造函数的问题: 方法都在构造函数中定义,因此函数复用就无从谈起了。

组合继承

指的是将原型链和借用构造函数的技术组合到一块,使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

function Person(name){
        this.name = name;    
        this.firends = ["A","B","C"];
    }
    Person.prototype.sayName = function(){
        alert(this.name);
    }
    function Student(name,age){
         Person.apply(this,[name]);
    }
    Student.prototype = new Person();
    Student.prototype.constructor = Student;
    Student.prototype.sayAge = function(){
        alert(this.age);
    }
    var s1 = new Student("Linda",25);
    var s2 = new Student("John",32);
    
    s1.sayName();    //"Linda"
    s1.firends.push("D");
    alert(s1.firends);        //"A","B","C","D"
    alert(s2.firends);        //"A","B","C"

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。

原型式继承(还是 引用类型的问题)

 在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。

function object(obj){
        function O(){};
        O.prototype = obj;
        return O;
    }

object()对传入其中的对象执行了一次浅复制。

function create(obj){
        function F(){};
        F.prototype = obj;
        return new F();
    }
    var person={
        name:"Linda",
        friends:["A","B","C"]
    }

    var p1 = create(person);
    alert(p1.friends);
    p1.friends.push("D");
    alert(p1.friends);
    var p2 = create(person);
    alert(p2.friends);

寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象

function create(obj){
        function F(){};
        F.prototype = obj;
        return new F();
    }    
    function createAnother(orignal){
        var clone = create(orignal);     //通过调用函数创建一个新对象
        clone.sayHi = function(){        //以某种方式来增强这个对象
            alert("sayHi");
        }
        return clone;                //返回这个对象
    }
    var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"],
        say : function(){
            alert(this.friends);
        }
    };
    var anotherPerson = createAnother(person);
    anotherPerson.sayHi(); //"hi"
    anotherPerson.say(); //"Nicholas"

createAnother()函数接收了一个参数(作为新对象的基础对象),把这个对象(original)传递给 create()函数,将返回的结果赋值给 clone。再为 clone 对象
添加一个新方法 sayHi(),最后返回 clone 对象。
基于 person 返回了一个新对象——anotherPerson。新对象不仅具有 person的所有属性和方法,而且还有自己的 sayHi()方法。
使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

寄生组合式继承

组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
找到了解决这个问题方法——寄生组合式继承。

寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法

不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型
的原型。寄生组合式继承的基本模式如下所示。

function create(obj){
        function F(){};
        F.prototype = obj;
        return new F();
    }
    function inheritPrototype(subType,superType){
        var clone = create(superType.prototype);       //创建对象
        clone.constructor = subType;                   //增强对象
        subType.prototype = clone;                     //指定对象
    }

inheritPrototype()函数实现了寄生组合式继承的最简单形式。接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。
第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。

    function create(obj){
        function F(){};
        F.prototype = obj;
        return new F();
    }
    function inheritPrototype(subType,superType){
        var clone = create(superType.prototype);       //创建对象
        clone.constructor = subType;                //增强对象
        subType.prototype = clone;                    //指定对象
    }

    function SuperType(name){
        this.name = name;
        this.color = ["red","yellow","blue"];
    }
    SuperType.prototype.sayName = function(){
        alert(this.name);
    }
    function SubType(name,age){
        SuperType.call(this,name);
        this.age = age;
    } 
    //注意这连个的书写顺序,写反了,sayAge 就不存在
    inheritPrototype(SubType,SuperType);
    SubType.prototype.sayAge = function(){
        alert(this.age);
    }

    var s1 = new SubType("Joke",33);
    var s2 = new SubType("Linda",22);
    s1.color.push("white");
    alert(s1.color);     //    "red","yellow","blue","white"
    alert(s2.color);     //    "red","yellow","blue"

演示 如下:

高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。
原型链还能保持不变,instanceof 和 isPrototypeOf()能 正常使用
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

小结

JavaScript 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。
使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。

存在下列可供选择的继承模式。
 原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅
复制。而复制得到的副本还可以得到进一步改造。
 寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强
对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问
题,可以将这个模式与组合继承一起使用。
 寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。

 

 

 



 

推荐阅读