【JavaScript】继承

79 阅读5分钟

前言

继承JavaScript的三座大山之一。

优点:可以更好地复用以前的开发代码;缩短开发周期、提升开发效率

对象相关知识请参考文章:【JavaScript】【对象】Object

在面向对象的编程语言中,都有一个对象(子类)可以从另外一个对象(父类)继承属性和方法,从而减少重复的代码。

在js中继承是通过原型和原型链的方式实现的,每个对象都有一个指向原型对象的链接,被称为原型链。当我们访问对象的属性或者方法时,如果该对象没有定义这个属性或者方法时,js就会一直沿着原型链往上查找,直到找到为止。

一、继承种类

1.1 原型链继承

  • 实现方式:将子类的原型链指向父类的对象实例。通俗讲,就是直接在原型链上写继承

    function Animal (){
        this.species = 'animal';
    }
    // 原型上的方法
    Animal.prototype.eat = function (){
        console.log('eating')
    }
    
    function Cat(name,color){
        this.name = name;
        this.color = color;
    }
    // 将 Animal 的实例挂载到 Cat 的原型链上
    Cat.prototype = new Animal();
    
    var cat1 = new Cat('福来','乳白色');
    console.log(cat1.species); // animal
    cat1.eat(); // eating
    
    var cat2 = new Cat('包子','蓝猫');
    

    cat1的打印值截图:

    image.png

    cat2的打印值截图:

    image.png

  • 优点:可以继承构造函数的属性/方法、父类构造函数的属性/方法,父类原型的属性/方法

  • 缺点

    • 子类实例共享父类引用类型属性
    • 在创建子类型的实例时,不能向父类型的构造函数中传递参数
    • 无法实现继承多个
    • 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
    function Animal (){
        this.species = 'animal';
        this.xingge = ["乖巧"]
    }
    Animal.prototype.eat = function (){
        console.log('eating')
    }
    
    function Cat(name,color){
        this.name = name;
        this.color = color;
    }
    Cat.prototype = new Animal();
    
    var cat1 = new Cat('福来','乳白色');
    var cat2 = new Cat('包子','蓝猫');
    
    cat1.xingge.push("可爱");
    
    console.log(cat1.xingge, cat2.xingge) // 都是:['乖巧', '可爱']
    
  • 原理

    子类实例cat1__proto__指向Cat的原型链prototype,而Cat.prototype指向Animal类的对象实例,该父类对象实例的__proto__指向Animal.prototype,所以Cat可继承Animal的构造函数属性、方法和原型链属性、方法

1.2 构造函数继承

  • 实现方式:在子类构造函数中使用 callapply 劫持父类构造函数方法,并传入参数

    function Parent(name, id){
      this.id = id;
      this.name = name;
      this.printName = function(){
        console.log(this.name);
      }
    }
    Parent.prototype.sayName = function(){
      console.log(this.name);
    };
    function Child(name, id){
      Parent.call(this, name, id);
      // Parent.apply(this, arguments);
    }
    var child = new Child("福来", "1");
    child.printName(); // 福来
    child.sayName() // Error => Uncaught TypeError: child.sayName is not a function
    
    console.log(child instanceof Child) // true
    console.log(child instanceof Parent) // false
    
  • 优点

    • 创建子类实例时,可以向父类传递参数(可以解决原型链继承的缺点)
    • 可以实现多继承(call多个父类对象)
  • 缺点:不可继承父类的原型链方法,构造函数不可复用

  • 原理:使用 callapply更改子类函数的作用域,使this执行父类构造函数,子类因此可以继承父类共有属性

1.3 组合继承

function MaoMao(name, breed) {
    this.name = name || "";
    this.breed = breed || "";
    this.sleep = function () {
        console.log(this.name + "在睡觉!");
    };
    this.xingge = ["乖巧"]
}

MaoMao.prototype = {
    eat: function() {
        console.log(this.name + "在吃饭")
    },
    play: function () {
        console.log(this.name + "在玩!");
    }
};

function YingDuan(name) {
    MaoMao.call(this, name, "英短长毛");
    // MaoMao.apply(this, arguments);
}

YingDuan.prototype = new MaoMao()

var fulai = new YingDuan("福来")
var baozi = new YingDuan("包子")

image.png

修改baozi的属性xingge: baozi 和 fulai的xingge属性值不一样

image.png

  • 优点
    • 可以继承父类原型上的属性,且可传参
    • 每个新实例引入的构造函数是私有的
  • 缺点
    • 会执行两次父类的构造函数(一次是在创建子级原型的时候;另一次是在自己构造函数内部)。消耗较大内存,子类的构造函数会代替原型上的那个父类构造函数
  • 原理:综合使用构造函数继承和原型链继承
    • 利用原型链继承,实现原型对象方法的继承
    • 利用构造函数继承,实现属性的继承,且可传参

1.4 原型式继承

function maomao(obj) {
    function YingDuan() {};
    YingDuan.prototype = obj;

    return new YingDuan();
}

const options = { name: "福来", aihao: { aihai1: "逗猫棒" } };
var fulai = maomao(options)
var baozi = maomao(options)

console.log(fulai, baozi)

image.png

// 修改fulai的aihao属性值
fulai.aihao.aihai1 = "滚球球"

console.log(fulai, baozi)

image.png

  • 优点
  • 缺点:(与原型链继承的缺点相同)
    • 子类实例共享父类引用类型属性
    • 在创建子类型的实例时,不能向父类型的构造函数中传递参数
  • 原理:通过借助原型,基于已有对象创建新的对象,是对参数对象的一种浅拷贝。

1.5 寄成式继承

function maomao(obj) {
    function YingDuan() {};
    YingDuan.prototype = obj;

    return new YingDuan();
}

function setYingDuanInfo(obj) {
    const yingduan = maomao(obj);
    yingduan.eatting = function() {
        console.log("猫猫在吃饭...")
    }

    return yingduan;
}

const options = { name: "福来", aihao: { aihai1: "逗猫棒", aihao2: "猫抓板" } };
var fulai = setYingDuanInfo(options)
var baozi = setYingDuanInfo(options)

console.log(fulai, baozi)

image.png

// 修改fulai的aihao属性值
fulai.aihao.aihai3 = "滚球球"

console.log(fulai, baozi)

image.png

  • 优点
  • 缺点:(同原型式继承)寄生式继承为构造函数新增属性和方法,增强了函数属性。并且新添加函数方法无法实现函数复用
  • 原理:是创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
    • 核心:在原型式继承的继承之上,增强对象,返回构造函数
    • 函数的主要作用是为构造函数新增属性和方法,以增强函数

1.6 寄生组合式继承

function inherit(children, parent) {
  // 创建对象 - 创建 父类原型的一个副本
  let prototype = Object.create(parent.prototype);
  // 增强对象 - 弥补因重写原型而失去的默认的constructor属性
  prototype.constructor = children;
  // 指定对象 - 将新创建的对象复制给子类的原型
  children.prototype = prototype;
}

// 父类初始化实例属性和原型属性
function Parent(name) {
  this.name = name;
  this.num = [0, 1, 2];
}
Parent.prototype.sayName = function() {
  console.log(this.name, "说,今天也很幸运的呢")
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inherit(Child, Parent);

// 新增子类原型属性
Child.prototype.sayAge = function() {
  console.log(this.age);
};

var fulai = new Child("福来", 1)

console.log(fulai instanceof Child); // true
console.log(fulai instanceof Parent); // true
console.log(fulai.sayName());  // 福来 说,今天也很幸运的呢

image.png

  • 优点
    • 只调用了一次Child构造函数
    • 避免了在Child.prototype上创建不要的、多余的属性
    • 原型链保持不变
  • 缺点
  • 原理:构造函数继承 + 组合继承
    • 借用构造函数来继承不可共享的属性
    • 通过寄生式来继承方法和可共享的属性

参考: