组合继承
组合继承(Combination Inheritance)(也叫伪经典继承),指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
其背后的思路是使用原型链实现对原型对象的属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
🌰 示例:
function Parent(name) {
this.name = name;
this.attr = {
eye: 'blue',
hair: 'black',
skin: 'white',
};
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// 第二次调用 Parent()
// Child.prototype 又得到了 name 和 attr 两个属性
// 并对上次得到的属性值进行了覆盖
Parent.call(this, name);
this.age = age;
}
// 第一次调用 Parent()
// 使得子类实例的原型对象指向父类实例对象
// Child.prototype 得到了 name 和 attr 两个属性
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
// 第一个实例对象
let uzi = new Child('Uzi', 3);
uzi.attr.height = 80;
console.log(uzi.attr);
// { eye: 'blue', hair: 'black', skin: 'white', height: 80 }
uzi.sayName();
// 'Uzi'
uzi.sayAge();
// 3
// 第二个实例对象
let kat = new Child('Kat', 1);
console.log(kat.colors);
// { eye: 'blue', hair: 'black', skin: 'white' }
kat.sayName();
// 'Kat'
kat.sayAge();
// 1
实现步骤分解:
- 父类构造函数定义自身属性(
Parent
构造函数定义了name
和attr
) - 父类原型上定义方法(
Parent
的原型定义了一个方法sayName
) - 子类构造函数调用父类构造函数,传入参数,继承父类构造函数中的属性,随后子类构造函数又自定义自身的属性(
Child
构造函数在调用Parent
构造函数时传入了name
参数,紧接着又定义了它自己的属性height
。) - 子类构造函数的原型指向父类构造函数生成的实例(将
Parent
的实例赋值给Child
的原型) - 在子类构造函数的原型上定义方法(在
Child
的原型上定义了方法sayAge
) - 这样一来,就可以让两个不同的子类实例对象既分别拥有自己属性,又可以使用相同的方法
缺陷
无论什么情况下,都会调用两次父类构造函数:第一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型对象最终会包含父类型对象的全部实例属性,但不得不在调用子类型构造函数时重写这些属性。
组合继承优化
组合继承优化示例一:
// Before
Child.prototype = new Parent();
// After
Child.prototype = Parent.prototype;
这种优化方式的缺点是,子类实例对象的构造函数无法区分是子类构造函数还是父类构造函数。
📌 完美写法:寄生组合式继承
组合继承优化示例二:通过中间对象,继承父类原型对象,实现子类与父类的隔离
function Parent() {
this.name = 'Parent';
this.num = [0, 1, 2];
}
function Child() {
Parent.call(this);
thi.type = 'Child';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
借用构造函数
借用构造函数(Constructor Stealing),即在子类型构造函数的内部调用父类构造函数以实现对父类构造函数属性的继承。
🌰 示例:
function Parent() {
this.attr = {
eye: 'blue',
hair: 'black',
skin: 'white',
};
this.sayName = function () {
console.log('Name');
};
}
function Child() {
Parent.call(this);
this.sayHi = function () {
console.log('Hello world!');
};
}
let boy = new Child();
boy.attr.age = 3;
console.log(boy.attr);
// { eye: 'blue', hair: 'black', skin: 'white', age: 3}
let girl = new Child();
console.log(girl.attr);
// { eye: 'blue', hair: 'black', skin: 'white'}
在构造函数 Child
内通过 call
方法(或 apply
方法也可以),使得 Parent
的构造函数能在 Child
构造函数的环境下调用。
如此一来,子类构造函数 Child
上执行父类构造函数 Parent
中定义的所有对象初始化代码。
Child
的每个实例都会具有自己的继承与父类构造函数的属性的副本。
⚠️ 注意: 函数只不过是在特定环境中执行代码的对象,因此通过使用
apply
和call
方法也可以在新创建的对象上执行构造函数。
传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即 可以在子类型构造函数中向父类型构造函数传递参数。
function Parent(name) {
this.name = name;
}
function Child() {
//继承了 Parent,同时还传递了参数
Parent.call(this, 'Uzi');
//实例属性
this.age = 18;
}
const child = new Child();
console.log(child.name);
// 'Uzi'
console.log(child.age);
// 18
- 通过往父类型构造函数传递参数,能自定义需要继承的属性
- 为了确保子构造函数自身定义的属性或方法不被父构造函数生成的属性重写,可以在调用父类型构造函数后,再添加子类型构造函数中定义的属性
缺陷
- 只能继承父类实例对象的属性和方法,不能继承原型对象的属性和方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能