关于js的继承实现的多种方法

250 阅读4分钟

昨天和一个以前在培训机构当老师的同事聊天,聊到js的继承,才发现自己虽然一直会用,但是给别人讲的时候却不能很清晰的讲明白。正好周末凑个时间,来试着写一下js的继承,以便自己将来翻看。

1、原型链继承

    function Father(name,age){
      this.name = name || 'father'
      this.age = age || '未知'
      this.likes = ['reading','music']
    }
    Father.prototype.say = function(){
      return `my name is ${this.name}`
    }
    function Child(name){
      this.name = name || 'child'
      this.sex = 'boy'
    }
    Child.prototype = new Father()

以上代码实现了两个类型FatherChild,通过创建Father的实例,并将该实例赋值给Child.prototype来实现了继承。原本存在于Father的实例的所有属性和方法,现在都存在于了Child.prototype上。试着来调用一下。

    var c1 = new Child()  
    console.log(c1.name)   //child   

此时我们可以证明父类的属性被子类所覆盖,严格意义上将,并不是被覆盖,而是js的在查找属性时,现在自身查找,如果找不到就去__proto__上面去找,如果没有就一直向上,知道找到或者最终为null为止。如下图:

可以很容易理解下面代码的输出结果:

    console.log(c1.age)   //未知      
    console.log(c1.say()) //my name is child
    console.log(c1.sex)   //boy

此外如果需要对Child添加方法,一定要在替换原型的语句之后。同时添加代码如下,可以发现原型链继承有一个很明显的缺点:

    var c2 = new Child('lili')
    c1.likes.push('run')
    console.log(c1.likes)   //["reading", "music", "run"]
    console.log(c2.likes)   //["reading", "music", "run"]

即引用数据类型,会被所有实例所共享,对其中一个实例改变时会引起其他实例的改变。

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

    function Father(name){
      this.name = name || 'father'
      this.likes = ['reading','music']
      this.say = function(){
        return `my name is ${this.name}`
      }
    }
    
    function Child(name){
      Father.call(this,name)
      this.sex = 'boy'
    }
    Father.prototype.show = function(){
      return `i likes ${this.likes.toString()}`
    }
    var c1 = new Child('huaihuai')
    var c2 = new Child()
    c1.likes.push('running')
    console.log(c1.likes)               //["reading", "music", "running"]
    console.log(c2.likes)               //["reading", "music"]
    console.log(c1.say === c2.say)      //fasle
    console.log(c1)

显然已经解决了引用类型属性被说有实例共享的问题。但是 函数也是引用数据类型,也没有办法共享了。也就是说所有实例中的函数,虽然功能一样,但是都不是同一个函数,相当于每实例化一个实例,就复制了一边代码。并且如下图:

对于父类原型上的方法show也是没有办法继承的!

3、组合继承

    function Father(name){
      this.name = name
      this.likes = ['reading','music']
    }
    Father.prototype.say = function(){
      return `my name is Father`
    }
    function Child(name){
      Father.call(this,name)    //构造函数继承    第一次调用父类的构造函数
    }
    Child.prototype = new Father()      //第二次调用父类构造函数
    Child.prototype.constuctor = Child
    var c1 = new Child('huaihuai')
    var c2 = new Child('shuaishuai')
    c1.likes.push('running')
    console.log(c1.likes)               //["reading", "music", "running"] 
    console.log(c2.likes)               //["reading", "music"]
    console.log(c1.say === c2.say)      //true
    console.log(c1)

此时即可以让不同的实例分别有自己的属性(即解决引用数据类型被所有实例共用的问题),又能使用同一个函数。避免了原型链继承和构造函数继承的缺陷,同时融合了他们的优点。是js中较为常用的继承方式。

4、原型式继承

    function object(proto){
      function F(){}
      F.prototype = proto
      return new F()
    }
    var person = {
      name:'shuaishuai',
      likes:['read','music']
    }
    var anotherPerson = object(person)
    var yetAnotherPerson = object(person)
    anotherPerson.likes.push('running')
    console.log(anotherPerson.likes)        //["read", "music", "running"]
    console.log(yetAnotherPerson.likes)     //["read", "music", "running"]
    console.log(person.likes)               //["read", "music", "running"]
    console.log(anotherPerson)

在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。

这意味着person.likes不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。实际上,这就相当于又创建了person对象的两个副本。

ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create()与 object()方法的行为相同。

    var person = {
      name:'shuaishuai',
      likes:['read','music']
    }
    var anotherPerson = Object.create(person)
    
    var yetAnotherPerson =  Object.create(person)
    anotherPerson.likes.push('running')
    console.log(anotherPerson.likes)        //["read", "music", "running"]
    console.log(yetAnotherPerson.likes)     //["read", "music", "running"]
    console.log(person.likes)               //["read", "music", "running"]

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相 同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属 性。

    var person = {
      name: "Nicholas",
      friends: ["Shelby", "Court", "Van"]
    };
    var anotherPerson = Object.create(person, {
      name: {
        value: "Greg"
      }
    });
    console.log(anotherPerson) 

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

5、寄生式继承

    function object(proto) {
      function F() {}
      F.prototype = proto
      return new F()
    }
    function createAnother(original) {
      var clone = object(original)
      clone.sayHi = function () {
        alert('hi')
      }
      return clone
    }
    var person = {
      name: "Nicholas",
      friends: ["Shelby", "Court", "Van"]
    };
    var anotherPerson = createAnother(person);
    var yetAnotherPerson = createAnother(person)
    yetAnotherPerson.friends.push('aaa')
    console.log(anotherPerson.friends)          //["Shelby", "Court", "Van", "aaa"]
    console.log(yetAnotherPerson.friends)       //["Shelby", "Court", "Van", "aaa"]
    console.log(yetAnotherPerson.sayHi === anotherPerson.sayHi)     //false

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示 范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一 点与构造函数模式类似。

6、寄生组合式继承

    function object(proto) {
      function F() {}
      F.prototype = proto
      return new F()
    }
    function inheritPrororype(Child, Father) {
      var prototype = object(Father.prototype)
      prototype.constructor = Child
      Child.prototype = prototype
    }
    function Father(name) {
      this.name = name || 'father'
      this.likes = ['reading', 'music']
    }
    Father.prototype.say = function () {
      return `my name is Father`
    }
    function Child(name) {
      Father.call(this, name) //构造函数继承    第一次调用父类的构造函数
    }
    inheritPrororype(Child,Father)
    var c1 = new Child('huaihuai')
    var c2 = new Child('shuaishuai')
    c1.likes.push('running')
    console.log(c1.likes)       //["reading", "music", "running"]
    console.log(c2.likes)       //["reading", "music"]
    console.log(c1.say === c2.say)  //true

寄生组合式继承,通过借用构造函数来继承属性,通过原型链的混入形式来继承方法。这种方法只调用了以此父类构造函数,并且因此避免了在子类的prototype上面创建不必要的多余的属性。因此是引用类型最理想的继承方法。

参考资料

《javaScript高级程序设计 第三版》

《javaScript权威指南 第六版》

个人觉得关于js的继承《javascript高级程序设计》比《javascript权威指南》更容易理解。