JS中的继承

2020-08-09  本文已影响0人  25度凉白开

以下内容总结自《js高级程序设计 第三版》

在许多面向对象的语言中,都支持接口继承和实现继承。

接口继承:只继承方法名;实现继承:继承实际的方法。

而在JS中,只支持实现继承,其原理是依靠原型链来实现的。

一、 原型链

首先来说说什么是原型链,原型链是通过原型串联起多个对象的一条链。说到原型,又要提到构造函数和实例这两个概念。

构造函数:即构造实例对象的函数;

实例:就是对象;

在构造函数中,有一个prototype属性,也就是原型对象,这个对象有两个作用:

    1. constructor属性:指回构造函数本身;

    2. 公共的属性和方法:供构造函数创建的实例复用;

而构造函数构造出来的实例对象,则包含一个指向构造函数的原型对象的内部指针。这个内部指针通常是对外隐藏的,不过在ES6中可以通过Object.getProtoOf()方法获取,这里我们暂时使用{{Prototype}}来代指这个指针。

这里有一个非常重要的 "公式",这对学习原型链十分有用:

    对象.{{Prototype}} = 对象构造函数.prototype

来看一段代码

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}

function SubType() {
  this.subproperty = false
}

SubType.prototype = new SuperType()

SubType.prototype.getSubValue = function () {
  return this.subproperty
}
var instance = new SubType()
alert(instance.getSuperValue)

这里有两个类型:SuperType和SubType。SubType通过修改prototype属性为SuperType的实例从而实现了继承。通过SubType创建的实例就能够使用SuperType中的属性和方法。
instance这个实例调用getSuperType()方法的过程:

  1. 查询自身有无该方法,没有,执行2
  2. 查询原型链中有无该方法,找到方法,执行。

注意

1. 默认原型

所有的函数默认原型都是Object的实例。
所以所有的对象都可以调用Object.prototype中的方法。

2. 确定原型和实例关系的两种方法
  1. instanceof
    例:
instance instanceof Object    // true
instance instanceof SubType   // true
instance instanceof SuperType   // true
  1. isProtoTypeOf()
    例:
Object.prototype.isPrototypeOf(instance)  // true
SubType.prototype.isPrototypeOf(instance)  // true
SuperType.prototype.isPrototypeOf(instance)  // true
3. 重写子类原型中的方法会覆盖父类中的原方法

    function SuperType() {
      this.property = true
    }
    SuperType.prototype.getSuperValue = function () {
      return this.property
    }

    function SubType() {
      this.subproperty = false
    }

    SubType.prototype = new SuperType()
    
    SubType.prototype.getSubValue = function () {
      return this.subproperty
    }
    // 重写超类中的方法
    SubType.prototype.getSuperValue = function(){
      return false
    }

    var instance = new SubType()
    alert(instance.getSuperValue)  // false

还有一点需要注意,向子类原型中添加方法,不能使用字面量的方式,这会导致原型的指向改变,因为字面量对象已经是一个新对象了。

function SuperType() {
      this.property = true
    }
    SuperType.prototype.getSuperValue = function () {
      return this.property
    }

    function SubType() {
      this.subproperty = false
    }

    SubType.prototype = new SuperType()
    // 使用字面量会改变原型指向!!!
    SubType.prototype = {
      getSubValue: function () {
        return this.subproperty
      },
      someOtherMethod: function () {
        return false
      }
    }

    var instance = new SubType()
    alert(instance.getSuperValue) // error!

这时的SubType已经和SuperType没有关系了。

4. 原型链存在的问题

共享原型的引用属性
一旦超类中有一个引用类型的属性,子类的所有实例对象会共享这个属性,一个实例去改变该属性,其他实例都会发生变化。

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

    function SubType() {}

    SubType.prototype = new SuperType()

    var instance1 = new SubType()
    instance1.colors.push('black')
    alert(instance1.colors) // red,blue,green,black

    var instance2 = new SubType()
    alert(instance2.colors) // red,blue,green,black

无法向超类构造函数传参
子类实例创建时,是无法向超类传参的,只能被动的使用超类中的属性和方法。

二、借用构造函数

什么是借用构造函数呢?
看下例

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

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

    var instance1 = new SubType()
    instance1.colors.push('black')
    alert(instance1.colors) // red,blue,green,black

    var instance2 = new SubType()
    alert(instance2.colors) // red,blue,green

这里SubType借用了SuperType中的构造函数代码,看起来就像是在SubType中声明了一个colors数组一样,使得所有的实例都有属于自己的colors。

它解决了什么问题?
能向超类中传参了! 比原型链厉害一点:)

    function SuperType(name) {
      this.name = name
    }

    function SubType() {
      SuperType.call(this, "Hello")
      this.age = 18
    }

    var instance1 = new SubType()
    alert(instance1.name) // Hello
    alert(instance1.age) // 18

存在的问题
因为仅仅是借用构造函数,所以无法做到方法复用。这里可能会有疑问,方法在原型中定义不就行了嘛,恭喜你,你已经预想到下一节组合继承的内容了。

三、组合继承

组合继承,是非常经典的继承方式,也是JS中最常用的继承方式,使用了构造函数+原型的方式进行继承。构造函数使实例拥有自己的私有空间(私有属性和方法),原型保证了实例的的公共空间(公共属性和方法)。

    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
    }
    // 继承方法
    SubType.prototype = new SuperType()
    SubType.prototype.constructor = SubType
    SubType.prototype.sayAge = function () {
      alert(this.age)
    }

    var instance1 = new SubType('Hello', 18)
    instance1.colors.push('black')
    alert(instance1.colors) // red,blue,green,black
    instance1.sayName() // Hello
    instance1.sayAge() // 18

    var instance2 = new SubType('World', 19)
    alert(instance2.colors) // red,blue,green
    instance2.sayName() // World
    instance2.sayAge() // 19

那组合继承就完美无缺了吗?并不是的,下面来看看四五六节。

四、原型式继承

这个继承方式有点鸠占鹊巢的感觉,看下例:

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

    let person = {
      name: "Hello",
      friends: ['Fir1', 'Fir2', 'Fir3']
    }

    let anotherPerson = object(person)
    anotherPerson.name = "World"
    anotherPerson.friends.push('Fir4')

    var yetAnotherPerson = object(person)
    yetAnotherPerson.name = "Happy"
    yetAnotherPerson.friends.push('Fir5')

    alert(person.name) // Hello
    alert(person.friends) // Fir1,Fir2,Fir3,Fir4,Fir5

看到这里可能会有疑问,为什么name没变而friends就变了呢?这是因为object()这个函数对传入的参数进行了一次浅复制
何谓浅复制?即基本类型传值,引用类型传址。
通俗的讲,基本类型各用各的,引用类型共用一个。

下面来说说另外一个ES5中的方法
Object.create(par1,par2)
par1: 用作新对象原型的对象
par2: 额外的属性
这个方法在传一个参数的时候和上面例子中的object()方法行为相同;

    let person = {
      name: "Hello",
      friends: ['Fir1', 'Fir2', 'Fir3']
    }

    let anotherPerson = Object.create(person)
    anotherPerson.name = "World"
    anotherPerson.friends.push('Fir4')

    var yetAnotherPerson = Object.create(person)
    yetAnotherPerson.name = "Happy"
    yetAnotherPerson.friends.push('Fir5')

    alert(person.name) // Hello
    alert(person.friends) // Fir1,Fir2,Fir3,Fir4,Fir5

嗯,看起来没什么变化。
par2和Object.defineProperties()方法的第二个参数格式是相同的,通过这个参数传入的属性会覆盖原型对象上的同名属性。

    let person = {
      name: "Hello",
      friends: ['Fir1', 'Fir2', 'Fir3']
    }

    let anotherPerson = Object.create(person, {
      name: {
        value: 'World'
      }
    })
    alert(anotherPerson.name) // World

如果传入的是同名的引用类型属性,也会覆盖,就会避免共享这个问题了。

五、寄生式继承

寄生式继承和原型式继承相关,和工厂模式有点类似。

    function createAnother(o) {
      let clone = object(o)             // 通过调用object()函数创建一个新对象
      clone.sayHi() = function () {     // 增强对象
        alert('Hi')
      }
      return clone                      // 返回对象
    }

    let person = {
      name: "Hello",
      friends: ['Fir1', 'Fir2', 'Fir3']
    }

    let anotherPerson = createAnother(person)
    anotherPerson.sayHi()  // Hi
缺点

无法做到函数复用。
嗯? 又不能复用?那再用上原型组合一下?好,这就是下一节的内容了。

用处

为寄生组合式继承做铺垫

六、寄生组合式继承

听着名字有点长,不过不用慌,知道寄生继承组合继承已经算是了解本节的大部分内容了,来看个例子

// ===================这是组合继承========================================
    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)         // 第二次调用SuperType()
      this.age = age
    }
    
    SubType.prototype = new SuperType()  // 第一次调用SuperType()
    SubType.prototype.constructor = SubType
    SubType.prototype.sayAge = function () {
      alert(this.age)
    }

为什么要再看一遍组合继承呢?这次是来了解它的缺陷的。可以看上面的注释,在组合继承中,调用了两次超类构造函数。

    第一次调用: SubType.prototype 会获得name和colors两个属性
    第二次调用: 也就是在创建子类实例时调用,子类实例对象同样会获得name和colors这两个属性,
               同时会屏蔽原型中的对应同名属性。

嗯,这么一看,这么操作确实有点浪费。
所以,寄生式继承就登场了,它解决了两次调用的问题,可以说是继承方式中的王者。
寄生组合继承基本模式

    function inheritPrototype(subType, superType) {
      let prototype = object(superType.prototype)     // 创建对象
      prototype.constructor = subType                 // 增强对象
      subType.prototype = prototype                   // 指定对象
    }

这里只是实现了寄生组合继承的最简单形式。这里做了三步操作:
第一步:创建一个超类原型的副本
第二步:给这个副本添加constructor属性
第三步:子类原型指向这个副本
可能说这些有些难懂,没关系,我们可以和组合继承对比来讲

    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)  // 调用了一次SuperType()
      this.age = age
    }
    // 这步操作和组合继承不同
    inheritPrototype(SubType,SuperType)

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

区别:

  上面说过组合继承调用两次,而寄生组合继承只调用了一次,
  也就是inheritPrototype(SubType,SuperType)这步操作避免了一次多余的调用,          
  取而代之的是通过object(super.prototype)方法创建了原型指向SuperType.prototype一个新的对象,
  使SubType.protoType指向了新对象
  也就是 
      SubType.protoType = new SuperType() ==> SubType.protoType = 新对象
  一句话总结:“狸猫换太子”

完结。

上一篇 下一篇

猜你喜欢

热点阅读