javascript继承

javascript支持实现继承,也就是继承父类的方法和属性。(注:这边继续沿用前面说过的,类的概念。具体看【javascript原型对象、构造函数和实例对象】中的说明。)

原型链

原理

原型链实现继承的基本思路:利用原型让一个引用类型继承另一个引用类型的属性和方法

那么怎么做到这种继承呢?方法很简单,就是让子类的原型等于父类的一个实例。这样一来,子类的原型中就拥有一个指向父类原型的内部指针(这里涉及到原型对象、构造函数和实例之间的关系,具体的可以参看【javascript原型对象、构造函数和实例对象】)。自然就继承了它的方法和属性。来看一个例子:

       //定义一个人的类
       function Person(){
              this.name="Tom";
              this.age=10;
       }

       Person.prototype.sayName=function(){
              return this.name;
       }
 
       //定义一个小孩的类,暂时不拥有属性和方法
       function Child(){}

       //设置Child类的原型为Person的实例
       Child.prototype=new Person();
       
       //设置Child类的方法
       Child.prototype.sayAge=function(){
              return this.age;
       }
       
       //创建Child的一个实例
       var p1=new Child();

       alert(p1.sayAge());  //10
       alert(p1.sayName()); //Tom
       alert(Child.prototype.constructor==Child);  //false
       alert(Child.prototype.constructor==Person); //true
       alert(p1.constructor==Child); //false
       alert(p1.constructor==Person); //true
上面这个例子,显示定义了一个Person类,它拥有两个属性(name、age)和一个方法(sayName())。然后定义了一个Child类,它暂时不拥有属性和方法。接着,将Child的prototype指向Person类的一个实例,这就实现了继承。在这之后,为Child的原型添加了方法。

从alert语句中可看出,Child的实例p1,继承了Person的属性和方法。

其实,在上面的例子中,还忽略了一个环,那就是Person继承了Object,这个继承也是通过原型链实现的。这样一来,上面那个例子的继承关系可用下图来表示:


其实上图所展示的也就是一条原型链。(这里省略了构造函数的指针关系。因为通过原型的指向也可以得到构造函数的指向,所以为了简洁,这里就不在指出。)

用文字,原型链也可以这样表示的:

       /*
        *原型链:
        *p1 [Child的实例]
        *   Child.prototype [Person的实例]
        *      Person.prototype
        *         {sayName:...}
        *         Object.prototype
        *            {toString:...}
        */
注意:

  1. 给子类原型添加方法或属性一定要放在替换原型的语句之后。在上面的例子中,就是要在Child.prototype=new Person()语句之后。
  2. 用instanceof检测在原型链中出现过的构造函数与实例之间的关系,都将返回true。
  3. 用isPrototypeOf()方法检测原型链中出现过的原型与实例之间的关系,也都将返回true。
  4. 用原型链来实现继承的方法中,不能使用对象字面量来创建子类原型的方法或属性。因为这样会重写了原型,切断了它与父类的联系,也就不存在继承关系了。

还用上面的例子,我们来验证下,注意项中的2,3点:

       //定义一个人的类
       function Person(){
              this.name="Tom";
              this.age=10;
       }

       Person.prototype.sayName=function(){
              return this.name;
       }
 
       //定义一个小孩的类,暂时不拥有属性和方法
       function Child(){}

       //设置Child类的原型为Person的实例
       Child.prototype=new Person();
       
       //设置Child类的方法
       Child.prototype.sayAge=function(){
              return this.age;
       }
       
       //创建Child的一个实例
       var p1=new Child();

       alert(p1 instanceof Object); //true
       alert(p1 instanceof Person); //true
       alert(p1 instanceof Child);  //true

       alert(Object.prototype.isPrototypeOf(p1)); //true
       alert(Person.prototype.isPrototypeOf(p1)); //true
       alert(Child.prototype.isPrototypeOf(p1));  //true

同样也看一个例子,来验证注意项中的第4点:

       //定义一个人的类
       function Person(){
              this.name="Tom";
              this.age=10;
       }

       Person.prototype.sayName=function(){
              return this.name;
       }
 
       //定义一个小孩的类,暂时不拥有属性和方法
       function Child(){}

       //设置Child类的原型为Person的实例
       Child.prototype=new Person();
       
       //用对象字面量添加新方法,会导致上面一行代码无效
       Child.prototype={
       	   sayAge:fucntion(){
       	   	  alert(this.age);
       	   }
       };
       
       //创建Child的一个实例
       var p1=new Child();

       alert(p1.sayAge());  //error!
用对象字面量的形式向Child.prototype中添加方法,现在原型中包含的将是Object中的实例,而不是Person中的实例,将导致错误。

属性搜索机制

在javascript中,对象属性的搜索是先从实例开始的,然后逐级向上搜索,直到找到相应的属性或者直到原型链的末端(即原型为null)才停止。

还是那前面的例子来说明。执行alert(p1.sayName()),搜索将分为以下几个步骤:1)搜索p1;2)搜索Child.prototype;3)搜索Person.prototype,最后一步才找到该方法。假如要执行alert(p1.sayAge()),则只要经历两个步骤即可,因为sayAge()存在与Child.prototype中,在这里找到了,就不必再去搜索Person.prototype了。

由上面的搜索机制也可以看出,假如在子类中重写了父类的属性(或方法),将屏蔽父类中同名的属性(或方法)而以子类重新定义的属性(或方法)替代之

缺点

用原型链来实现继承会存在一些问题。这个问题主要是由于:原型中的属性是被所有实例所共享的。假如包含了引用类型值的原型,那么就会导致一些我们不希望看到的现象:

       //定义一个人的类
       function Person(){
           this.friends=["Lily"];
       }
 
       //定义一个小孩的类,暂时不拥有属性和方法
       function Child(){}

       //设置Child类的原型为Person的实例
       Child.prototype=new Person();

       //创建Child的实例
       var p1=new Child();
       var p2=new Child();

       alert(p1.friends); //Lily
       alert(p2.friends); //Lily

       p1.friends.push("Sam");

       alert(p1.friends); //Lily,Sam
       alert(p2.friends); //Lily,Sam
在上面的例子中,我们只为p1添加一个朋友,却导致p2也同时增加了这个朋友,这不是我们想要的。那为什么会导致这样的结果呢?

原因就是,当Child通过原型链继承了Person,Child.prototype就变成了Person的一个实例,它就拥有了friends这个属性,这就相当于Child.prototype自己添加了friends属性一样。我们知道,添加在Child.prototype中的属性是被所有Child的实例所共享的。所以,当p1改变了friends这个属性的值,这个结果也将立即反应到p2中。

用原型链实现继承还有另一个问题,那就是子类不能向父类传递参数。

借用构造函数

原理

借用构造函数来实现继承的基本思路是:在子类的构造函数中调用父类的构造函数。主要通过call()方法或apply()方法来实现这种调用。

       function Person(name){
           this.name=name;
           this.friends=["Lily"];
       }

       function Child(){
       	   Person.call(this,"Tom"); //向父类传递参数
       }

       var p1=new Child();
       alert(p1.friends);  //Lily
       alert(p1.name);  //Tom

       var p2=new Child();
       alert(p2.friends); //Lily

       p1.friends.push("Sam");

       alert(p1.friends);  //Lily,Sam
       alert(p2.friends);  //Lily

看上面的例子,在Child类中向Person类传递了名字参数,通过调用call()方法,实现在Child实例的环境中调用Person构造函数,这样就会在Child上实现Person()中所有对象的初始化,最终结果就是Child的每个实例都拥有一份Person中属性的副本,从而实现继承。

优点

上面的例子中,实现了在Child中向Person传递参数,并且,由这种方式实现的继承,是让Child每个实例都拥有一份friends属性的副本,这样当p1修改friends属性的值时,实际上修改的是Person中friends属性的副本,不会影响到其他实例。这么说来,借用构造函数实现继承就有一下两个有点:

  1. 可以实现在子类构造函数中向父类构造函数传递参数;
  2. 子类的每个实例中都拥有父类属性的一个副本,即实例属性相对独立,不会互相影响。

缺点

借用构造函数实现继承也存在问题,就是方法都必须在构造函数中定义,原因在于父类原型中定义的方法,在子类中是不可见的。这样一来,函数的复用就无从谈起了。

       function Person(name){
           this.name=name;
       }

       Person.prototype.sayName=function(){
       	   return this.name;
       }

       function Child(){
       	   Person.call(this,"Tom");
       }

       var p1=new Person("Lily");
       alert(p1.sayName()); //Lily

       var p2=new Child();
       alert(p2.sayName());  //error!
上面这个例子,alert(p2.sayName());将导致错误。因为对于Child来说,它从未定义这样的一个方法,而且并不能从父类中继承。

基于上面原因,也很少单独用这种方式实现继承。

组合继承

原理

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

       function Person(name){
       	   this.name=name;
       	   this.friends=["Lily"];
       }

       Person.prototype.sayName=function(){
       	   return this.name;
       }

       function Child(name,age){
       	   Person.call(this,name);
       	   this.age=age;
       }

       Child.prototype=new Person();
       Child.prototype.constructor=Child; //将指针重新指向Child
       Child.prototype.sayAge=function(){
       	   return this.age;
       }

       var p1=new Child("Tom",10);
       alert(p1.sayName());  //Tom
       alert(p1.sayAge());   //10
       alert(p1.friends);    //Lily

       var p2=new Child("Sam",5);
       alert(p2.sayName());  //Sam
       alert(p2.sayAge());   //5
       alert(p2.friends);    //Lily

       p1.friends.push("Bob");

       alert(p1.friends);    //Lily,Bob
       alert(p2.friends);    //Lily

缺点

看上面的例子,Person被调用了两次,第一次是在创建Child原型的时候,将Child.prototype作为Person的一个实例,这时Child.prototype会得到两个实例属性(name和friends),它们位于Child的原型中,是被所有Child实例所共享的;第二次是在创建Child实例时,调用了Child构造函数,它的内部就也调用了Person,这时Child的每个实例都拥有了Person中属性的副本,于是,这两个属性(name和friends)就覆盖了原型中的两个同名属性。等于说,每一次在调用Child构造函数时,都会产生两组name和friends属性:一组在Child原型中,一组在实例上。造成资源浪费。

寄生组合式继承

为了避免组合继承的确定,就有了寄生组合式继承的方法。

原理

寄生组合式继承的基本思路是:通过借用构造函数来继承属性,通过原型链混成形式来继承方法。本质上,就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类型的原型。

寄生式继承

在讲寄生组合式继承前,先来了解下寄生式继承。

所谓寄生式继承,就是创建一个用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回该对象。

首先,它会借用一个函数来实现类似原型链的继承,只不过它不显式让子类的原型等于父类的一个实例,而是创建了一个函数,在其内部实现这种继承。这种方式也称原型式继承。

        //原型式继承,所有实例会共享属性的值
        function object(o){
        	function F(){}  //创建一个临时的构造函数
        	F.prototype=o;  //将传入的参数作为该构造函数的原型
        	return new F(); //返回这个构造函数的一个实例
        }
接着,它利用另一个函数以某种方式来增强object返回的那个对象,最终返回增强后的对象。

        fucntion createAnother(original){
            var clone=Object(original); //调用一个新对象
            clone.sayHi=function(){  //增强对象
                alert("Hi");
            };
            return clone;
        }
来看一个寄生式继承的例子:

        //原型式继承,所有实例会共享属性的值
        function object(o){
        	function F(){}  //创建一个临时的构造函数
        	F.prototype=o;  //将传入的参数作为该构造函数的原型
        	return new F(); //返回这个构造函数的一个实例
        }

        function createAnother(original){
            var clone=object(original); //调用一个新对象
            clone.sayHi=function(){  //增强对象
                alert("Hi");
            };
            return clone;
        }

        var person={
            name:"Tom",
            friends:["Lily"]
        };

        var anotherPerson=createAnother(person);
        anotherPerson.sayHi(); //Hi
        alert(anotherPerson.name); //Tom
上面的例子,anotherPerson继承了person的name和friends属性,并且还拥有自己的sayHi()方法。

寄生组合式继承模式

就是使用寄生式继承来继承父类的原型,然后再将结果指定给子类型的原型。

        function inheritPrototype(subType,superType){
        	var prototype=Object(superType.prototype); //继承父类的原型
        	prototype.constructor=subType;  //将构造函数指针指向子类,增强对象
        	subType.prototype=prototype;  //将结果指定给子类原型
        }

inheritPrototyoe()函数接受两个参数,分别为子类构造函数和父类构造函数。

在函数内部,第一步是创建了父类的一个副本;第二步是为创建的副本添加constructor属性,弥补重写原型而失去的默认的constructor属性值;第三步是,将新创建的这个父类的副本赋给子类的原型。

来看一个例子:

        //所有实例会共享属性的值
        function object(o){
        	function F(){}  //创建一个临时的构造函数
        	F.prototype=o;  //将传入的参数作为该构造函数的原型
        	return new F(); //返回这个构造函数的一个实例
        }

        
        function inheritPrototype(subType,superType){
        	var prototype=Object(superType.prototype); //继承父类原型
        	prototype.constructor=subType;  //将构造函数指针指向子类,增强对象
        	subType.prototype=prototype;  //将结果指定给子类原型
        }

        function Person(name){
        	this.name=name;
        	this.friends=["Lily"];
        }

        Person.prototype.sayName=function(){
        	return this.name;
        }

        function Child(name,age){
            Person.call(this,name); //借用构造函数继承实例属性
            this.age=age;
        }

        inheritPrototype(Child,Person); 

        Child.prototype.sayAge=function(){
        	return this.age;
        }

        var p1=new Child("Tom",10);
        alert(p1.sayName()); //Tom
        alert(p1.sayAge()); //10
        alert(p1.friends); //Lily

        var p2=new Child("Sam",5);
        alert(p2.sayName()); //Sam
        alert(p2.sayAge()); //5
        alert(p2.friends); //Lily

        p1.friends.push("Bob");

        alert(p1.friends); //Lily,Bob
        alert(p2.friends); //Lily

优点

寄生组合式继承,在整个实现的过程中只调用一次父类构造函数,避免了创建不必要的、多余属性。并且,能够保持原型链不变。





  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值