加载中...
  • JS 实现继承的几种方式 loading

    继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。
    下面我们就来看看 JavaScript 中都有哪些实现继承的方法。

    原型链继承


    原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

    原型链继承的主要思想是:重写子类的prototype属性,将其指向父类的实例
    下面我们结合代码来了解一下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function Animal (name) {
    // 属性
    this.name = name
    this.type = 'Animal'
    // 实例函数
    this.sleep = function () {
    console.log(this.name + '正在睡觉');
    }
    }
    // 原型函数
    Animal.prototype.eat = function (food) {
    console.log(`${this.name}正在吃${food}`);
    }

    // 子类
    function Cat (name) {
    this.name = name
    }
    // 原型继承
    Cat.prototype = new Animal()
    // 将Cat的构造函数指向自身
    Cat.prototype.constructor = Cat

    let cat = new Cat('Tom')
    console.log(cat.name) // Tom
    console.log(cat.type) // Animal
    cat.sleep() // Tom正在睡觉
    cat.eat('猫罐头') // Tom正在吃猫罐头

    在子类Cat中,我们没有增加type属性,因此会直接继承父类Animaltype属性。

    在子类Cat中,我们增加了name属性,在生成子类实例时,name属性会覆盖父类Animal属性值。

    同样因为Catprototype属性指向了Animal类型的实例,因此在生成实例Cat时,会继承实例函数和原型函数。

    需要注意:

    Cat.prototype.constructor = Cat

    如果不将Cat原型对象的constructor属性指向自身的构造函数,那将指向父类Animal的构造函数。

    原型链继承的优点


    简单,易于实现


    只需要设置子类的prototype属性指向父类的实例即可。

    可通过子类直接访问父类原型链属性和函数


    原型链继承的缺点


    子类的所有实例将共享父类的属性


    子类的所有实例将共享父类的属性会带来一个很严重的问题,父类包含引用值时,子类的实例改变该引用值会在所有实例中共享。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Animal () {
    this.skill = ['eat', 'jump', 'sleep']
    }
    function Cat () {}
    Cat.prototype = new Animal()
    Cat.prototype.constructor = Cat

    let cat1 = new Cat()
    let cat2 = new Cat()
    cat1.skill.push('walk')
    console.log(cat1.skill) // ["eat", "jump", "sleep", "walk"]
    console.log(cat2.skill) // ["eat", "jump", "sleep", "walk"]

    在子类实例化时,无法向父类的构造函数传参


    在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类关联,从而导致无法向父类的构造函数传递参数。

    无法实现多继承


    子类的prototype只能设置一个值,设置多个值时,后面的值会覆盖前面的值。

    构造函数继承(借助 call)


    构造函数继承的主要思想:在子类的构造函数中通过call()函数改变thi的指向,调用父类的构造函数,从而将父类的实例的属性和函数绑定到子类的this上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
      // 父类
    function Animal (age) {
    // 属性
    this.name = 'Animal'
    this.age = age
    // 实例函数
    this.sleep = function () {
    console.log(this.name + '正在睡觉');
    }
    }
    // 原型函数
    Animal.prototype.eat = function (food) {
    console.log(`${this.name}正在吃${food}`);
    }
    function Cat (name) {
    // 核心,通过call()函数实现Animal的实例的属性和函数的继承
    Animal.call(this)
    this.name = name
    }

    let cat = new Cat('Tom')
    cat.sleep() // Tom正在睡觉
    cat.eat() // Uncaught TypeError: cat.eat is not a function

    通过代码可以发现,子类可以正常调用父类的实例函数,而无法调用父类原型上的函数,这是因为子类并没有通过某种方式来调用父类原型对象上的函数

    构造继承的优点


    解决了子类实例共享父类属性的问题


    call()函数实际时改变父类Animal构造函数中this的指向,然后调用this指向了子类Cat,相当于将父类的属性和函数直接绑定到了子类的this中,成了子类实例的熟属性和函数,因此生成的子类实例中是各自拥有自己的属性和函数,不会相互影响。

    创建子类的实例时,可以向父类传参


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
       // 父类
    function Animal (age) {
    this.name = 'Animal'
    this.age = age
    }
    function Cat (name, parentAge) {
    // 在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承
    Animal.call(this, parentAge)
    this.name = name
    }

    let cat = new Cat('Tom', 10)
    console.log(cat.age)

    可以实现多继承


    在子类的构造函数中,可以多次调用call()函数来继承多个父对象。

    构造函数的缺点


    实例只是子类的实例,并不是父类的实例


    因为我们并未通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系。

    只能继承父类实例的属性和函数,并不能继承原型对象上的属性和函数


    与上面原因相同。

    无法复用父类的构造函数


    因为父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例的引用,这会造成不必要的内存消耗,影响性能。

    组合继承


    组合继承的主要思想:结合构造继承和原型继承的两种方式,一方面在子类的构造函数中通过call()函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中;另一方面,通过改变子类的prototype属性,继承父类的原型对象上的属性和函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    // 父类
    function Animal (age) {
    // 实例属性
    this.name = 'Animal'
    this.age = age
    this.skill = ['eat', 'jump', 'sleep']
    // 实例函数
    this.sleep = function () {
    console.log(this.name + '正在睡觉')
    }
    }
    // 原型函数
    Animal.prototype.eat = function (food) {
    console.log(`${this.name}正在吃${food}`)
    }

    // 子类
    function Cat (name) {
    // 通过构造函数继承实例的属性和函数
    Animal.call(this)
    this.name = name
    }
    // 通过原型继承原型对象上的属性和函数
    Cat.prototype = new Animal()
    Cat.prototype.constructor = Cat

    let cat = new Cat('Tom')
    console.log(cat.name) // Tom
    cat.sleep() // Tom正在睡觉
    cat.eat('猫罐头') // Tom正在吃猫罐头

    组合继承的优点


    既能继承父类实例的属性和函数,又能继承原型对象上的属性和函数


    既是子类的实例,又是父类的实例


    不存在引用属性共享的问题


    构造函数作用域优先级比原型链优先级高,所以不会出现引用属性共享的问题。

    可以向父类的构造函数中传参


    组合继承的缺点


    父类的实例属性会被绑定两次


    在子类的构造函数中,通过call()函数调用了一次父类的构造函数;在改写子类的prototype属性,生成的实例时又调用了一次父类的构造函数。

    寄生组合继承


    组合继承方案已经足够好,但是针对其存在的缺点,我们仍然可以进行优化。

    在进行子类的prototype属性的设置时,可以去掉父类实例的属性的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
      //父类
    function Animal (age) {
    // 实例属性
    this.name = 'Animal'
    this.age = age
    this.skill = ['eat', 'jump', 'sleep']
    // 实例函数
    this.sleep = function () {
    console.log(this.name + '正在睡觉')
    }
    }
    // 原型函数
    Animal.prototype.eat = function (food) {
    console.log(`${this.name}正在吃${food}`)
    }
    // 子类
    function Cat (name) {
    // 继承父类的实例和属性
    Animal.call(this)
    this.name = name
    }
    // 继承父类原型上的实例和属性
    Cat.prototype = Object.create(Animal.prototype)
    Cat.prototype.constructor = Cat
    let cat = new Cat('Tom')

    console.log(cat.name) // Tom
    cat.sleep() // Tom正在睡觉
    cat.eat('猫罐头') // Tom正在吃猫罐头

    其中最关键的语句:

    Cat.prototype = Object.create(Animal.prototype)

    只取父类Animal的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。

    这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销。

    整体看下来,这六种继承方式中,寄生组合式继承是这里面最优的继承方式。

    总结


    继承

    上一篇:
    性能优化:防抖和节流
    下一篇:
    Vue2的基本使用方法
    本文目录
    本文目录