# 👉 继承

# 原型链继承

实现方式:以父类的实例作为子类的原型。

优点:子类可以继承父类的属性方法,也继承父类原型上的属性方法

缺点:

  1. 原型链继承无法多继承;

  2. 父类的实例属性 变成了 子类的原型属性,原型属性会被子类所有实例所共享。
    当原型属性是原始类型时,不会相互影响;但如果某个原型属性是引用类型时(即同享同一个内存地址引用),其中一个实例修改这个原型属性,将会影响到其他实例对象。

  3. 创建子类实例时,无法向父类构造函数进行动态传参。

原型链继承例子:

function Parent() {
    this.name = "parent";
    this.habit = ["exercise"];
}

Parent.prototype.getInfo = function() {
    console.log("my info: ", this.name, this.habit);
};

// 原型链继承
function Child() {}

// 重写Child原型等于Parent的实例(作为父类的实例,这样就拥有的父类的属性和方法),实现了原型链继承
Child.prototype = new Parent();

// child1可访问Parent属性,且可继承Parent原型对象上的属性方法(getInfo)
const child1 = new Child();
child1.getInfo(); // my info:  parent ["exercise"]

const child2 = new Child();
// child2 修改了引用类型属性habit,将会影响其他子类实例
child2.habit.push("sleep");
child2.name = "child2";

child2.getInfo(); // my info:  child2 ["exercise", "sleep"]

child1.getInfo(); // my info:  parent ["exercise", "sleep"]

# 构造函数继承

实现方式:在子类构造函数中,通过apply/call的方式调用父类构造函数

优点:

  1. 解决了原型链继承中子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call 多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类实例的属性和方法,不能继承其原型上的属性和方法
function Parent(name) {
    this.name = name;
    this.habit = ["exercise"];
}

Parent.prototype.getInfo = function() {
    console.log("my info: ", this.name, this.habit);
};

function Child(name) {
    // 只是通过call复制一份父类内部属性方法的副本,但子类和父类原型没有创建起任何连接关系,因此子类实例是无法访问父类原型对象上的属性方法
    Parent.call(this, name);
    // Parent.call(this, "我是传给父类的参数");
}

const child1 = new Child("child1");
console.log(child1); // Child { habit: ["exercise"], name: "child1" }
// child1.getInfo(); // Uncaught TypeError: child1.getInfo is not a function

const child2 = new Child("child2");
child2.habit.push("eat");
console.log(child2); //  Child { habit: ["eat", exercise], name: "child2" }

console.log(child1); //  Child { habit: ["exercise"], name: "child1" }

# 组合式继承(原型链+构造函数)

结合原型链继承和构造函数继承的优缺点,将两种方式结合使用,解决无法继承父类原型属性方法和父类的问题。

实现方式:在子类构造函数中,通过apply/call的方式调用父类构造函数,并且使将子类原型链继承父类原型上的属性和方法,这样既可以让每个实例都有自己的属性,又可以把方法定义在原型上以实现重用。

优点:

  1. 弥补了构造继承的缺点,现在既可以继承实例的属性和方法,也可以继承原型的属性和方法
  2. 既是子类的实例,也是父类的实例
  3. 可以向父类传递参数

缺点:

  1. 调用了两次父类构造函数,生成了两份实例
  2. constructor 指向问题
function Parent(name) {
    this.name = name;
    this.habit = ["exercise"];
}
Parent.prototype.getInfo = function() {
    console.log("my info: ", this.name, this.habit);
};

function Child(name) {
    // 第一次调用父类构造器 子类实例增加父类实例
    Parent.call(this, name);
}
// 第二次调用父类构造器 子类原型也增加了父类实例
Child.prototype = new Parent();

const child1 = new Child("child1");
child1.getInfo(); // my info: child1 ["exercise"]

const child2 = new Child("child2");
child2.habit.push("eat");
child2.getInfo(); // my info: child2 ["exercise", "eat"]

child1.getInfo(); // my info: child1 ["exercise"]

console.log(child2);
// Child {name: "child2", habit: Array(2)}
// __proto__: Parent
//   habit: ["exercise"]
//   name: undefined
//   __proto__:
//     getInfo: ƒ ()
//     constructor: ƒ Parent(name)
//     __proto__: Object

虽然这种方式能够解决无法继承父类原型属性方法的问题,但它也引入了新的问题:执行了两次父类构造函数,导致父类属性方法会被多拷贝一份至原型链上,这样造成了资源浪费(存储占用内存)。

# 组合式继承-优化 1

function Parent(name) {
    this.name = name;
    this.habit = ["exercise"];
}
Parent.prototype.getInfo = function() {
    console.log("my info: ", this.name, this.habit);
};

function Child(name) {
    Parent.call(this, name);
}

// 直接修改子类原型对象 指向 父类原型对象
Child.prototype = Parent.prototype;

const child1 = new Child("child1");
child1.getInfo(); // my info: child1 ["exercise"]

const child2 = new Child("child2");
child2.habit.push("eat");
child2.getInfo(); // my info: child2 ["exercise", "eat"]

child1.getInfo(); // my info: child1 ["exercise"]

// 此时父类的属性对象不会被拷贝多一份在子类原型上,但却指示constructor为Parent(因为Parent.prototype的constructor指向Parent)
// 但按常规出牌,理应该为Child才对
console.log(child2);
// Child {name: "child2", habit: Array(2)}
// __proto__:
//   getInfo: ƒ ()
//   constructor: ƒ Parent(name)
//   __proto__: Object

# 寄生组合继承

为了解决 组合式继承-优化 1 的问题,寄生组合继承方案出现了,就是将子类原型对象 constructor 指向子类本身就好啦!

function Parent(name) {
    this.name = name;
    this.habit = ["exercise"];
}
Parent.prototype.getInfo = function() {
    console.log("my info: ", this.name, this.habit);
};

function Child(name) {
    Parent.call(this, name);
}

// 直接修改子类原型对象 指向 父类原型对象
// function f{}; f.prototype = Parent.prototype; Child.prototype = new f();
// Child.prototype.__proto = Parent.prototype
Child.prototype = Object.create(Parent.prototype);

// 同时,也要修复 Child.prototype 的constructor指向为Child
Child.prototype.constructor = Child;

const child1 = new Child("child1");
child1.getInfo(); // my info: child1 ["exercise"]

const child2 = new Child("child2");
child2.habit.push("eat");
child2.getInfo(); // my info: child2 ["exercise", "eat"]

child1.getInfo(); // my info: child1 ["exercise"]

console.log(child2);
// Child {name: "child2", habit: Array(2)}
// __proto__:
//   getInfo: ƒ ()
//   constructor: ƒ Child(name)
//  __proto__: Object

以上的方案,已经算是完善的方案了。

为什么叫做寄生组合继承呢,其实它是结合了原型式继承寄生式继承的方式。

简单说说这两种继承方式:

  • 原型式继承:本质就是Object.create的底层实现原理,通过定义一个继承父类的临时类,让子类原型指向这个中间类。
function createObject(parent, properties = {}) {
    // 创建临时类
    function f() {}
    // 修改类的原型为parent, 于是f的实例都将继承parent上的方法
    f.prototype = parent;
    const newObj = new f();
    Object.defineProperties(newObj, properties);
    return newObj;
}

const child1 = createObject(Parent, {
    name: { value: "child1" },
});

这种方式依然存在原型链继承的父类引用类型值的属性会共享相同值问题。

  • 寄生式继承:寄生式和原型式方法相同,都需要定义一个继承父类的临时类,不同的是它将对子类例的修改放到也放到了函数中,将整个过程(创建、增强、返回)封装了起来。
function createChild(parent, properties) {
    // child.__proto__ === parent
    const child = Object.create(parent, properties);

    // 相当于把对子类的修改寄托到这个中间函数
    child.getInfo = function() {
        console.log("my info: ", this.name, this.habits);
    };

    return child;
}

这种方式依然存在原型链继承的父类引用类型值的属性会共享相同值问题 -.-。