這次,徹底弄清js的繼承方式

關于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時,sub2name也受到了影響。

將子類的原型對象指向超類型的實例的方法稱作是原型鏈繼承。這種繼承方式的缺點是:

  • 超類型的屬性會被所有實例共享
  • 子類的實例不能向超類型構造函數(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__

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容