前言
本篇谈一下关于JavaScript 继承相关的知识。包括继承的方式,已经一些优缺点等。本篇知识点获取来源红宝书。
继承方式
关于JavaScript 继承的方式,有大概的如下六种:
- 原型链继承;
- 借用构造函数继承;
- 组合继承;
- 原型式继承;
- 寄生式继承;
- 寄生组合式继承;
这里我们分别展开来讲解。
原型链继承
主要思想:
利用原型让一个引用类型继承另一个引用类型的属性和方法。
实现代码:
function Parent () {
this.name = 'cook';
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child () {
this.age = 1
}
const child = new Child();
console.log(child.getName()) // 'cook'
缺点:
- 引用类型的原型属性会被所有实例共享;
- 创建子类型的实例时(
new Child()
),不能向超类型的构造函数(Parent
)中传递参数;
借用构造函数继承
借用构造函数也叫伪造对象或经典继承。
主要思想:
在子类型的构造函数的内部,调用超类型构造函数。
实现代码:
function Parent (name) {
this.colors = ['red', 'blue', 'green'];
this.name = name;
}
function Child (name) {
// 继承了Parent,并传递了参数
Parent.call(this, name)
}
const child1 = new Child('james');
child1.colors.push('pink');
console.log(child1.colors); // ['red', 'blue', 'green', 'pink']
console.log(child1.name); // 'james'
const child2 = new Child('henry');
console.log(child2.colors); // ['red', 'blue', 'green']
console.log(child2.name); // 'henry'
优点:
- 避免引用类型的原型属性会被所有实例共享;
- 子类型的构造函数(
Child
)能向超类型构造函数(Parent
)传递参数。
缺点:
- 方法都在构造函数中定义,无法函数复用(无法继承超类型的原型
Parent.prototype
定义的方法); - 每次创建实例都会创建一遍方法(每
new Child()
一次,就会执行Parent.call(this, name)
);
组合继承
组合继承也叫伪经典继承。
主要思路:
使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
实现代码:
function Parent (name) {
this.colors = ['red', 'blue', 'green'];
this.name = name;
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
// 继承了Parent,并传递了参数
// 通过借用构造函数来实现对实例属性的继承
Parent.call(this, name)
this.age = age;
}
// 使用原型链实现对原型属性和方法的继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child('james', 12);
child1.colors.push('pink');
console.log(child1.colors); // ['red', 'blue', 'green', 'pink']
console.log(child1.name); // 'james'
console.log(child1.age); // 12
const child2 = new Child('henry',15);
console.log(child2.colors); // ['red', 'blue', 'green']
console.log(child2.name); // 'henry'
console.log(child2.age); // 15
优点:
融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
- 避免引用类型的原型属性会被所有实例共享;
- 子类型的构造函数(
Child
)能向超类型构造函数(Parent
)传递参数; - 继承超类型的原型
Parent.prototype
定义的方法,实现函数复用;
缺点:
-
会调用两次父构造函数
Parent
。- 一次是设置子类型实例的原型的时候(
Child.prototype = new Parent()
); - 一次在创建子类型实例的时候;
const child1 = new Child('james', 12); // 内部调用了Parent.call(this, name)
- 一次是设置子类型实例的原型的时候(
-
存在实例属性过度赋值。使用
Child.prototype = new Parent()
,导致Child.prototype
和child1
都拥有构造函数实例的属性。
原型式继承
主要思路:
借助原型,基于已有的对象创建新对象。简单来说就是Object.create
的模拟实现。
实现代码:
function create(o) {
function F(){}
F.prototype = o;
return new F();
}
缺点:
-
引用类型的原型属性会被所有实例共享,这点跟原型链继承一样。
const parent = { colors: ['red', 'blue', 'green'], age: 1, } const child1 = create(parent); const child2 = create(parent); child1.age = 18; console.log(child2.age); // 1 child1.colors.push('pink'); console.log(child2.colors); // ['red', 'blue', 'green', 'pink']
寄生式继承
主要思路:
创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后返回对象。
实现代码:
function createAnother (o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
缺点:
跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
寄生组合式继承
主要思路:
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
实现代码:
function create(o) {
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(child, parent) {
// 使用寄生式继承来继承超类型的原型
const prototype = create(parent.prototype);
prototype.constructor = child;
// 将结果指定给子类型的原型
child.prototype = prototype;
}
function Parent (name) {
this.colors = ['red', 'blue', 'green'];
this.name = name;
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
// 通过借用构造函数来继承属性
Parent.call(this, name)
this.age = age;
}
// 使用的时候:
inheritPrototype(Child, Parent);
优点:
- 只调用了一次构造函数,避免了在Child.prototype上创建不必要的、多余的属性;
- 与此同时,原型链还能保持不变;
- 还能够正常使用 instanceof 和 isPrototypeOf。