白驹过隙,这篇文章距今已有一年以上的历史。技术发展日新月异,文中的观点或代码很可能过时或失效,请自行甄别:)

前言

在看<<JS高级程序设计\>\>第六章"面向对象的程序设计"时对JS有了新的认识,但涉及的知识量有点大,做下笔记,以此作为温习.

本文基于<<JS高级程序设计\>\>第3章,第5章函数部分和第6章,同时加入自己的理解和网络相关博客的内容.如果能对看此文的朋友有所帮助,甚幸!如果有任何错误,欢迎指正:)

函数的基础知识

因为JS的面向对象基本都是借助函数实现的,所以先上点关于函数的开胃菜便于后续的理解.

函数的声明

1. 函数声明语法定义

    function sum(num1,num2){
        return sum1+sum2;
    }

2. 函数表达式定义(匿名函数)

    var sum=function(num1,num2){
       return sum1+sum2;
    }

3. Function构造函数定义(不推荐,影响性能)

    var sum=new Function('num1','num2','return num1+num2');

Notes:函数表达式和函数声明式定义的不同,JS解析器会率先读取函数声明,并使其在执行任何代码之前可用,而函数表达式则必须等到解析器先执行等到解析器执行到它所在的代码行才会被真正的被执行.具体见下两个代码:

    alert(sum(10,10));
    function sum(num1,num2){
        return num1+num2;
    }/*正确,会执行*/

    alert(num(10,10));
    var sum=function(num1,num2){
       return num1+num2;
    }/*错误,不会执行,因为sum的表达式在调用之后*/

函数的属性和方法

1. 属性

  • length:函数系统为能够接收的命名参数的个数
  • prototype:对引用类型来说,prototype保存他们所有实例方法的真正所在.(这个很重要,面向对象中基本都有它的身影)

2. 方法

  • apply()和call():在特定的作用域中调用函数

    apply()方法支持两个参数,参数1代表在其中运行函数的作用域,第二个是参数数组(可以是Array实例,亦可以是arguments对象),当初在这里有点迷糊,详讲下:

        function sum(num1,num2){
            return num1+num2;
        }
        
        function callsum(num1,num2){
            sum.call(this,arguments);//这里的this就是函数`callsum`,可以换成callsum,效果相同,arguments为callsum中的参数列表,即num1,num2,同时这里的arguments可以换成`[num1,num2]`,效果相同

        alert(callsum(10,20));//结果为30
可以看出来,callsum中本来没有sum方法,用了call后callsum(10,20)==sum(10,20)了.再来一个高级点的
        function class1(name){
            this.add=function(num1,num2){
                return num1+num2;
            }

            this.sub=function(num1,num2){
                return num1-num2;
            }

            this.name=name;

            this.getname=function(){
                return this.name;
            }
        }

        function class2(name){
            class1.call(this,name);
        }

        var instance=class2('apple');
        alert(instance.name);//'apple'
        alert(instance.add(10+20));//30
很强大吧,class2里面的class1.call把class1中的this转换为了class2的作用域,这样class2就"继承"了class1的所有属性和方法,因此,instance.add(10+20)==class1.(10+20)

对象

概况

创建对象有两种方法.

  1. his用new操作符后跟object构造函数.
    var person=new Object();
    person.name="Scofield";
    person.age=1;
  1. 使用对象字面量表示法.(推荐只在考虑对象属性名的可读性以及向函数传递大量可读参数时使用)
    var person={
        name: "Scofield",
        age:1,
    }

Object类型的属性和方法

属性
  1. constructor:保存着创建当前对象的函数,比如:
        function Person(name){}
        var person1=new Person('apple');
        alert(person1.constructor==Person);//true
方法
  1. hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中(非实例的原型中)是否存在.其属性名(propertyName)必须以字符串形式指定,如:

     o.hasOwnProperty('name');
    
  2. isPrototypeOf(object):检测传入的对象是否是另一个对象的原型.
  3. propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用for-in语句来枚举,其属性名(propertyName)必须以字符串形式给出
  4. toString():返回对象的字符串表示
  5. valueOf():返回对象的字符串,数值或者布尔值表示.通常与toString()方法返回的值相同.

创建对象

工厂模式

知道设计模式的人应该知道这个家喻户晓的模式,隐藏了创建对象的细节.该模式也可以在JS下间接实现,即在函数里面创建一个对象,然后函数内创建对象的各种属性和方法,最后返回该对象.如下例子:

    function personFactory(name,age,job){
        var o=new Object();
        
        o.name=name;
        o.age=age;
        o.job=job;
        
        o.getName=function(){
            alert(this.name);
        }
       
        return o;
    }

    var person1=personFactory('Scofield',1,'Programer');
    var person2=personFactory('Julia',1,'Manager');

    person1.getName();//Scofield
    person2.getName();//Julia

优点:能够根据参数来构建不同的对象,每次调用该对象都会返回一个包含相应属性和方法的对象.

缺点:未解决对象识别的问题.(如何知道一个对象是什么类型).

构造函数模式

ECMAScript中的构造函数可以用来创建特定类型的对象.我们可以创建自己的构造函数,从而自定义对象类型的属性和方法.因此上面工厂模式的例子可以重写为:

    function PersonConstructor(name,age,job){
        this.name=name;
        this.age=age;
        this.job=job;
        this.getName=function(){
            alert(this.name);
        }
    }
    
    var person1=new PersonConstructor('Scofield',1,'Programer');
    var person2=new PersonConstructor('Julia',1,'Manager');

    person1.getName();//Socifield
    person2.getName();//Manager

上面的代码和工厂模式区别很小,少了创建对象,并且将属性和方法对赋值给了this,同时也少了return语句.

注意创建PersonConstructor的新实例必须使用new操作符,这样调用构造函数会经历以下4个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(this指向了新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性和方法)
  4. 返回新对象

在上面的例子中person1和person2分别保存着一个不同的实例.但是他们都有一个constructor属性,这个属性指向了PersonConstructor,因此:

    alert(person1.constructor==PersonConstructor);//true
    alert(person2.constructor==PersonConstructor);//true

不过在检测对象类型时我们更加侧重于使用instanceof操作符来检测.并且注意,我们上面创建的两个实例既属于Object实例(因为所有的对象都是继承于Object),也同时属于PersonConstructor实例.如下:

    alert(person1 instanceof Object);//true
    alert(person2 instanceof Object);//true
    alert(person1 instanceof PersonConstructor);//true
    alert(person2 instanceof PersonConstructor);//true

优点:可以用来将构造函数的实例标识为一种特定的类型

缺点:每个方法都要在每个实例上重新创造一遍,具体见下:

在上面的例子中person1和person2都有一个名为getName()的方法,但那两个方法不是同一个Function的实例,因为ECMAScript中的函数是对象,每定义一个函数也就是实例化一个对象.因此我们:

    alert(person1.getName==person2.getName);//false

因为我们在构造函数中的getName是如下定义的:

    this.getName=function(){
        alert(this.name);
    }

从逻辑角度上讲也可以这样定义:

    this.getName=new Function("alert(this.name)");

因此每个PersonConstructor实例都包含一个不同的Function实例.

Notes:关于构造函数与其它函数

构造函数与一般的函数的唯一区别就是在于他们的调用方式不同.任何函数只要通过new操作符来调用就是构造函数,否则就是普通的函数.例如刚刚的例子这样用:

    PersonConstructor('Scofield',1,'Programer');
    window.getName();//Scofield(在全局对象中调用this,指向的是global对象)

    //在另一个对象的作用域中调用
    var o=new Object();
    PersonConstructor.call(o,'Scofield',1,'Programer');
    alert(o.getName());//Scofield

原型模式

在我们创建的函数都含有一个prototype(原型)属性,该属性是一个对象,包含可以由特定类型的所有类型的所有实例共享的属性和方法.使用原型的好处就是可以让所有对象实例共享它所包含的属性和方法.如下:

    function Person(){}
    
    Person.prototype.name="Scofield";
    Person.prototype.age=29;
    Person.prototype.job="Programmer";
    Person.prototype.getName=function(){
        alert(this.name);
    };

    var person1=new Person();
    person1.sayName();//Scofield
    
    var person2=new Person();
    person2.sayName();//Scofield

    alert(person1.getName==person2.getName);//true

由上可知,实例person1和person2共享了Person函数的所有属性和方法,且方法为同一个实例.这就是原型函数的魅力.下面详细说明下原型函数的原理.

  1. 理解原型

    前面我们说过,每一个函数在创建的时候都会有一个prototype属性,也就是原型,而这个原型最初创建的时候只有一个constrcutor属性,指向的是prototype属性所在函数的指针.因此,上面的Person.prototype.constrcutor指向的是Person.而其他的方法都是通过Object继承而来,当然了,我们也可以给prototype添加各种属性和方法.这也是我们上面Person.prototype.age的由来.

    当我们实例化了一个构造函数后,这个实例的内部就会有一个指向构造函数原型的指针,在很多实现中一般都为"__proto__"这个内部属性.这里要记住的是这个指针式指向构造函数的原型(prototype)而非构造函数.它们的内部关系如下图:

    Untitled Diagram.png

    从图上可以看出person1和person2的__proto__属性都是指向Person Prototype的,可以通过isPrototypeOf()方法来确定,该方法可以确定某个对象内部的__proto__是否指向某对象的prototype,如果是则返回true,否则返回false,比如:

     alert(Person.prototype.isPrototypeOf(person1));//true
    

    因为person1内部有一个指向Person.prototype的指针,因此返回真,那么,当我们调用person1.getName()的时候发生了什么呢?

    首先,person1会在自己这个实例的内部去找有没有getName这个方法,如果有,好了,就是它了,停止搜索,如果没有,去瞅瞅它的__proto__指向的那个原型有没有,有的话就算找到了,停止搜索.因此我们调用person1.getName()的时候,现在自己的实例找,没找到,接着去__proto__指向的原型中找,找到了,停止搜索并输出.

    知道了这一点,我们就能明白,我们能够读取原型中的方法,但是却不能通过对象实例重写原型中的值,而只能屏蔽它.如果我们一定要读取原型中的值怎么办呢?用delete操作符即可,如下:

        var person3=new Person();
    
        person3.name="Julia";
        alert(person3.name);//Julia
    
        delete person3.name;
        alert(person3.name);//Scofield

那么如何查看一个属性是实例中的还是原型中的呢?用我们最开始的时候说过的hasOwnProperty()方法即可,该方法只有在给定的属性存在于对象实例中时才会返回true.如下:
        var person4=new Person();
    
        person4.name="Apple";
        alert(person4.hasOwnProperty("name"));//true
    
        var person5=new Person();
        alert(person5.hasOwnProperty("name"));//false
不过这样还不够,我们一般都是还要加一个判断,用in操作符,我们知道在for-in循环中有这个操作符,当我们单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性是存在于实例还是原型中.如下:
        alert("name" in person4);//true
        alert("name" in person5);//true

结合in和hasOnwproperty我们可以就可以判断出一个属性或者方法到底是存在于实例还是原型中了.
  1. 更简单的原型语法

    如果原型属性或者方法很多,我们就要敲很多的Person.prototype了.针对这样的情况,我们可以用一种更加简洁的做法来创建原型对象,如下:

        function Person(){}
    
        Person.prototype={
            name:"Scofield",
            age:29,
            job:"Programmer",
            getName:function(){
                alert(this.name);
            }
        };
这样做就简洁多了不是?但是有一个问题,constrcutor属性就不再指向Person了.因为我们这里的语法相当于重写了默认的prototype对象,榆次constructor属性与旧变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数.(instanceof依旧能够返回Object和Person).因此如果constructor值很重要,那么我们可以显性的将constructor属性赋值为Person,如下:

    Person.prototype={
        constrcutor:Person,
        name:"Scofield",
        age:29,
        job:"Programmer",
        getName:fucntion(){
            alert(this.name);
        }
    };
  1. 原型的动态性

    因为在原型中查找值的过程是一个搜索,因此我们对原型对象所做的任何修改都能够立即从实例中反映出来--即使是先创建了实例后修改原型也是如此,如下:

     var person7=new Person();
     
     Person.prototyp.name="Julia";
     
     alert(person7.name);//Julia

    不过如果我们重写了整个原型对象的时候,情况就不同了.因为当我们重写的时候相当于创建了一个新的prototype实例,而这个实例跟我们原来的实例不是同一个.而我们创建一个person实例的时候其__proto__是一个指向Perosn.prototype的指针,现在我们重写了Person.prototype,而__proto__仍然是指向原来的那个实例,而这个实例现在已经被显得prototype重写,也就不能访问了.用一个例子说明下面的问题:

     function Person(){}
     
     var person=new Person();
    
     Person.prototype={
         constructor:Person,
         name:"Scofield",
         age:1,
         getName:function(){
             alert(this.name);
         }
     };
    
     alert(person.name);//error
    

    用图来说话就是这样:

    Untitled Diagram (1).png

  2. 原生对象的原型

    原型模式不仅可以体现在自定义的类型中,原生中的引用类型都是这种模式创建的.例如可以在Array.prototype中找到sort()方法.我们可以通过原生对象的原型取得默认方法的引用,也可以定义新的方法.比如我们可以给String添加一个名为startWidth()的方法:

     String.prototype.startWidth=function(text){
         return this.indexOf(text)==0;
     };
    
     var msg="hello world!";
     alert(msg.startWidth('hello'));//true
    

4.原型对象的问题

因为原型中的所有属性是被很多实例共享的,对于一个包含引用类型值的属性来说,某个实例修改该类型属性时原型中相应的值也被修改.如下:

    function Person(){};
    Person.prototype={
        hobby:['reading','coding']
    };
    
    var person1=new Person();
    
    person1.hobby.push('chasing girl');

    var person2=new Person();
    console.log(person2.hobby);//reading,coding,chasing girl

组合使用构造模式和原型模式

既然单用构造模式和原型模式都有缺点,那就把它们都用起来,这样不就可互补了吗?构造模式用来定义实例属性,原型模式用来定义公共方法和共享的属性.这样每个实例都会有自己的一份实例属性,同时又共享着对方法的引用,酷毙了...!必须把上面写过的PersonConstructor用这种高大上的写法重写一下:

function PersonConstructorPrototype(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.hobby=['reading','coding'];
}

PersonConstructorPrototype.prototype.getName=function(){
    alert(this.name);
}

var person1=new PersonConstructorPrototype('Scofield',1,'Programmer');
person1.hobby.push('chasing girls');

var person2=new PersonConsturctorPrototype('Julia',1,'Manager');
peson2.hobby.shift();

alert(person1.hobby);//reading,coding,chasing girls
alert(person2.hobby);//reading

动态原型模式

该模式把所有信息都封装到了构造函数中,在构造函数中初始化原型(仅第一次),这样同时使用了构造函数且保存了原型的优点.即通过检查某个方法是否有效来决定是否需要初始化原型.把上面的例子改为动态原型模式:

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    
    if(typeof this.getName != 'function'){
        Person.prototype.getName()=function(){
            alert(this.name);
        }
        
        Person.prototype.hobby=['reading','coding'];
    }
}

var person1=new Person('Scofield',1,'Programmer');

person1.getName();//Scofield

寄生构造函数模式

该模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,如下:

function Person(name,age,job){
    var o=new Object();
    
    o.name=name;
    o.age=age;
    o.job=job;
    
    o.getName=function(){
        alert(this.name);
    }
    
    return o;
}

var person1=new Person('Scofield',1,'Programmer');
person1.getName();//Scofield

这个模式除了使用new操作符并把使用的包装函数(这里指Person)叫做构造函数外,这个模式跟工厂模式没什么区别.这个模式主要是用于特殊的情况下为对象创建构造函数.如我们想为Array构造函数添加一个新方法但是不能修改Array构造函数本身时,如下:

function newArray(){
    var a=new Array();
    
    o.push.apply(o,arguments);
    o.toPipedString=function(){
        return this.join('|');
    };
    
    return o;
}

var colors=new newArray('red','blue','green');
alert(colors.toPipedString());//'red|blue|green'

稳妥构造函数模式

稳妥对象,就是指没有公共属性,并且其方法也不引用this的对象.稳妥对象最适合在一些安全的环境下(这些环境会禁用this,new)或者防止数据被其他应用程序改动时使用.没有见到过这种不是很明白,不过看了书上的例子后就明白了.也就是要访问构造函数里面的值只能自己定义方法和属性,而没有其他办法访问,比如书上的这段代码:

function Person(name,age,job){
    var o=new Object();
        
    //这里可以添加私有变量和函数
    //咱给添加一个
    var hobby=['reading','coding'];
    
    o.sayName=function(){
        alert(name);
    };

    return o;
}

因为返回了o,所以对于构造函数里面的自己定义,比如上面的hobby属性,如果不定义一个方法或者属性根本是不能访问的.

对象之继承

原型链

  1. 基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法.具体也就是让子类的原型直接继承父类的构造函数,这样子类的原型里面就有一个一个__proto__的属性指向父类的原型,而构造函数的本身的属性和方法都继承到了子类的原型中,如果父类的构造函数的原型也是继承自它父类的构造函数,这样一个接一个,就跟学数据结构时的链表一样环环相扣,就有了这个"原型链".书上给出的例子很好,再次贴出来.

     function SuperType(){
         this.property=true;
     }
         
     SuperType.prototype.getSuperValue=function(){
         return this.property;
     };
    
     function SubType(){
         this.subproperty=false;
     }
     //inherited from SuperType
     SubType.prototype=new SuperType(){
         return this.subproperty;
     }
    
     var instance=new SubType();
     alert(instance.getSuperValue());//true
    
     alert(instance instanceof Object);//true
     alert(instance instanceof SuperType);//true
     alert(instance instanceof SubType);//true
    
     alert(Object.prototype.isPrototypeOf(instance);//true
     alert(SuperType.prototype.isPrototypeOf(instance));//true
     alert(SubType.prototype.isPrototypeOf(instance));//true
    

    上面用一个图标来看一目了然:

    Untitled Diagram (2).png

    很好理解,子类SubType.prototype被我们用父类SuperType的构造函数给重写了.要注意instance.constuctor现在指向的是SuperType,因为SubType.prototype指向的是SuperType.prototype对象,而这个对象指向的是SuperType.

    我们知道,所有的引用类型都是继承于Object,而这个集成也是通过原型链实现的.也就是说在我们的SuperType.prototype中也有一个__proto__属性,指向的是Object Prototype,书上给的图也很明确,如下:

    Untitled Diagram (3).png

    那么如何确定一个原型和实例之间的关系呢?有两种方法,一种是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,就会返回true,第二种是使用isPrototypeOf()方法.也是只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原理,因此isPrototypeOf()也可以返回true.

    不过原型链有两个要注意的地方,第一个就是如果子类要重写超类的某个方法或者是添加超类型中不存在的方法,给原型添加方法的代码一定要放在替换原型的语句之后,只有这样才不会被父类的原型给覆盖掉.第二个就是不能使用对象字面量来创建原型的方法,这样会重写原型链,新的原型会是Object的实例,而非SuperType的实例,原型链已经被切除了.

    当然了,原型链也有两个缺点.第一个就是原型中的属性是被所有的子类共享的,如果一个子类更改了原型的属性,那么另外一个子类调用该属性时的值也是被更改后的值,第二个就是子类无法在不影响素有对象实例的情况下向超类的构造函数传递值.这个用一个例子来说明:

     function Person(name,age,job){
         this.name=name;
         this.age=age;
         this.job=job;
     }
     
     Person.prototype.getName=function(){
         return this.name;
     };
    
     function Person2(){}
     
     Person2.prototype=new Person();
    
     function Person3(){}
    
     Person3.prototype=new Person();
    
     new instance1=new Person1('Scofield',1,'Programmer');
     new instance2=new Person2('Julia',1,'Manager');
    
     instance1.getName();//null
     instance2.getName();//null
    

    你看,子类实例化无法设置超类中果构造函数的属性吧!因此在实践中我们很少单独使用原型链.

借用构造函数(伪造对象或经典继承)

为了解决原型中包含引用类型值带来的问题,于是有了这种技术.这种技术很简单,就会在子类的构造函数内部调用超类型构造函数.因为函数只不过是在特定环境中执行代码的对象,因此通过使用apply()或者call()可以在新创建的对象上执行构造函数.如下:

    function SuperType(name){
        this.name=name;
    }
    
    function SubType(name){
        SuperType.call(this,name);
    }

    var instance=new SubType('Scofield');
    alert(instance.name);//Scofield
    instance.age=1;
   alert(instance.age);//1
    var instance2=new SubType('Julia');
    alert(instance2,name);//Julia

从上面可以看出,原型链的子类实例传递参数,以及原型中属性共享的问题都解决了,但是如果方法都是在构造函数中的话,那么函数不就没有复用了吗?(回顾前面的构造函数模式每一个构造函数中的方法在实例化的时候都是一个实例化的过程),而且,如果我们在超类中的原型中定义了方法,子类不就看不见了吗?因此,组合继承就登场了...

组合继承(伪经典继承)

原理也很简单,就是将原型链和借用构造函数的优点结合起来使用发挥二者长度的一种继承模式.原型链实现对原型属性和方法的继承,而通过借用构造模式来实现对实例属性的继承.下面是例子:

    function Person(name,age,job){
        this.name=name;
        this.age=age;
        this.job=job;
    }
    
    Person.prototype.getName=function(){
        alert(this.name);
    };
    Person.prototype.nation='China';

    function Person2(name,age,job){
        Person.apply(this,arguemnts);
    }

    Person2.prototype=new Person();

    var instance=new Person2('Scofield',1,'Programmer');
    instance.getName();//Scofield

    var instance2=new Person2('Julia',1,'Manager');
    instance2.getName();//Julia

这样就不错了,两个实例既可以拥有自己的属性,又可以拥有相同的属性和方法.

原型式继承

看这里时我有点迷糊,首先看下书上怎么说的:借助原型基于已有的对象创建新的对象,同时还不必因此创建自定义类型.给出了如下函数:

    function object(o){
        function F(){}
        F.prototype=o;
        return new F();
    }

    var person={
        name:"Scofield",
        friends:['Julia']
    };

    var anotherPerson=object(person);
    anotherPerson.name="Greg";
    anotherPerson.friends.push('Bob');

    var yetanotherPerson=object(person);
    yetAnotherPerson.name="Linda";
    yeaAnotherPerson.friends.push('Barbie');

    alert(person.friends);//Julia,Bob,Barbie

显示很迷糊,丫的,这厮干嘛的,直接一个原型链不就行了,搞得这么麻烦.比如说这样:

    function Person(array){};

    Person.prototype.name=array[0];
    Person.prototype.age=array[1];
    Person.prototype.job=array[2];
    
    var newPerson=['Scofield',1,'Programmer'];

    var instance1=new Person(newPerson);

这样效果不是一样的吗,干嘛要那样呢?看到书上说:在没有必要兴师动众地创建构造函数,而只是让一个对象与另一个对象保持类似的情况下使用.意思是说我如果我嫌上面的构造函数太麻烦了,那就先写一个函数专门来"克隆"我要继承的对象?总之虽然明白了,对于实际的用途仍然不是很清楚,如果有知道的话,还望告知下:)

寄生式继承

寄生式继承是与上面原型式继承是紧密相连的,即创建一个仅用于封装继承过程的寒素,该函数在内部以某种方式来增强对象,然后像真的做了所有工作一样返回对象.用书上的代码:

    function createAnother(original){
        var clone=object(orginal);
        clone.sayHi=function(){
            aler('hi');
        };
        return clone;
    }

    var person={
        name:'Scofield',
        friends:['sheldby','court','van']
    };

    var anotherPerson=createAnother(person);
    anotherPerson.sayHi();//hi

用途:在主要考虑对象而不是自定义类型和构造函数的时候寄生式继承是一种很不错的模式.但是注意在使用时如果为对象添加函数会由于不能做到函数复用而降低效率,这一点与构造函数类似.

说实话,这种和上一种我都不是很理解他们的实际用途能够干嘛,先留在这里,哪天顿悟了再添加上.

寄生组合式继承

虽然看起来组合继承是一个很棒的模式,但是万物都有缺点,组合继承也是如此.最大的短处就是无论在什么情况下都会调用两次超类型构造函数:一次是在创建子类型原型的时候,二次是在子类型构造函数内部子类型构造函数内部.如下:

    functon SuperType(name){
        this.name=name;
        this.colors=['red','blue','green'];
    }

    SuperType.prototype.sayName=function(){
        alert(this.name);
    };

    function SubType(name,age){
        SuperType.call(this,name);//first invoke SuperType()
        
        this.age=age;
    }

    SubType.prototype=new SuperType();//second invoke SuperType()

    SubType.prototype.sayAge=function(){
        alert(this.age);
    };

如上,第一次调用SuperType()是在将超类的构造函数赋给子类的原型的时候,这是子类的原型中有了超类中的name和color属性,尽管现在为undefined.第二次调用SuperType()的时候调用子类的构造函数时,这次在新对象中创建了name和color属性,然后屏蔽掉了子类原型中的name和color属性.具体见下图:

Untitled Diagram (4).png

其实我们需要的只是超类中原型的属性和方法而已,子类中的name和colors是不需要的,且可以达到节省开销的作用.如何实现呢?很简单,前面讲的寄生上场.我们只把超类中的原型连接到子类的原型去不就行了?

function inheritPrototype(subType,superType){
    var prototype=object(superType.prototype);
    prototype.constructor=subType;
    subType.prototype=prototype;
}

然后客户端这样用:

function SuperType(name){
    this.name=name;
    this.colors=['red','blue','green'];
}

SuperType.prototype.sayName=function(){
    alert(this.name);
};

function SubType(name,age){
    SuperType.call(this,name);

    this.age=age;
}

inheritPrototype(SubType,SuperType);

SubType.prototype.sayAge=function(){
    alert(this.age);
};

修改后就只调用了一次SuperType()构造函数,inheritPrototype()只把SubType的原型的__proto__指向了SuperType的原型,没有了多余不必要的属性.并且还能够正常使用instanceof和isPrototypeOf操作符.