手把手教你如何實現繼承

haloislet發表於2018-03-15

本文將從最簡單的例子開始,從零講解在 JavaScript 中如何實現繼承。


小例子

現在有個需求,需要實現 Cat 繼承 Animal ,建構函式如下:

function Animal(name){
    this.name = name
}

function Cat(name){
    this.name = name
}
複製程式碼

注:如對繼承相關的 prototype、constructor、__proto__、new 等內容不太熟悉,可以先檢視這篇文章:理性分析 JavaScript 中的原型


繼承

在實現這個需求之前,我們先談談繼承的意義。繼承本質上為了提高程式碼的複用性。

對於 JavaScript 來說,繼承有兩個要點:

  1. 複用父建構函式中的程式碼
  2. 複用父原型中的程式碼

下面的內容將圍繞這兩個要點展開。

第一版程式碼

複用父建構函式中的程式碼,我們可以考慮呼叫父建構函式並將 this 繫結到子建構函式。

複用父原型中的程式碼,我們只需改變原型鏈即可。將子建構函式的原型物件的 __proto__ 屬性指向父建構函式的原型物件。

第一版程式碼如下:

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype.__proto__ = Animal.prototype
複製程式碼

檢驗一下是否繼承成功:我們在 Animal 的原型物件上新增 eat 函式。使用 Cat 建構函式生成一個名為 'Tom' 的例項物件 cat 。程式碼如下:

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype.__proto__ = Animal.prototype

// 新增 eat 函式
Animal.prototype.eat = function(){
    console.log('eat')
}

var cat = new Cat('Tom')
// 檢視 name 屬性是否成功掛載到 cat 物件上
console.log(cat.name) // Tom
// 檢視是否能訪問到 eat 函式
cat.eat() // eat 
// 檢視 Animal.prototype 是否位於原型鏈上
console.log(cat instanceof Animal) // true
// 檢視 Cat.prototype 是否位於原型鏈上
console.log(cat instanceof Cat) //true
複製程式碼

經檢驗,成功複用父建構函式中的程式碼,並複用父原型物件中的程式碼,原型鏈正常。

圖示

手把手教你如何實現繼承

弊端

__proto__ 屬性雖然可以很方便地改變原型鏈,但是 __proto__ 直到 ES6 才新增到規範中,存在相容性問題,並且直接使用 __proto__ 來改變原型鏈非常消耗效能。所以 __proto__ 屬性來實現繼承並不可取。

第二版程式碼

針對 __proto__ 屬性的弊端,我們考慮使用 new 操作符來替代直接使用 __proto__ 屬性來改變原型鏈。

我們知道例項物件中的 __proto__ 屬性指向建構函式的 prototype 屬性的。這樣我們 Animal 的例項物件賦值給 Cat.prototype 。不就也實現了Cat.prototype.__proto__ = Animal.prototype 語句的功能了嗎?

程式碼如下:

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
複製程式碼

使用這套方案有個問題,就是在將例項物件賦值給 Cat.prototype 的時候,將 Cat.prototype 原有的 constructor 屬性覆蓋了。例項物件的 constructor 屬性向上查詢得到的是建構函式 Animal 。所以我們需要矯正一下 Cat.prototype 的 constructor 屬性,將其設定為建構函式 Cat 。

圖示

手把手教你如何實現繼承

優點

相容性比較好,並且實現較為簡單。

弊端

使用 new 操作符帶來的弊端是,執行 new 操作符的時候,會執行一次建構函式將建構函式中的屬性繫結到這個例項物件。這樣就多執行了一次建構函式,將原本屬於 Animal 例項物件的屬性混到 prototype 中了。

第三版程式碼

考慮到第二版的弊端,我們使用一個空建構函式來作為中介函式,這樣就不會將建構函式中的屬性混到 prototype 中,並且減少了多執行一次建構函式帶來的效能損耗。

程式碼如下:

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}
function Func(){}
Func.prototype = Animal.prototype

Cat.prototype = new Func()
Cat.prototype.constructor = Cat
複製程式碼

圖示

手把手教你如何實現繼承

ES6

使用 ES6 就方便多了。可以使用 extends 關鍵字實現繼承, 複用父原型中的程式碼。使用 super 關鍵字來複用父建構函式中的程式碼。

程式碼如下:

class Animal {
    constructor(name){
        this.name = name
    }
    eat(){
        console.log('eat')
    }
}
class Cat extends Animal{
    constructor(name){
        super(name)
    }
}

let cat = new Cat('Tom')
console.log(cat.name) // Tom
cat.eat() // eat
複製程式碼

相關知識點


參考書籍

  • 《JavaScript高階程式設計(第3版)》
  • 《Java核心技術 卷Ⅰ(第9版)》

相關文章