在 JavaScript 中,子类可以通过将其原型对象prototype指向父类的实例或原型,从而继承父类的属性和方法。原型链负责在对象之间建立继承关系。
原型链继承
原型链继承是将子类的原型prototype指向父类的示例实现继承
function Parent() {
// 原型属性
this.name = "parent";
this.colors = ["red", "blue", "green"];
}
// 原型方法
Parent.prototype.sayName = function () {
console.log(this.name);
}
function Child() {
this.type = "child";
}
Child.prototype = new Parent()
const child = new Child()
const child2 = new Child();
child2.name = "child2";
console.log(child.name); // parent
console.log(child2.name); // child2
child.sayName(); // parent
child2.sayName(); // child2
console.log(child.type); // child
child.colors.push("yellow");
console.log(child.colors); // ["red", "blue", "green", "yellow"]
console.log(child2.colors); // ["red", "blue", "green", "yellow"]
通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,修改其中一个子类实例的父类引用类型的数据,其他子类实例也会跟着改变。
构造函数继承
构造函数继承是通过在子类原型中调用父类的call方法,然后改变this指向进行继承
function Parent1() {
this.name = "parent1";
this.colors = ["red", "blue", "green"];
}
Parent1.prototype.sayName = function () {
console.log(this.name)
}
function Child1() {
Parent1.call(this);
this.type = "child1";
}
const child3 = new Child1();
const child4 = new Child1();
console.log(child3.name); // parent1
// console.log(child3.sayName()); // 报错 Uncaught TypeError: child3.sayName is not a function
child3.colors.push("yellow");
console.log(child3.colors); // ["red", "blue", "green", "yellow"]
console.log(child4.colors); // ["red", "blue", "green"]
通过上述代码可以看出子类可以继承父类的属性,但不能继承父类的方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类实例不会受到影响。
组合继承
组合继承是结合上述两种继承方式集两家之大成进行继承。
function Parent2() {
this.name = "parent2";
this.colors = ["red", "blue", "green"];
}
Parent2.prototype.sayName = function () {
console.log(this.name);
};
function Child2() {
// 第二次调用 Parent3()
Parent2.call(this);
this.type = "child2";
}
// 第一次调用 Parent3()
Child2.prototype = new Parent2();
// 手动挂上构造器,指向自己的构造函数
Child2.prototype.constructor = Child2;
const s1 = new Child2();
const s2 = new Child2();
s1.colors.push("yellow");
console.log(s1.colors); // ["red", "blue", "green", "yellow"]
console.log(s2.colors); // ["red", "blue", "green"]
console.log(s1.sayName()); // 正常输出'parent2'
console.log(s2.sayName()); // 正常输出'parent2'
通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类实例不会受到影响。但是在调用过程中执行了两次父类的构造函数。
原型式继承
let Parent4 = {
name: "parent4",
colors: ["red", "blue", "green"],
getName: function () {
console.log(this.name);
}
}
let s3 = Object.create(Parent4);
let s4 = Object.create(Parent4);
s3.name = "child4";
s3.colors.push("yellow");
Parent4.sayHello = function () {
console.log("hello " + this.name);
}
console.log(s3.colors); // ["red", "blue", "green", "yellow"]
console.log(s4.colors); // ["red", "blue", "green", "yellow"]
s3.getName() // 正常输出'child4'
s4.getName() // 正常输出'parent4'
s3.sayHello() // 正常输出'hello child4'
s4.sayHello() // 正常输出'hello parent4'
通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类数据会受到影响。
寄生式集成
let Parent5 = {
name: "parent5",
colors: ["red", "blue", "green"],
getName: function () {
console.log(this.name);
}
}
function clone(original) {
let clone = Object.create(original);
clone.type = "child5";
clone.sayHello = function () {
console.log("hello " + this.name);
}
return clone;
}
Parent5.sayName = function () {
console.log(this.name);
}
let s5 = clone(Parent5);
let s6 = clone(Parent5);
s5.name = "child5";
s5.colors.push("yellow");
s5.sayHello() // 正常输出'hello child5'
s6.sayHello() // 正常输出'hello parent5'
s5.sayName() // 正常输出'child5'
s6.sayName() // 正常输出'parent5'
console.log(s5.colors); // ["red", "blue", "green", "yellow"]
console.log(s6.colors); // ["red", "blue", "green", "yellow"]
通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类数据会受到影响。
寄生组合式继承
function clone2(parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
/**
* 寄生组合式继承
* 优点:融合原型链继承和构造函数继承的优点
* 缺点:
*/
function Parent6() {
this.name = "parent6";
this.colors = ["red", "blue", "green"];
}
Parent6.prototype.sayName = function () {
console.log(this.name);
}
function Child6() {
Parent6.call(this);
this.type = "child6";
}
clone2(Parent6, Child6);
Child6.prototype.sayHello = function () { console.log("hello " + this.name) }
const s7 = new Child6();
const s8 = new Child6();
s7.colors.push("yellow");
console.log(s7.colors); // ["red", "blue", "green", "yellow"]
console.log(s8.colors); // ["red", "blue", "green"]
s7.sayName() // 正常输出'parent6'
s8.sayName() // 正常输出'parent6'
s7.sayHello() // 正常输出'hello parent6'
s8.sayHello() // 正常输出'hello parent6'
通过上述代码可以看出子类可以继承父类的属性及方法,当有多个子类实例的时候,改变其中一个子类实例的父类引用类型的数据,其他子类数据不会受到影响,这也是目前ES5中实现继承的最优方式。
class 继承
class Parent7 {
constructor() {
this.name = "parent7";
this.colors = ["red", "blue", "green"];
}
sayName() {
console.log(this.name);
}
}
class Child7 extends Parent7 {
constructor() {
super();
this.type = "child7";
}
sayHello() {
console.log("hello " + this.name);
}
}
const s9 = new Child7();
const s10 = new Child7();
s9.colors.push("yellow");
console.log(s9.colors); // ["red", "blue", "green", "yellow"]
console.log(s10.colors); // ["red", "blue", "green"]
s9.sayName() // 正常输出'parent7'
s10.sayName() // 正常输出'parent7'
s9.sayHello() // 正常输出'hello parent7'
s10.sayHello() // 正常输出'hello parent7'
从上述代码中看出,ES6的语法中使用class 和 extends 关键字实现继承,是现代 JavaScript 的推荐方式。更符合面向对象编程的习惯,内置支持继承,避免了手动设置原型和修复 constructor。
总结
| 继承方式 | 优点 | 缺点 |
|---|---|---|
| 原型链继承 | 简单,方法在原型上复用 | 引用类型属性共享,构造函数无法传参 |
| 构造函数继承 | 不共享引用类型属性,支持传参 | 方法无法复用,每次都要重新定义 |
| 组合继承 | 结合了原型链和构造函数继承的优点 | 父类构造函数会被调用两次 |
| 原型式继承 | 创建新对象非常简单 | 共享引用类型属性,不能传参 |
| 寄生式继承 | 灵活,可以增强对象功能 | 性能不佳,无法复用 |
| 寄生组合式继承 | 避免了组合继承的缺点,是 ES5 最优的继承方式 | 实现稍复杂 |
ES6 class 继承 | 简洁、现代,内置支持继承 | 需要现代浏览器或编译工具支持 |