[打牢基礎系列]JavaScript的類與繼承

前端古力士發表於2019-08-15

什麼是繼承

  1. 繼承的概念

繼承簡單來說就是使子類擁有父類的屬性和方法,而不需要重複編寫相同的程式碼.

舉個例子,飛機有客運機和戰鬥機,這兩種機型的共有屬性是顏色,公有方法是起飛,但是客運機的承載的是乘客,而戰鬥機承載的是子彈,它還有一個功能是射擊.我們就可以建立一個父類,具有屬性顏色和方法起飛,這樣客運及和戰鬥機這兩個子類就可以繼承該父類,然後在父類的基礎上擴充套件自己的屬性和方法.

  1. 原型

繼承要靠原型來實現,我們先來了解一下原型的概念.

  • 所有物件都有一個屬性 proto 指向一個物件,也就是原型
  • 每個物件的原型都可以通過 constructor 找到建構函式,建構函式也可以通過 prototype 找到原型
  • 所有函式都可以通過 proto 找到 Function 物件
  • 所有物件都可以通過 proto 找到 Object 物件
  • 物件之間通過 proto 連線起來,這樣稱之為原型鏈。當前物件上不存在的屬性可以通過原型鏈一層層往上查詢,直到頂層 Object 物件

實現繼承的方式

查詢一個物件的屬性或者方法,首先會在該物件上查詢,找不到會沿著原型鏈向上查詢,因此實現繼承也就是使得一些方法和屬性在該物件的原型鏈上,這樣該物件也就具有了這些屬性和方法.

原型鏈繼承

原型鏈繼承的實現

原型鏈的繼承即建立建構函式的多個例項,從而這多個例項就擁有了建構函式的屬性和方法.示例程式碼:

// 片段A
function Plane(color) {
    this.color = color
}
Plane.prototype.fly = function() {
    console.log('flying')
}
// Fighter 建構函式
function Fighter() {
    this.bullets = []
}
// 繼承Plane建構函式的屬性和方法
Fighter.prototype = new Plane('blue')
// 特有方法
Fighter.prototype.shoot = function() {
    console.log('biu biu biu')
}
var fighter1 = new Fighter()
console.log(fighter1.color)   // blue
fighter1.fly()    // flying
複製程式碼

這樣fighter1就具有了父類Plane的color屬性和fly方法,並在此基礎上擴充套件了自己的shoot方法.

原型鏈繼承的不足

  • constructor指向問題 程式碼片段A的寫法會導致一個constructor指向的問題.

      // 正常情況下
      function A(color) {
          this.color = color
      }
      var a = new A()
      a.__proto__ = A.prototype
      a.constructor = A.prototype.constructor = A
      // 片段A的寫法
      filter.__proto__  = Filter.prototype
      filter.constructor = Filter.prototype.constructor
      // 由於Filter.prototype = new Plane(),我們手動更改了Filter的prototype屬性
      // 所以有
      Filter.prototype.constructor = Plane
      filter.constructor = Plane
      // 為了解決上述問題,我們需要對片段A的程式碼做以下調整
      Fighter.prototype. = new Plane()
      Fighter.prototype.constructo = Fighter
      console.log(filter.constructor)     // Filter
    複製程式碼
  • 屬性共享問題

多個例項共享父類建構函式的屬性,如果共享的屬性是引用型別,會導致其中一個示例最這些屬性做了更改影響到其他例項中對該屬性的使用.屬性共享會導致資料汙染,程式碼的可維護性降低.

  • 引數

如程式碼片段A這樣,Fighter建構函式建立的例項的繼承得來的color屬性都是blue,例項的顏色最好統一是blue,不然後面一個個修改color,程式碼的複用性的降低了.

建構函式繼承

建構函式繼承的實現

    // 片段B
    function Plane(color) {
        this.color = color
    }
    Plane.prototype.fly = function() {
        console.log('flying')  
    }
    function Fighter(color, content) {
        Plane.call(this, color) 
        this.content = content
    }
    Fighter.prototype.shoot = function() {
        console.log('biu biu biu')
    }
    var flighter1 = new Fighter('blue', 'zidan');
    console.log(flighter1.color);  // blue
    console.log(flighter1.content); // zidan
    flighter1.shoot(); // 'biu biu biu';
    flighter1.fly();   //  Uncaught TypeError: flighter1.fly is not a function
    at <anonymous>:19:15
複製程式碼

建構函式繼承的不足

建構函式繼承的不足就片段B中既可以看出,它只能實現繼承建構函式本身的屬性和方法,而不能繼承建構函式原型上的屬性和方法.

組合繼承

組合繼承的程式碼實現

組合繼承, 顧名思義是原型鏈繼承和建構函式函式繼承的組合.父類的屬性通過建構函式繼承為私有屬性,父類的方法通過原型鏈繼承,彌補了原型鏈繼承和建構函式繼承的不足

function Plane(color) {
    this.color = color
}
Plane.prototype.fly = function() {
    console.log('flying')
}
function Fighter(color) {
    Plane.call(this, color)
    this.bulltes = []
}
Fighter.prototype = new Plane()
Fighter.prototype.constructor = Fighter
Fighter.prototype.shoot = function() {
    console.log('biu biu biu')
}
複製程式碼

組合繼承的優點

  • 屬性和方法都是從父類繼承的(程式碼複用)
  • 繼承的屬性是私有的(互不影響)
  • 繼承的方法都在原型裡(程式碼複用)

組合繼承的不足

[打牢基礎系列]JavaScript的類與繼承

組合繼承的方法導致了我們重複呼叫了建構函式Plane.且Plane建構函式內部的color不會被用到,導致了屬性冗餘.

最佳實踐(優化版的組合繼承)

主要原理

  • 基於組合繼承
  • 避免重複呼叫父類建構函式,只需繼承原型

組合繼承的程式碼實現(優化版, 也有說法叫寄生組合繼承)

    // 片段C
    function Plane(color) {
        this.color = color
        this.pilots = []
    }
    Plane.prototype.fly = function() {
        console.log('flying')
    }
    // Fighter 建構函式
    function Fighter(color) {
        Plane.call(this, color)
        this.bullets = []
    }
    // 繼承Plane建構函式的屬性和方法
    inheritPrototype(Fighter, Plane)
    // 特有方法
    Fighter.prototype.shoot = function() {
        console.log('biu biu biu')
    }
    var fighter1 = new Fighter()
    console.log(fighter1)
    // inheritPrototype的第一種實現方式
    function inheritPrototype(child, parent) {
        var proto = Object.create(parent.prototype)
        proto.constructor = child
        child.prototype = proto
    }
    // inheritPrototype的第二種實現方式
    function inheritPrototype(child, parent) {
        var proto = Object.create(parent.prototype)
        child.prototype = proto
        child.prototype.constructor = child
    }
    // inheritPrototype的第三種實現方式
    function inheritPrototype(child, parent) {
        var proto = function() {}
        proto.prototype = parent.prototype
        child.prototype = new proto()
        child.prototype.constructor = child
    }   
複製程式碼

ES6的繼承

ES6的繼承程式碼實現:

我們都知道,目前的js的繼承的實現比較繁瑣, 要呼叫建構函式,又要自己封裝繼承原型的函式,所以ES6標準將class宣告類和繼承類的方式納入標準,示例程式碼如下:

// 片段D
class Plane1 {
    constructor(color) {
        this.color = color
    }
    fly() {
        console.log('flying')
    }
}
// 使用extends關鍵字實現繼承
class Fighter1 extends Plane1 {
    constructor(color, content) {
        super(color)
        this.content = content
    }
    shoot() {
        console.log('biu biu biu')
    }
}
const fight1 = new Fighter1('blue', 'zidan')
fight1.color      // blue
fight1.content    // zidan
fight1.fly()      // flying
fight1.shoot()    // biu biu biu
複製程式碼

注意: 目前部分現代瀏覽器新版本已經實現對 ES6 中的class和繼承的,但是注意在舊版本或者 IE 瀏覽器中是不支援的,所以使用的時候要注意,或者配合使用 Babel 等編譯工具。

總結

啦啦啦~~~,寫完了,後續我會堅持一系列的前端知識的分享的,希望你們讀完我的文章後能夠有所收穫~~~.聽明白和講明白之間還是差了很多,如果我有表達不當的地方或者可以優化的地方還請指出,希望努力的我們都能成為優秀的自己~~~

擴充套件閱讀

相關文章