# 👉 继承
提到继承,就离不开最基本的原型链概念。这里简单回顾一下原型链的概念:
每个函数都有一个 prototype 属性指向它的原型对象,这个原型对象是这个函数作为构造函数时,所创建实例的原型。
实例通过
_proto_
指向构造函数原型对象原型对象通过
constructor
指向构造函数原型链就是多个对象通过
_proto_
连接起来的链条。示例:
function Person() {} const parent1 = new Parent() // parent1.__proto__ === Person.prototype // Person.prototype.__proto__ === Object.prototype // Object.prototype.__proto__ === null // 原型链:parent1 __proto__ -> Person.prototype __proto__ -> Object.prototype __proto__ -> null
# 原型链继承
实现方式:以父类的实例作为子类的原型。
优点:子类可以继承父类的属性方法,也继承父类原型上的属性方法
缺点:
原型链继承无法多继承;
父类的实例属性 变成了 子类实例的原型属性,原型属性会被子类所有实例所共享。
当原型属性是原始类型时,不会相互影响;但如果某个原型属性是引用类型时(即同享同一个内存地址引用),其中一个实例修改这个原型属性,将会影响到其他实例对象。创建子类实例时,无法向父类构造函数进行动态传参。
原型链继承例子:
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.__proto__ ---> Child.prototype .__proto__ ---> Parent.prototype .__proto__ ---> Object.prototype .__proto__ ---> null
// 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
的方式调用父类构造函数
优点:
- 解决了原型链继承中子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call 多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例(instanceof 检查不通过)
- 只能获取父类自身的属性和方法,不能继承父类原型上的属性和方法
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法(内存浪费)
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, "我是传给父类的参数");
}
// child1.__proto__ ---> Child.prototype .__proto__ ---> Object.prototype .__proto__ ---> null
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
的方式调用父类构造函数,并且使将子类原型链继承父类原型上的属性和方法,这样既可以让每个实例都有自己的属性,又可以把方法定义在原型上以实现重用。
优点:
- 弥补了构造继承的缺点,现在既可以继承实例的属性和方法,也可以继承原型的属性和方法
- 既是子类的实例,也是父类的实例
- 可以向父类传递参数
缺点:
- 调用了两次父类构造函数,生成了两份实例(内存浪费)
- 多余的属性会存在于 Child.prototype 中(冗余)
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();
Child.prototype.constructor = Child; // 修复构造函数指向
// 原型链:child1.__proto__ ---> Child.prototype=Parent.prototype .__proto__ ---> Object.prototype .__proto__ ---> null
// 原型闭环:child1.__proto__ ---> Child.prototype=Parent.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 instanceof Child); // true
console.log(child2 instanceof Parent); // true
console.log(child2);
// Child {name: "child2", habit: Array(2)}
// __proto__: Parent
// habit: ["exercise"]
// name: undefined
// constructor: ƒ Child(name)
// getInfo: ƒ ()
虽然这种方式能够解决无法继承父类原型属性方法的问题,但它也引入了新的问题:执行了两次父类构造函数,导致父类属性方法会被多拷贝一份至原型链上,这样造成了资源浪费(存储占用内存)。
# 寄生组合继承
为了解决组合式继承的问题,寄生组合继承方案出现了,就是将子类原型对象 constructor
指向子类本身就好啦!
优点:
- 只调用一次父类构造函数
- 避免在子类原型上创建不必要的属性
- 保持原型链不变(instanceof 和 isPrototypeOf 有效)
- 较理想的继承范式
缺点:实现相对复杂,需要额外辅助函数
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;
}
但这种方式依然存在原型链继承的父类引用类型值的属性会共享相同值
问题。