手把手教你如何實現繼承

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


小例子

如今有個需求,須要實現 Cat 繼承 Animal ,構造函數以下:函數

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

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

注:如對繼承相關的 prototype、constructor、__proto__、new 等內容不太熟悉,能夠先查看這篇文章:理性分析 JavaScript 中的原型post


繼承

在實現這個需求以前,咱們先談談繼承的意義。繼承本質上爲了提升代碼的複用性。性能

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

  1. 複用父構造函數中的代碼
  2. 複用父原型中的代碼

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

初版代碼

複用父構造函數中的代碼,咱們能夠考慮調用父構造函數並將 this 綁定到子構造函數。spa

複用父原型中的代碼,咱們只需改變原型鏈便可。將子構造函數的原型對象的 __proto__ 屬性指向父構造函數的原型對象。prototype

初版代碼以下:設計

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

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

Cat.prototype.__proto__ = Animal.prototype
複製代碼

檢驗一下是否繼承成功:咱們在 Animal 的原型對象上添加 eat 函數。使用 Cat 構造函數生成一個名爲 'Tom' 的實例對象 cat 。代碼以下:code

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版)》
相關文章
相關標籤/搜索