關于js繼承的文章一直以來是數(shù)不勝數(shù),每隔一段時間就會復習一下,感覺明白了,但是過段時間又忘記了。有人說ES6的class不是用的挺好的嗎,還需要去了解ES5及之前的繼承原理做什么。別的不說,讀一讀vue2.0的源碼,class繼承有他的局限性,不利于代碼的分層和模塊的拆分,基于原型鏈去拓展方法可以很好地拆分功能模塊,極大的提高代碼的復用性。
本文是基于《JavaScript高級程序設計(第三版)》做的總結,有興趣的話,去讀一讀,它就像前端開發(fā)者的實用手冊一般,遇到不懂的知識點去查一查,翻一翻,逐步地填充自己的知識盲點,形成系統(tǒng)的知識體系。
首先創(chuàng)造一個超類型的構造函數(shù)Super,他擁有自己的靜態(tài)屬性name,以及原型鏈方法getSuper。再創(chuàng)造一個子類構造函數(shù)Sub。下面我們將使用6種方法去分析如何讓Sub繼承Super,并討論下各自的優(yōu)劣。
1.原型鏈繼承
function Super() {
this.name = ["super"];
}
Super.prototype.getSuper = function () {
return this.name;
}
function Sub() {}
Sub.prototype = new Super();
var sub1 = new Sub();
sub1.name.push("sub1");
var sub2 = new Sub();
sub2.name.push("sub2");
console.log(sub2.getSuper()) //["super", "sub1", "sub2"]
將Sub的原型對象Sub.prototype指向Super的實例,然后創(chuàng)建兩個Sub的實例sub1、sub2。這樣我們可以在Sub中繼承Super的屬性name以及原型鏈方法getSuper,然而在sub1中修改name時,sub2的name也受到了影響。
將子類的原型對象指向超類型的實例的方法稱作是原型鏈繼承。這種繼承方式的缺點是:
- 超類型的屬性會被所有實例共享
- 子類的實例不能向超類型構造函數(shù)傳參
2.構造函數(shù)繼承
function Super(name) {
this.name = name
}
Super.prototype.getSuper = function () {
return this.name;
}
function Sub(name) {
Super.call(this, name);
}
var sub1 = new Sub("Tom");
// console.log(sub1.getSuper()) //Uncaught TypeError
console.log(sub1.name) //Tom
var sub2 = new Sub();
console.log(sub2.name) //undefined
var sup = new Super()
console.log(sup.getSuper()) //undefined
在Sub中使用call去調用Super時,繼承了Super的所有靜態(tài)屬性。在實例sub1、sub2中,各自對name的修改也互不影響,做到了屬性不共享,同時子類的實例也能向超類型構造函數(shù)傳參。而這種方式的缺點也顯而易見:
- 不能繼承原型鏈方法:
console.log(sub1.getSuper()) //Uncaught TypeError
3.組合繼承
function Super(name) {
this.name = name
}
Super.prototype.getSuper = function () {
return this.name;
}
function Sub(name) {
Super.call(this, name); //第二次調用
}
Sub.prototype = new Super(); //第一次調用
Sub.prototype.constructor = Sub;
var sub1 = new Sub("Tom");
console.log(sub1.getSuper()) //Tom
console.log(sub1.name) //Tom
console.log(sub1 instanceof Sub) //true
console.log(sub1 instanceof Super) //true
var sub2 = new Sub();
console.log(sub2.name) //undefined
在子類Sub中,我們仍然使用call去繼承超類型的屬性,同時也使用原型鏈的繼承方式去繼承原型鏈的方法和屬性。這樣我們彌補了以上兩種繼承方式的三種不足。唯一美中不足的是:
- 調用了兩次超類型的構造函數(shù)
第一次是在使用原型鏈繼承Sub.prototype = new Super()時,調用了一次超類型構造函數(shù),第二次是在實例化Sub new Sub(),然后在Sub內使用call方法時,又調用了一次超類型構造函數(shù)。并且在之后的每次實例化子類sub1、sub2...的過程中,都會調用超類型的構造函數(shù),這種方式顯然不是我們愿意看見的。
以上三種繼承方式通屬于函數(shù)式繼承。
-------------------------------- 分界線 -----------------------------------
在沒有必要興師動眾地創(chuàng)建構造函數(shù),而只想讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。
什么是原型式繼承呢?
4.原型式繼承
先來看一個函數(shù):
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
首先創(chuàng)造了一個臨時的構造函數(shù)F,將F的原型指向傳進來的對象,再返回F的實例。等等,是不是和原型鏈繼承很類似?這樣,我們就完成了一次對對象的淺拷貝。來看一個例子:
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
var person = {
name: "Nicholas",
friends: ["Sherlly", "Van"]
}
// 在傳入一個參數(shù)的情況下,Object.create()和object()相同
// var people1 = Object.create(person);
var people1 = object(person);
people1.name = "Greg";
people1.friends.push("Rob")
var people2 = object(person);
people2.name = "Linda";
people2.friends.push("Barbie")
console.log(person.name) //Nicholas
console.log(person.friends) //["Sherlly", "Van", "Rob", "Barbie"]
原型式繼承和原型鏈繼承類似,區(qū)別是一個是對對象進行復制,另一個是對構造函數(shù)進行繼承。缺點也是一致的:
- 屬性會被共享
5.寄生式繼承
基于4,高級程序設計中還介紹了一種關于對象復制的方式:寄生式繼承。實質是基于4的一層封裝,來看下代碼:
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
function createAnother(o) {
var clone = object(o)
clone.sayHi = function () {
console.log("hi")
}
return clone;
}
var person = {
name: "Nicholas",
friends: ["Sherlly", "Van"]
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi()
關于這段篇幅的介紹比較少,本質上就是說,可以通過這種方式實現(xiàn)子類方法sayHi的復用。通過createAnother創(chuàng)造出來的對象,都擁有sayHi方法。這種封裝方式和工廠模式類似。
-------------------------------- 分界線 -----------------------------------
在前三種方法中,我們學會了對構造函數(shù)屬性,和原型鏈上屬性及方法的繼承。唯一不足的是需要調用兩次超類型的構造函數(shù)。在4、5方法中,我們學會了,對于對象的拷貝式繼承。所以,是時候迎來我們的終極解決方案了:寄生組合式繼承。
6.寄生組合式繼承
思考一下,在方式3-組合繼承中,如果我們需要優(yōu)化一次調用,那一定是第一次調用,對于原型鏈繼承的優(yōu)化,怎么優(yōu)化呢?方式4-原型式繼承恰巧滿足我們的需要。
Sub.prototype = new Super(),實質上就是完成一次對超類型原型對象的拷貝,看代碼:
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var clone = object(superType.prototype); //復制超類型的原型對象
clone.constructor = subType; //將構造函數(shù)指向子類型
subType.prototype = clone;
}
function Super(name) {
this.name = name
}
Super.prototype.getSuper = function () {
return this.name;
}
function Sub(name) {
Super.call(this, name); //第二次調用
}
// 優(yōu)化前:
// Sub.prototype = new Super(); //第一次調用
// Sub.prototype.constructor = Sub;
// 優(yōu)化后:
inheritPrototype(Sub, Super);
var sub1 = new Sub("Tom");
console.log(sub1.getSuper()) //Tom
console.log(sub1.name) //Tom
console.log(sub1 instanceof Sub) //true
console.log(sub1 instanceof Super) //true
var sub2 = new Sub();
console.log(sub2.name) //undefined
我們封裝了inheritPrototype這樣一個函數(shù),首先利用object(或Object.create())復制出超類型的原型對象,然后將原型對象的構造函數(shù)指向自身(抄完了別忘了把名字改成自己的:clone.constructor = subType,constructor相當于一張身份證,身份證上的名字一定得是自己),最后將拷貝出來的對象塞給子類的原型對象。至此,完成了子類對超類型的原型對象的繼承。
總結
讓我們來總結一下,結合方式一的原型鏈繼承和方式二的構造函數(shù)繼承,衍生出方式三的組合繼承。為了優(yōu)化組合繼承,引入了方式四原型式繼承,最終得到方式六寄生組合式繼承。這么理解,思路是不是清晰了很多?
-------------------------------- 完結 -----------------------------------
補充,有幾個概念需要關注下,后續(xù)更新:
構造函數(shù)、原型對象、實例三種關系
拓展原型鏈的方法: new、Object.create、Object.setPrototypeOf、__ proto__