js 繼承的幾種方式詳解

不同于其他面向對象語言,js是一種弱類型語言,它沒有interface、implements、constructor、extends等關鍵字【后續(xù)在typescript中有實現(xiàn),es6中也有constructor、extends的語法糖】,js的繼承是利用原型鏈實現(xiàn)的。為什么這樣設計,看這里Javascript繼承機制的設計思想。

了解js繼承前首先應該明確幾個概念:

實例:let c1 = new Child() 這步操作叫做實例化,稱c1是Child的實例,實例化出來的都是對象。
繼承:讓子類具有父類的“方法、屬性”,當然父類還可以有父類,父類也叫超類、基類,子類也叫派生類。
原型鏈相關知識徹底理解js的原型鏈
js繼承達到的目標:父類構造函數(shù)內定義的屬性應該是私有的,也就是每個子類實例維護自己的,實例對象需要共享的屬性和方法都放在prototype對象下

方式一、原型鏈繼承:子類原型等于父類的實例

注意需要在子類中添加新的方法或者是重寫父類的方法時候,切記一定要放到替換原型的語句之后

// 定義父類
    function Parent() {
        this.name = "parent";
        this.colors = ["red", "blue", "yellow"]; // 父類構造函數(shù)內的引用類型數(shù)據(jù)
    }
    Parent.prototype.proArr = [1,2,3] // 父類原型上的引用類型屬性
    Parent.prototype.sayFather = function () {
        console.log("來自父類的吶喊")
    }

    // 幾種繼承主要是這部分在變動 start ------
   // 定義子類
    function Child() {
        this.type = "child"; // 在繼承父類屬性的基礎上,擴展子類屬性
    }
    Child.prototype = new Parent()
    Child.prototype.constructor = Child; // 修復原型鏈-Child.prototype.constructor應該指向自己
    // 在繼承父類屬性的基礎上,擴展子類方法
    Child.prototype.sayChild = function () {
        console.log("來自子類的吶喊")
    }
    // 幾種繼承主要是這部分在變動 end------

    // 調用:
    let c1 = new Child()
    let c2 = new Child()
    c1.name = '更改值類型屬性'
    c1.colors.push('green')
    c1.proArr.push('xxxxxxxxxx')

    console.log('---c1:', c1,'---c1.name:',c1.name,'---c1.colors:',c1.colors,'---c1.proArr:',c1.proArr)
    console.log('---c2:', c2,'---c2.name:',c2.name,'---c2.colors:',c2.colors,'---c2.proArr:',c2.proArr)
  • 特點:
    1、新實例可繼承的有:子類中構造函數(shù)定義的屬性,子類原型上的方法,父類構造函數(shù)中的屬性(共享),父類原型的方法。
    2.簡單,易于實現(xiàn)
  • 缺點:
    1、新實例無法向父類構造函數(shù)傳參。(案例中為了簡單,沒有示范傳參數(shù)的情況,可自己嘗試)
    2、繼承單一。
    3、所有新實例都會共享父類實例的屬性。(原型上的屬性是共享的,因此父類構造函數(shù)“引用類型的屬性”在一個實例上被修改,另一個實例的原型屬性也會被修改?。?,注意這里致命缺陷是父類構造函數(shù)內定義的屬性共享了,父類prototype上的屬性或者方法共享是沒有問題的---致命缺陷
    image.png

那么問題來了,為什么更改引用類型的會影響,而更改值類型不影響?

答:
其實‘修改’共享的引用類型的重點是“怎么修改”,如果采用直接賦值的方式如:
c1.colors=['green'] // 這樣只會在c1的自身(非繼承)屬性下新增一個colors并賦值['green'] ,也就是不會去更改c1.__proto__.colors為colors,因此這樣的更改不會影響到其他的子類實例

不知道我描述清楚沒,也就是直接賦值的方式是不會影響其他子類實例的,但是采用借助引用地址方式更改,
如:
數(shù)組類型的c1.colors[0]、push、splice、pop、unshift等等,
對象(別較真,就這里指一般的對象)如c1.xxx.xxx的方式修改,都會影響其他的

注意:在繼承中使用 Child.prototype = xxxx 的時候,需要修復constructor的指向,原因如下:

constructor的指向可以這樣理解:假設沒有繼承父類的操作,只定義子類

   // 定義子類
    function Child() {
        this.type = "child"; // 在繼承父類屬性的基礎上,擴展子類屬性
    }

此時:Child.prototype.constructor 是指向的 Child 的引用【注意:prototype是函數(shù)的一個屬性,是函數(shù)的原型對象。prototype只能夠被“函數(shù)”調用】
然后當執(zhí)行Child.prototype = new Parent()后,原型鏈接入父類,此時子類原本的constructor丟失【賦值了嘛,被覆蓋了嘛,不就丟了原本的信息】

不修復去執(zhí)行 console.log(c1.constructor) 返回Parent  其實是這樣找的c1.__proto__.__proto__.constructor 
修復后  console.log(c1.constructor) 返回Child,是這樣找的c1.__proto__.constructor

網(wǎng)上很多教程在提到繼承時都很少去修復constructor的指向,這里建議加上。關于constructor的理解可參考這篇文章:javascript中constructor指向問題,那么constructor有什么用呢?來來來,各位看官看這里JS中原型對象中的constructor的作用

方式二、構造繼承:在子類構造函數(shù)中使用call、apply或者bind等,以繼承父類構造函數(shù)中定義的屬性

      // 定義父類
    function Parent() {
        this.name = "parent";
        this.colors = ["red", "blue", "yellow"]; // 父類構造函數(shù)內的引用類型屬性
    }
    Parent.prototype.proArr = [1,2,3] // 父類原型上的引用類型屬性
    Parent.prototype.sayFather = function () {
        console.log("來自父類的吶喊")
    }

     // 幾種繼承主要是這部分在變動 start ------
     // 定義子類
    function Child() {
        Parent.call(this) // 關鍵就是這行代碼,若要傳參也是在這里調用
        this.type = "child"; // 在繼承父類屬性的基礎上,擴展子類屬性
    }
    // 在繼承父類屬性的基礎上,擴展子類方法
    Child.prototype.sayChild = function () {
        console.log("來自子類的吶喊")
    }
    // 幾種繼承主要是這部分在變動 end------

   // 調用:
    let c1 = new Child()
    let c2 = new Child()
    c1.name = '更改值類型屬性'
    c1.colors.push('green')
    //c1.proArr.push('xxxxxxxxxx') // 父類原型上的沒有被繼承,這里沒有proArr

    console.log('---c1:', c1,'---c1.name:',c1.name,'---c1.colors:',c1.colors,'---c1.proArr:',c1.proArr)
    console.log('---c2:', c2,'---c2.name:',c2.name,'---c2.colors:',c2.colors,'---c2.proArr:',c2.proArr)
  • 特點:
    1、只繼承了父類構造函數(shù)的屬性,沒有繼承父類原型的屬性和方法。
    2、解決了原型鏈繼承缺點1、2、3。
    3、可以繼承多個構造函數(shù)屬性(call多個)。
    4、在實例化子類時可向父類構造方法傳參。用call、applay等時可傳參。
  • 缺點:
    只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法。--- 致命缺陷


    image.png

方式三、構造組合繼承:結合原型鏈繼承和構造繼承:結合了兩種模式的優(yōu)點,傳參和復用

       // 定義父類
    function Parent() {
        this.name = "parent";
        this.colors = ["red", "blue", "yellow"]; // 父類構造函數(shù)內的引用類型屬性
    }
    Parent.prototype.proArr = [1,2,3] // 父類原型上的引用類型屬性
    Parent.prototype.sayFather = function () {
        console.log("來自父類的吶喊")
    }

    // 幾種繼承主要是這部分在變動 start ------
    // 定義子類
    function Child() {
        Parent.call(this)
        this.type = "child"; // 在繼承父類屬性的基礎上,擴展子類屬性
    }
    Child.prototype = new Parent()
    Child.prototype.constructor = Child; // 修復原型鏈-Child.prototype.constructor應該指向自己
    // 在繼承父類屬性的基礎上,擴展子類方法
    Child.prototype.sayChild = function () {
        console.log("來自子類的吶喊")
    }
    // 幾種繼承主要是這部分在變動 end------

    // 調用:
    let c1 = new Child()
    let c2 = new Child()
    c1.name = '更改值類型屬性'
    c1.colors.push('green')
    c1.proArr.push('xxxxxxxxxx')

    console.log('---c1:', c1,'---c1.name:',c1.name,'---c1.colors:',c1.colors,'---c1.proArr:',c1.proArr)
    console.log('---c2:', c2,'---c2.name:',c2.name,'---c2.colors:',c2.colors,'---c2.proArr:',c2.proArr)
  • 特點:
    1、可以繼承父類原型上的屬性,可以傳參,可復用。
    2、每個新實例引入的構造函數(shù)屬性是私有的。
  • 缺點:
    1.稍顯臃腫,父類構造方法中定義的屬性重復掛載到子類的原型上,這部分根本不會被訪問。
    2.調用了兩次父類構造函數(shù)(初始化的時候,Child.prototype = new Parent()一次,后續(xù)每次實例化子類的時候調一次),多耗一點內存。
    image.png

    再次強調:父類prototype上的屬性或者方法共享是沒有問題的,prototype的目的就是實現(xiàn)共享

方式四、寄生組合繼承,也叫做組合繼承的優(yōu)化 --- 推薦

寄生組合繼承實現(xiàn)方式:通過寄生方式,在組合繼承基礎上砍掉父類的實例屬性,子類的原型指向父類副本的實例從而實現(xiàn)原型共享。

組合繼承方法我們已經(jīng)說了,它的缺點是兩次調用父級構造函數(shù),為了解決這個問題只能砍掉一次調用。因為Parent.call(this)就是來解決父類構造函數(shù)內的屬性和方法共享問題的,因此必不可少,那么只能更改Child.prototype = new Parent()這行代碼,如下:

    // 定義父類
    function Parent() {
        this.name = "parent";
        this.colors = ["red", "blue", "yellow"]; // 父類構造函數(shù)內的引用類型屬性
    }
    Parent.prototype.proArr = [1,2,3] // 父類原型上的引用類型屬性
    Parent.prototype.sayFather = function () {
        console.log("來自父類的吶喊")
    }
        
       // 幾種繼承主要是這部分在變動 start ------
       // 定義子類
        function Child(){
            Parent.call(this);
            this.type = "child"; // 擴展父類屬性
        }

//        function createObj(o){
//            function F(){}
//            F.prototype = o;
//            return new F();
//        }
//
//        Child.prototype = createObj(Parent.prototype); 
        Child.prototype = Object.create(Parent.prototype); // 這里只將父類的prototype拿過來并使用Object.create(是一種創(chuàng)建對象的方式,它會創(chuàng)建一個中間對象)
        Child.prototype.constructor = Child; // 修復原型鏈 Child.prototype.constructor應該指向自己

        Child.prototype.sayChild = function(){console.log("來自子類的吶喊")} // 擴展父類方法
        // 幾種繼承主要是這部分在變動 end------

    // 調用:
    let c1 = new Child()
    let c2 = new Child()
    c1.name = '更改值類型屬性'
    c1.colors.push('green')
    c1.proArr.push('xxxxxxxxxx')

    console.log('---c1:', c1,'---c1.name:',c1.name,'---c1.colors:',c1.colors,'---c1.proArr:',c1.proArr)
    console.log('---c2:', c2,'---c2.name:',c2.name,'---c2.colors:',c2.colors,'---c2.proArr:',c2.proArr)

  • 特點:
    堪稱完美
  • 缺點:
    實現(xiàn)較為復雜

Object.create其實是干了這么件事,用上面這段代碼是一樣的,es5出現(xiàn)的 Object.create 替代了上面那段代碼(當然 Object.create 還做了其他處理,不過在這里可以看做是一樣的)。


image.png

這里有幾個問題:
問題1:為什么不直接用 Child.prototype = Parent.prototype,這樣既沒有調用父類構造也讓子類擁有了父類原型上的屬性方法?

    Child.prototype = Parent.prototype; 
    Child.prototype.constructor = Child; // 修復原型鏈 Child.prototype.constructor應該指向自己
    // 在繼承父類屬性的基礎上,擴展子類方法
    Child.prototype.sayChild = function () {
        console.log("來自子類的吶喊")
    }

這個問題比較簡單,Child不僅繼承了父類原型上的方法和屬性,還有自己的方法,如果使用上面的方式,
那么Parent.prototype上也加上了sayChild方法,那么由Parent實例化的其他對象或者Parent的子類都將受到影響

問題2:為什么不直接用 Child.prototype.__proto __ = Parent.prototype,而要使用Object.create?

Object.create的原理就是生成一個新對象,該新對象的 __proto__ 指向現(xiàn)有對象,而且我們使用Child.prototype = Object.create(Parent.prototype)其實也只是把Parent的原型拿去放入函數(shù)副本中,并不涉及Parent內部構造函數(shù)內的屬性。

綜上,我認為Child.prototype.__proto __ = Parent.prototype也是可以的,但是通常我們不會這樣直接操縱__proto __去賦值,因此了解即可。
(當然也可能我的水平不夠沒能看出問題,如果哪位小伙伴有準確答案請在評論區(qū)告訴我,我也會進行更正)

評論區(qū)大神的指點:
阮一峰的ES6有說明過,__proto__屬性不是語言本身的特性,而是各大廠商添加的私有屬性,盡量不直接使用__proto__防止對環(huán)境產(chǎn)生依賴

方式五、實例繼承:子類對父類實例進行擴展,子類本身不在原型鏈上

    // 定義父類
    function Parent() {
        this.name = "parent";
        this.colors = ["red", "blue", "yellow"]; // 父類構造函數(shù)內的引用類型屬性
    }
    Parent.prototype.proArr = [1,2,3] // 父類原型上的引用類型屬性
    Parent.prototype.sayFather = function () {
        console.log("來自父類的吶喊")
    }

    // 幾種繼承主要是這部分在變動 start ------
    function Child(){
        var instance = new Parent();
        instance.type = "child"; // 擴展父類屬性
        instance.sayChild = function(){console.log("來自子類的吶喊")} // 擴展父類方法
        return instance;
    }
    // 幾種繼承主要是這部分在變動 end ------

    // 調用:
    let c1 = new Child()
    let c2 = new Child()
    c1.name = '更改值類型屬性'
    c1.colors.push('green')
    c1.proArr.push('xxxxxxxxxx')

    console.log('---c1:', c1,'---c1.name:',c1.name,'---c1.colors:',c1.colors,'---c1.proArr:',c1.proArr)
    console.log('---c2:', c2,'---c2.name:',c2.name,'---c2.colors:',c2.colors,'---c2.proArr:',c2.proArr)
  • 特點:
    不限制調用方式,不管是new 子類()還是子類(),返回的對象具有相同的效果
  • 缺點:
    實例是父類的實例,不是子類的實例
    不支持多繼承

繼承的正常思路是子類實例 <- 子類 <-父類,而當前這種方式的思路是子類實例 <-父類,也就是實例其實是父類的實例,不是子類的實例,從原型鏈上也能看出來。

image.png

方式六、其他繼承 --了解思路即可

個人覺得掌握上面的繼承方式就行了,下面這幾種麻煩也不好用,這里只粗略過一下。

    // 定義父類
    function Parent() {
        this.name = "parent";
        this.colors = ["red", "blue", "yellow"]; // 父類構造函數(shù)內的引用類型屬性
    }
    Parent.prototype.proArr = [1, 2, 3] // 父類原型上的引用類型屬性
    Parent.prototype.sayFather = function () {
        console.log("來自父類的吶喊")
    }
//  ----------  1.原型繼承  start------------

    var object = function (obj) {
        function F() {};//臨時構造函數(shù)
        F.prototype = obj;//傳入對象obj作為臨時構造函數(shù)的原型對象
        return new F();//返回臨時構造對象實例
    }

    // 這部分在變動 start ------
    var c1 = object(new Parent())
    var c2 = object(new Parent())
    // 這部分在變動 end ------

    c1.colors.push('green')
    console.log('c1:', c1)
    console.log('c2:', c2)
/*
個人覺得這個和實例繼承差不多,只是中間再加了一層F,而且沒有擴展屬性和方法
*/
//  ----------  1.原型繼承  end------------


//  ----------  2.寄生式繼承  start------------
// 寄生式繼承就是把原型式繼承再次封裝,然后在對象上擴展新的方法,再把新對象返回

  var object = function (obj) {
        function F() {};//臨時構造函數(shù)
        F.prototype = obj;//傳入對象obj作為臨時構造函數(shù)的原型對象
        return new F();//返回臨時構造對象實例
    }

   // 這部分在變動 start ------
   var Child = function(obj){
        var sub = object(obj)
        sub.type = "child"; // 擴展父類屬性
        sub.sayChild = function(){console.log("來自子類的吶喊")} // 擴展父類方法
       return sub
    }
    var c1 = Child(new Parent())
    var c2 = Child(new Parent())
    // 這部分在變動 end ------

    c1.colors.push('green')
    console.log('c1:', c1)
    console.log('c2:', c2)
/*
寄生式繼承和實例繼承對比那就真的只是中間再加了一層F的區(qū)別
*/
//  ----------  2.寄生式繼承  end------------

//  ----------  3.拷貝繼承  start------------
    // 這部分在變動 start ------
    function Child() {
        var instance = new Parent();
        for (var p in instance) {
            Child.prototype[p] = instance[p];
        }
        this.type = "child"; // 擴展父類屬性
    }
    // 在繼承父類屬性的基礎上,擴展子類方法
    Child.prototype.sayChild = function () {
        console.log("來自子類的吶喊")
    }

    // 調用
    var c1 = new Child()
    var c2 = new Child()
    // 這部分在變動 end------

    c1.colors.push('green')
    console.log('c1:', c1)
    console.log('c2:', c2)
/*
拷貝繼承是,子類實例 <-子類,和實例繼承類似,不同之處在于子類中是將父類的實例遍歷,
這樣父類中構造函數(shù)內的屬性和原型上的屬性方法都將掛載到子類的原型上,后面實例化時也是返回子類實例化的對象
特點:
   支持多繼承
缺點:
   效率較低,內存占用高(因為要拷貝父類的屬性)
   無法獲取父類不可枚舉的方法(不可枚舉方法,不能使用for in 訪問到)
*/
//  ----------  3.拷貝繼承  end------------

方式七、es6繼承

 // 定義父類
    class Parent{
        static proArr = [1,2,3] // 要多個子類實例共享部分數(shù)據(jù)可以使用 static
        constructor(){
            this.name = "parent";
            this.colors = ["red", "blue", "yellow"];
        }

        sayFather(){
            console.log("來自父類的吶喊")
        }
    }

    // 定義子類
    class Child extends Parent{ // 繼承父類且擴展
        constructor(){
            super();
            this.type = "child"; // 擴展父類屬性
        }

        sayChild() {
            console.log("來自子類的吶喊")
        }
    }
    // 調用:
    let c1 = new Child()
    let c2 = new Child()
    c1.name = '更改值類型屬性'
    c1.colors.push('green')
    Parent.proArr.push('xxxxxxxxxx')
    console.log('Parent.proArr:',Parent.proArr)
    console.log('不知道父類的時候可根據(jù)原型鏈找父類:',c2.__proto__.__proto__.constructor.proArr)


    console.log('---c1:', c1,'---c1.name:',c1.name,'---c1.colors:',c1.colors,'---c1.proArr:',c1.proArr)
    console.log('---c2:', c2,'---c2.name:',c2.name,'---c2.colors:',c2.colors,'---c2.proArr:',c2.proArr)
  • 特點:
    相比es5的原型鏈方式繼承實現(xiàn)方式簡單很多,傳參也方便,便于理解書寫
  • 缺點:
    不支持低版本瀏覽器,不支持ie,需要借助babel轉成es5才能執(zhí)行
    image.png

    可以看到結構和es5繼承發(fā)生了些變化,但大體不變。注意:根據(jù)原型鏈找到的constructor不一定可信,因為是可以改的

注意子類中在“this前”調用了super,因為子類沒有自己的this,調用super是為了將子類構造函數(shù)向上傳遞給父類,父類調用這個構造函數(shù)并生成this對象返回,可參考ES6中派生類的Super為什么一定要在使用this前調用

擴展:typescript中的繼承

  // 定義父類
  class Parent{
    name:string;
    colors:Array<string>;
    static proArr:Array<number> = [1,2,3] // 要多個子類實例共享部分數(shù)據(jù)可以使用 static
    constructor(){
        this.name = "parent";
        this.colors = ["red", "blue", "yellow"];
    }

    sayFather():void{
        console.log("來自父類的吶喊")
    }
}

// 定義子類
class Child extends Parent{ // 繼承父類且擴展
    type:string;
    constructor(){
        super();
        this.type = "child"; // 擴展父類屬性
    }

    sayChild():void {
        console.log("來自子類的吶喊")
    }
}
// 調用:
let c1 = new Child()
let c2 = new Child()
c1.name = '更改值類型屬性'
c1.colors.push('green')
Parent.proArr.push(999)
console.log('Parent.proArr:',Parent.proArr)
// console.log('不知道父類的時候可根據(jù)原型鏈找父類:',c2.__proto__.__proto__.constructor.proArr)

console.log('---c1:', c1,'---c1.name:',c1.name,'---c1.colors:',c1.colors)
console.log('---c2:', c2,'---c2.name:',c2.name,'---c2.colors:',c2.colors)

這樣會報錯,因此不能訪問__proto __


image.png

參考網(wǎng)址:
https://www.cnblogs.com/ranyonsue/p/11201730.html
https://www.cnblogs.com/humin/p/4556820.html

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容