写在前面
本文是近期学习总结的一套关于 JavaScript 继承方式的几种方法以及优缺点。
你即将收获
- JavaScript 中几种继承方式的实现原理, 以及各种继承方式的优缺点。
- 理解 JavaScript 中几种继承方式之间的联系。
在此, 极力推荐 《JavaScript高级程序设计》、《# ECMAScript 6 入门》
话不多说, 开始码!
1. 原型链继承
/*
构造函数、原型和实例之间的关系: 每个构造函数都有一个原型对象, 原型对象都包含一个指向构造函数的
指针, 而实例都包含一个原型对象的指针。
继承的本质就是: 复制(即重写原型对象), 代之以一个新类型的实例。
*/
function Child() {
this.name = "Arvin";
}
Child.prototype.getName = function () {
return this.name;
}
function newChild() {
this.newName = "Arvin1";
}
// 关键之处 --> 创建Child实例(构造函数), 并将该实例赋值给到 newChild的prototype(原型)
newChild.prototype = new Child();
newChild.prototype.getNewName = function () {
return this.newName;
}
var params = new newChild();
console.log("name值====", params.getName()); // Arvin
原型链方式书写相对简单, 但是存在缺点:
- 原型链继承最大的问题就是对于引用类型的值的原型。引用类型值的原型属性会被所有的实例共享。一句话概括就是: 多个实例对引用类型的操作会被篡改。
- 构造函数实例化对象无法进行参数传递
下面我们来整个例子
function Child() {
this.proData = ["25", "Arvin", "男"]; // 引用类型数据
}
function newChild() {}
// 关键之处 --> 创建name实例(构造函数), 并将该实例赋值给到 newName的prototype(原型)
newChild.prototype = new Child();
var params = new newChild();
// 往 name 中的 proData 属性添加一个新属性
params.proData.push("max"); // 实际上是操作 new newChild() 的原型上的属性
console.log("params--name值====", params.proData); // ["25", "Arvin", "男", "max"]
var params1 = new newChild();
console.log("params1--name值====", params1.proData); // ["25", "Arvin", "男", "max"]
// 由于 params, params1都是通过 new newChild()的方式取值(都是同一个实例), 所以结果一致
2. 构造函数继承
function Child() {
this.color = ["red", "blue", "green"];
}
function ChildCall() {
Child.call(this); // 继承 Arvin, 核心的一步
}
var params1 = new ChildCall();
params1.color.push("yellow");
console.log("params1更新后的值=====", params1.color); // ["red", "blue", "green", "yellow"]
var params2 = new ChildCall();
console.log("params2当前值=====", params2.color); // ["red", "blue", "green"]
构造函数继承方式核心代码就是 Child.call(this);, 创建子类实例时创建 Child 构造函数, 在ChildCall 的每个实例中都会将 Arvin 中的属性复制一份。
缺点:
- 构造函数继承方式, 只能继承父类的实例, 不能够继承原型上的属性和方法。
- 无法复用, 每个子类都有父类实例函数的副本, 这对于性能方面就比较差。
优点:
- 避免了引用类型的属性被所有实例共享的问题。
- 可以在构造函数实例化对象传参。
3. 组合继承
所谓的组合继承, 其实就是将 原型链继承 和 构造函数继承 两种继承方法合并, 使用构造函数来实现实例属性的继承。
function Child(name) {
this.name = "传入值--" + name;
this.color = ["red", "blue", "green"];
}
Child.prototype.getName = function () {
console.log(this.name);
}
function ChildCall(name, theme) {
// 在这里通过改变this指向实现继承 Child 的属性
Child.call(this, name); // 赋值 第二次调用
this.theme = theme; // 添加新属性
}
// 继承 Child 中的方法--> 设置子类型实例原型
ChildCall.prototype = new Child(); // 第一次调用
// 重写 ChildCall.prototypr的constructor属性, 指向自己的构造函数 ChildCall
// ChildCall.prototype.constructor = ChildCall;
ChildCall.prototype.getTheme = function () {
console.log("theme===", this.theme);
}
let params = new ChildCall("Arvin", "dark"); // 创建子类型实例
params.color.push("yellow");
console.log("params===", params, params.color);
let params1 = new ChildCall("艾文", "light"); // 创建子类型实例
console.log("params1===", params1, params1.color);
缺点:
- 组合模式的缺点就是在使用子类创建实例对象时, 其原型中会存在两份相同的属性和方法。
- 组合继承最大的缺点就是会调用两次父构造函数
优点:
- 融合原型链继承和构造函数的优点, 是 Javascript 中最常用的继承模式。
4. 原型式继承
原型式继承实现很简单, 就是利用一个空对象作为中介, 将某个对象直接赋值给空对象构造函数的原型。
function createObj(obj) {
// createObj() 对传入的参数(obj)执行一次潜复制, 将构造函数的 F 的原型直接指向传入的对象。
function F() {} // 创建一个空函数
F.prototype = obj; // 将该函数的原型指向传入的参数 obj
return new F(); // 返回构造函数
}
var person = {
name: "Arvin",
motto: "减低期待, 减少依赖 !",
other: [20, 25, 60, 55]
}
var personOne = createObj(person);
personOne.name = "personOne--Arvin";
personOne.other.push("Arvin--one");
console.log(personOne, personOne.name, personOne.other); // F {name: "personOne--Arvin"}, "personOne--Arvin", [20, 25, 60, 55, "Arvin--one"]
var personTwo = createObj(person);
personTwo.name = "personTwo--Arvin";
personTwo.other.push("Arvin--two");
console.log(personTwo, personTwo.name, personTwo.other); // F {name: "personTwo--Arvin"}, "personTwo--Arvin", [20, 25, 60, 55, "Arvin--one", "person-two"]
看上图输出结果我们可以看到, 修改
personOne.name 的值, personTwo.name 的值并没有发生改变, 欸很奇怪, 没达到预期, 就很棒! 其实这里原因就在 createObj 这个方法, 因为每次通过new操作符都会新建一个实例, 所以 personOne.name 这个操作其实是给 personOne 新增了一个 name 属性而已, 并非修改器原型上的 name 的值。
欸! other 这个属性的值居然发生了改变(因为原型链继承多个实例的引用类型属性指向相同, 存在篡改的可能), 这也是 原型式继承 的弊端之一。
缺点:
- 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
- 无法传递参数。
5. 寄生式组合
创建一个仅用于封装继承过程的函数, 该函数在内部一某种形式来做增强对象, 最后将对象返回。
function createObj(obj) {
var newObj = Object.create(obj); // 使用 ES5 的 Object.create()方法创建新对象
newObj.sayName = function (data) { // 增强对象
console.log("我是 Arvin", data);
}
return newObj; // 返回对象
}
var person = {
name: "Arvin",
colors: ["green", "red", "blue"]
}
var personOne = createObj(person);
console.log("personOne===", personOne); // {syaName: f}
personOne.sayName("哈哈"); // 我是 Arvin 哈哈
var personTwo = createObj(person);
personTwo.colors.push("yellow");
console.log("personTwo===", personTwo, personTwo.colors);
personTwo.sayName();
输出结果如下图:
如上图数据结果可以看出 寄生式继承和原型式继承缺点是一样的:
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
6. 寄生组合式继承---结合构造函数传递参数和寄生模式实现继承
function initPrototype(child, parent) {
var prototype = Object.create(parent.prototype); // 创建对象, 拿到父类实例的原型对象
prototype.constructor = child; // 增强对象, 避免重写的时候 constructor 属性存在空值情况, 直接将子类赋值过去
child.prototype = prototype; // 指定对象, 子类的原型对象指向父类的原型对象也就是新创建的对象 prototype
}
// 创建父类实例
function parent(name) {
this.name = name;
this.colors = ["blue", "red", "grey"]
}
// 在父类实例原型对象上添加方法
parent.prototype.getName = function () {
console.log('父类实例方法=====', this.name);
}
// 创建子类实例--支持传参
function child(name, theme) {
parent.call(this, name); // 构造函数, 增强子类实例, 支持传递参数避免篡改现象
this.theme = theme;
}
/*
在这里需要注意的是, 想要在子类原型上新增属性, 需要在父类原型指向子类这个方法(initPrototype)之后才行, 否则会报错
因为上面我们已经讲过, 子类原型是指向父类的原型上的, 所以你在 initPrototype 方法执行之前必然会报错。
*/
initPrototype(child, parent); // 调用方法, 将父类的原型指向子类
child.prototype.getChildName = function () {
console.log(this.theme);
}
var params1 = new child("Arvin", "dark");
console.log("params1======", params1); // params1====== child {name: "Arvin", colors: Array(3), theme: "dark"}
params1.getChildName(); // dark
优点: 来自于《JavaScript高级程序设计》
- 目前就所有的继承方式而言, 寄生组合式继承式最成熟的, 而且很多库的实现也是用该方法。
- 寄生组合式继承, 效率高,在实例代码中只调用了一次
Parent构造函数, 这也避免了在child的原型上prototype创建不必要的属性。 - 同时保持原型链不变, 并且能够正常使用
instanceof和isPrototypeOf()。
同时还有两种继承的方法:
- ES6 Object.assign(), 混入方式继承多个对象
- ES6 extends, ES6 类继承 这两个方法对于有看过 ES6文章的同学, 肯定很熟悉,我就不再做出介绍。如果没有看过的同学, 我在下面贴出了ES6官方文档地址, ES6入门教程