彻底理解js面向对象之继承

作者:代码拾荒

时间:2026-10-22

浏览:552

本文深入解析JavaScript中的面向对象继承机制。从原型链的基本原理出发,探讨构造函数调用、组合继承、原型式与寄生式继承等核心模式。重点分析各继承方式的优缺点及适用场景,帮助开发者掌握JS中实现代码复用的最佳实践。

文章目录

在面向对象编程中,继承通常分为两种类型:接口继承,即仅继承方法的签名;以及实现继承,即继承具体的方法实现。

由于JavaScript语言中没有签名的概念,因此它仅支持实现继承,且这种继承是通过原型链机制来实现的。接下来将详细阐述JavaScript中继承的相关原理。

1、原型链

原型链是实现继承的核心方法,它利用原型对象使一个引用类型能够继承另一个引用类型的属性和方法。

回顾构造函数、原型对象与实例三者之间的关系:

每个构造函数都拥有一个原型对象(例如Person.prototype);该原型对象包含一个指向构造函数的指针(constructor);而每个实例对象都包含一个指向原型对象的指针(即不可见的_proto_指针)。

原型链是如何形成的?

当某个构造函数的原型对象是另一个构造函数的实例时,该构造函数的原型对象便拥有一个(不可见的_proto_指针)指向另一个构造函数的原型对象。

如果另一个原型对象又是其他构造函数的实例,这种关系会层层递进,从而形成原型链。具体结构如下所示:


    //第一个构造函数;有一个属性和一个原型方法
    function SuperType(){
        this.property=true;
    } 
    
    SuperType.prototype.getSuperValue=function(){
        return this.property
    }


    //第二个构造函数;目前有一个属性
    function SubType(){
        this.subproperty=false
    }
    
    //继承了SuperType;SubType原型成了SuperType的实例;实际就是重写SubType的原型对象;给SuperType原型对象继承了
    SubType.prototype=new SuperType()
    
    //现在这个构造函数有两个属性(一个本身的subproperty,一个继承的存在原型对象的property);两个方法(一个原型对象的getSubValue,一个原型对象的原型对象的getSuperValue)
    SubType.prototype.getSubValue=function(){
        return this.subproperty
    }
    
    var instance=new SubType()  //创建第二个构造函数的实例

    console.log(instance.getSuperValue())  //true 先查找instance这个实例有没有此方法;显然没有,再查找SubType原型对象有没有此方法;也没有,再查找SubType原型对象的原型对象;显然是存在的

注意:此时instance的constructor指向的是SuperType构造函数。因为SubType.prototype被重写,其内部的constructor也随之指向了SuperType的原型对象。关于原型搜索机制的具体运行过程,请仔细参考上述代码,相信你能理解。

1.1完整的原型

在前文关于原型的章节中已有提及,这里再次说明。完整的原型链包括Object。

所有函数的默认原型都是Object的实例;每个默认原型都拥有指向Object.prototype的_proto_指针。因此,自定义类型可以继承toString和valueOf等方法。

而Object.prototype的_proto_指针指向null,以此结束原型链。以Person构造函数为例,完整的原型链结构如下所示:

1、原型链

1.2原型和实例的关系判断

第一种方法是使用instanceof操作符:测试实例与原型链中出现的构造函数,结果均为true。

第二种方法是使用isPrototypeOf()方法:只要原型出现在原型链中,该原型就可以说是该原型链所派生实例的原型。


    console.log(instance instanceof Object)   //都为true
    console.log(instance instanceof SuperType)
    console.log(instance instanceof SubType)

   
    console.log(Object.prototype.isPrototypeOf(instance)) //都为true
    console.log(SuperType.prototype.isPrototypeOf(instance))
    console.log(SubType.prototype.isPrototypeOf(instance))

1.3谨慎定义方法

注意:给原型对象添加方法时,必须放在替换原型的代码之后。如果放在替换之前,方法将无法被找到,因为原型已被重写。

注意:在使用原型链继承时,不能使用对象字面量来创建原型方法,因为这会重写原型链。


    function SuperType(){
        this.property=true;
    } 
    
    SuperType.prototype.getSuperValue=function(){
        return this.property
    }
    
    function SubType(){
        this.subproperty=false
    }
    
    //继承SuperType
    SubType.prototype=new SuperType()
    
    //使用字面量添加新方法,导致上一行无效   因为现在的原型替换了Object实例而非SuperType的实例,关系中断
    SubType.prototype={
       getSubValue:function(){
           return this.subproperty;
       },
       somOtherMethod:function(){
           return false
       }
    };

    var instance=new SubType()
    console.log(instance.getSuperValue())  //error

1.4原型链的问题

问题一:包含引用类型值的原型。当实例作为另一个函数的原型时,引用类型值会成为原型上的属性,从而被该函数的所有实例共享。


    function SuperType(){
       this.colors=["yellow","red","olive"]
    }

    function SubType(){
    }

    SubType.prototype=new SuperType()  //color实际上就是原型上的了

    var instance1=new SubType()
    instance1.colors.push("purple")
    var instance2=new SubType()

    console.log(instance1.colors==instance2.colors)  //true

问题二:在创建子类型实例时,无法向超类型的构造函数传递参数(即无法在不影响所有对象实例的情况下,给超类型的构造函数传递参数)。

2、借助构造函数

为了解决原型中包含引用类型值带来的问题,可以采用借助构造函数的方法。

在子类型构造函数的内部调用超类型构造函数(函数是特定环境中执行代码的对象,可以通过apply或call方法进行调用)。


    function SuperType(){
        this.color=["yellow","red","olive"]
    }

    function SubType(){
        //继承了SuperType
        SuperType.call(this)
    }

    var instance1=new SubType()
    instance1.color.push("purple")
    var instance2=new SubType()

    console.log(instance1.color)  //["yellow","red","olive","purple"]
    console.log(instance2.color)  //["yellow","red","olive"]


    //传递参数
    function SuperType(name){
       this.name=name
    }
    function SubType(){
        SuperType.call(this,"double")
        this.age=12
    }

    var instance1=new SubType()
    console.log(instance1.name)  //double
    console.log(instance1.age)  //12

问题:如果仅使用构造函数继承,则无法避免构造函数本身的问题,即方法都在构造函数内部定义,导致函数无法复用。

3、组合继承(常用的还是组合,和原型与构造结合一样)


    function SuperType(name){
        this.name=name;
        this.color=["yellow","red","olive"];
    }

    SuperType.prototype.sayName=function(){
        console.log(this.name);
    }
 
    function SubType(name,age){
        //继承属性,创建属性副本
        SuperType.call(this,name);
        this.age=age;
    }
    
    //继承属性和方法,只是原型中属性被后来的函数调用生成的属性副本遮盖
    SubType.prototype=new SuperType();

    alert(SubType.prototype.constructor)  //指向的是SuperType

    SubType.prototype.constructor=SubType; //将constructor回归到SubType构造函数身上
    SubType.prototype.sayAge=function(){
        console.log(this.age)
    }
    
    
    var instance1=new SubType("double",23)
    instance1.color.push("pink")
    console.log(instance1.color)     //["yellow","red","olive","pink"]
    instance1.sayName()         //double
    instance1.sayAge()          //23

    var instance2=new SubType("single",34)
    console.log(instance2.color)     //["yellow","red","olive"]
    instance2.sayName()         //single
    instance2.sayAge()          //34

还有其他几种继承方式,这里简要介绍。

1、原型式继承

由Crockford提出;借助原型可以基于已有的对象创建新对象,同时无需创建自定义类型。


    function object(o){      //本质上object()函数对其中对象的浅复制
        function F(){}      //创建一个新的构造函数
        F.prototype=o      //构造函数原型为传入的对象
      return new F()      //返回构造函数的实例
    }

    var person={
        name:"double",
        friends:["tom","jack","mike"]
    }

    var person1=object(person)   //事实上为原型共享
    person1.name="grey"
    person1.friends.push("single")
    
    console.log(person1.friends)  //["tom", "jack", "mike", "single"]

    var person2=object(person)
    person2.name="red"
    console.log(person2.friends)   //["tom", "jack", "mike", "single"]

ES5为了规范原型式继承,提供了Object.create()方法以便使用,IE9及以上版本支持;如果希望一个对象与另一个对象保持相似状态,完全可以使用这种方法。


    var person={
        name:"double",
        friends:["tom","jack","mike"]
    }

    var person1=Object.create(person)
    person1.name="single"
    person1.friends.push("singles")

    var person2=Object.create(person)

    console.log(person1.friends==person2.friends) //true

    //Object.create()接受两个参数,一个为作为新对象原型的对象,一个为新对象定义额外属性对象
    var person={
        name:"double",
        friends:["tom","jack","mike"]
    }

    var person1=Object.create(person,{
         name:{ 
            value:"single"  //每个属性都是通过自己描述符定义的
         }
    })

2、寄生式继承

其思路与原型式继承一脉相承,创建一个用于封装继承过程的函数,内部通过某种方式增强对象并返回该对象;主要适用于对象场景。


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

    function createPerson(original){
       var clone=object(original)   //继承原型
       clone.sayName=function(){ 
           alert("name")
       }
       return clone
    }

    var person={
       name:"double",
       friends:["single","tom","jack"]
    }

    var person1=createPerson(person)
    person1.sayName()  //name   引用类型值还是共享的

3、寄生组合继承

组合继承是继承中常用的方式,但会调用两次超类型构造函数;寄生组合继承正是为了解决这一问题而设计的。


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


  function inheritPrototype(subType,superType){
     var prototype=object(superType)    //创建对象  (superType实例)
     prototype.constructor=subType     //增强对象
     subType.prototype=prototype      //指定对象  (原型赋予实例)
  }
  

   function SuperType(name,sex){
      this.name=name
      this.sex=sex
      this.colors=["red"]
   }

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

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


   inheritPrototype(SubType,SuperType)    //目前subType.prototype什么都没有
   SubType.prototype.sayAge=function(){   //为subType.prototype添加个方法
      alert(this.age)
   }

   var person1=new SubType("double","man",34)
   console.log(person1.name)  //SuperType 这是个Bug
   console.log(person1.sex)   //man
   console.log(person1.colors) //["red"]
   person1.sayAge()       //34

到此,内容基本结束,感谢你对本站的支持,希望我们整理的内容能够帮助到你。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章