JavaScript中的繼承模式
本文是前端學習筆記的第六篇,對應web前端開發JavaScript精英課js的第21課時,本篇主要寫關於JS中的四種繼承方式,這四種也可以說是整個JS繼承的發展史了
目錄
JavaScript中的繼承發展史
JS繼承一共可分為四種,根據不斷的發展進化由第一種進化到如今普遍使用的第四種
- 原型鏈
- 借用建構函式(通過call/apply)
- 共享原型
- 聖盃模式
1. 原型鏈
通過原型鏈的方式實現繼承,是最初級版的繼承,這種繼承方式存在一定的弊端,譬如我只想繼承某個建構函式的原型物件的一些屬性,但卻不得不一併繼承了屬於建構函式自己的一些屬性
<script>
Father.prototype.Firstname = "周";
function Father() {
this.englishName = "I am Father";
}
var father = new Father();
Son.prototype = father;
function Son() {
}
var son = new Son();
</script>
son此時只想繼承Father的FirstName,但卻因為原型鏈的緣故,不得不繼承了原本不需要的屬性EnglishName
2. 借用建構函式(通過call/apply)
通過call/apply函式,我們可以借用別的建構函式來為物件增添屬性。先複習一下call和apply的用法,二者都是用於重定義this的指向,區別是前者是通過 函式.call(物件引用,args0,args1...)的方式可以把物件引用取代前面函式中的this的位置進行操作,後者是通過函式.apply(物件引用,args[n])的方式改變函式中的this引用,也即是引數用陣列來表示
接下來看下面一個例子
<script> function Car(weight,height,speed) { CarFactory.call(this,weight,height,speed); } function CarFactory(weight,height,speed) { this.weight = weight; this.height = height; this.speed = speed; } var car = new Car('4900','160','1km/s'); </script>
此時在建立物件car時通過借用建構函式CarFactory,為自己添加了屬性weight,height,speed,當然準確來說這不能算繼承,只是借用別的建構函式為自己增加屬性,且這種方式也有缺點,若過多使用會造成編碼效率降低,程式碼冗餘
3. 共享原型
在第一第二種繼承方式都不理想的情況下,又發展出了第三種繼承模式:共享原型
看下面一個例子
<script>
Father.prototype.firstName = "周";
function Father() {
this.englishName = "I am Father";
}
Son.prototype = Father.prototype;
function Son() {
}
var son = new Son();
</script>
共享了Father.prototype與Son.prototype後,此時物件son相比第一種原型鏈的情況有了很大的改善,不再會繼承無關的屬性englishName,因為這個屬性是屬於用建構函式Father建立的物件,而此時並沒有用建構函式Father建立物件,只是共享了彼此的原型,也就是隻是獲得了屬性firstName
但是這種做法仍有缺陷,再看下面一個例子
<script>
Father.prototype.firstName = "周";
function Father() {
this.englishName = "I am Father";
}
Son.prototype = Father.prototype;
function Son() {
}
var son = new Son();
Son.prototype.firstName = '陳';
console.log(Father.prototype.firstName); // 陳
</script>
此時試圖修改Son的原型物件上的firstName屬性,但是因為共享原型的緣故,Father.prototype和Son.prototype的引用是相同的,那麼修改其中一個的屬性就會修改另外一個物件的屬性,這顯然不是我們希望看到的,因此,便提出了新的解決方案
4. 聖盃模式
既然修改原型物件會導致共享的原型物件的屬性改變,那麼是否可以在二者之間建一箇中間層隔開一下呢,基於這樣的考慮,聖盃模式便誕生了,看下面一個例子
<script>
function inherit(Orign,Target) {
function F () {}
F.prototype = Orign.prototype;
Target.prototype = new F();
Target.prototype.constructor = Target;
Target.prototype.uber = Orign.prototype;
}
function Son() {
}
Father.prototype.firstName = '周';
function Father() {
}
inherit(Father,Son);
var son = new Son();
</script>
原本的共享原型是直接Target.prototype = Orign.prototype,而這裡通過一個建構函式F,作為一箇中間層,使得Target.prototype修改的用建構函式 f 造出來的物件的屬性,而不會干預到Father.prototype的屬性值,這裡再提提另外兩行特別的程式碼
- Target.prototype.constructor = Target
- Target.prototype.uber = Orign
第一行是修改原型物件的構造器,我們知道,物件本身是沒有constructor屬性的,這個屬性在物件的原型物件身上的,而這裡son的原型物件是用建構函式F造出來的物件,本身系統預設給的prototype,其身上同樣沒有constructor,繼續在其原型物件身上找,也就是Father.prototype身上找,終於找到了constructor屬性,而constructor屬性表示的是物件的建構函式,因此如果沒有這句,那麼son的constructor便是Father.prototype身上的constructor,修改後,便變為了自己,更符合實際情況
第二行是為Son的原型物件新增一個uber屬性,值為Orign.prototype,因為此時已改變了構造器,此時不再知道其共享的到底是哪個原型物件,因此便新增一個屬性用於找回這個原型物件
當然,還可以更進一步,把聖盃模式寫的更加簡潔、結構清晰
<script>
var inherit = (function () {
var f = function () {}
return function (Orign,Target) {
f.prototype = Orign.prototype;
Target.prototype = new f();
Target.prototype.constructor = Target;
Target.prototype.uber = Orign.prototype;
}
}());
function Son() {
}
Father.prototype.firstName = '周';
function Father() {
}
inherit(Father,Son);
var son = new Son();
</script>
把整個共享原型的過程以立即執行函式的方式表現,程式碼可讀型大大提高,程式碼複用率也高,最重要的是,避免了對全域性變數的汙染,推薦平時如果需要用繼承就用這種做法