爲何要用繼承?前端
平時寫代碼時, 彷佛不用繼承也能夠比較好地完成需求, 但若是你在一些恰當的場景使用繼承, 能夠大幅地簡化代碼, 提升代碼複用率, 使得代碼在後期更易於維護. 舉個例子(這個例子有點長, 但仍是比較有趣的):react
筆者在某個廠工做, 這個廠有不少 App, 並且這些 App 屬於各個部門, 他們之間各自爲政, 沒有通用的 js sdk, 而是每一個 App 都有一份本身的 js sdk. (沒有通用 js sdk 原本是不合理的, 但現狀如此). 這裏有個需求: 在全部 App 內嵌 webview 裏打開的網頁實現頁內下載. 簡言之, 須要實現一個 js 下載器, 他彌合了全部 App 的 js sdk 的差別. 對調用者來講, 只須要這麼作:web
import Downloader from 'Downloader'; const downloader = new Downloader(options); downloader.onStatusUpdate((status) => { // 在 UI 上設置狀態 this.setState({ buttonText: status.message, buttonProgress: status.progress, }) }) button.addEventListener('click', () => { downloader.dispatchDownloadAction(); });
如上, 咱們要實現一個 Downloader, 他監聽了狀態變動, 在咱們點擊按鈕時, 須要觸發下載, 暫停, 安裝, 打開等動做, 咱們只須要調用如下語句面試
downloader.dispatchDownloadAction();
便可觸發當前狀態下的正確行爲, 好比在初始狀態, 會觸發下載, 而且在 onStatusUpdate 的回調中, 傳遞當前狀態的文案, 好比 已下載 10%, 和當前的進度數值, 如 0.1, 方便調用方設置進度條.瀏覽器
若是正在下載中, 調用 downloader.dispatchDownloadAction(), 則會暫停, 而且返回適當的 status, 同理, 再調用一次該函數, 則繼續下載, 下載完成, 再次調用, 則進行安裝... 直到打開.微信
這裏再次強調, 有不少 App, 好比說有 30 個, 30 個 App 都提供不同的 js sdk, 那麼要在 Downloader 中實現 30 份下載器, 這裏邊就有不少可複用的邏輯了.app
好比處理狀態的邏輯: 在初始, 下載, 暫停, 安裝, 打開這些狀態下, dispatchDownloadAction 應該有哪些動做, 這是能夠複用的, 另外, 下載器的單例, 是否自動下載, 事件模型的實現, 均可以複用.框架
此時, 繼承是一個很好的實踐, 甚至是最佳實踐. 咱們來看看在這個場景下的類圖:ide
咱們實現一個父類 Downloader, 他有一個 status 屬性, 還有兩個公有方法 dispatchDownloadAction 和 onUpdateStatus, 有兩個私有方法 on 和 trigger, on 能夠監聽自定義事件, trigger 能夠觸發事件, 用於給子類調用, 好比子類在下載時, 能夠 trigger 進度條的變化信息.模塊化
咱們要實現 30 個子類, 他們分別都實現了各自的下載, 暫停, 安裝, 打開方法, 這些方法裏的邏輯都是不能複用的, 就是一些和各個 app 的 js sdk 打交道的邏輯. 而這些方法中, 有的能夠調用父類的 trigger 方法更新狀態, 有的能夠被父類經過 this.xxx 調用.
而父類的 dispatchDownloadAction 具備統籌帷幄的做用, 大體僞代碼以下:
dispatchDownloadAction: () => { switch(status) { case 默認狀態: { this.download() // 調用的是子類實現的方法 break; } case 下載中: { this.pause() // 調用的是子類實現的方法 break; } case 暫停中: { this.download() // 調用的是子類實現的方法 break; } case 下載完成: { this.install() // 調用的是子類實現的方法 break; } case 已安裝: { this.launch() // 調用的是子類實現的方法 break; } } }
這就是咱們用繼承的方式, 將可複用的方法抽象到父類中, 而子類只實現各自不可複用的部分.
除了這個例子, 這裏再簡單舉一個, 咱們寫 react component 的時候, 以下語句就使用了繼承:
class MyComponent extends React.Component;
000該語句使得組件的各個生命週期函數可被調用, 如 render, componentDidMount, componentDidUpdate, 也讓咱們能訪問到父類 Component 的屬性和方法, 如 this.props 和 this.setState()
可見, 繼承在咱們身邊發揮着重要的做用. 繼承 是面向對象的三個基本特徵之一, 掌握好了, 可讓咱們設計出更好維護的代碼.
接下來咱們講一下在 js 中如何實現繼承
在 ES6 中, 實現繼承很簡單, 使用 extends 便可完成, 以下:
class subType extends superType;
以上寫法在一些低版本的瀏覽器中沒法支持, 一般咱們的打包工具會把 extends 編譯成 es5 的實現.
本篇的主旨, 就是講 es5 原型繼承的實現. 爲何 2020 年了, 還要了解 es5 的原型繼承呢? 緣由以下:
本篇講解幾個經常使用的繼承實現方法.
原型繼承基於原型鏈, 咱們能夠順着原型鏈一直往上找, 找到想要的屬性和方法. 但在這個過程當中, 須要解決一些問題, 好比怎麼防止原型裏的屬性繼承後, 被共享, 怎麼給父類傳參, 怎麼減小性能浪費, 如減小存儲空間和函數執行次數等.
咱們來看一下最基本的繼承實現:
// 形狀 function Shape() { this.values = ['a', 'b', 'c']; } // 三角形 function Triangle() {}
此時的原型關係以下:
如圖, 此時形狀與三角形尚未繼承關係, 如下語句將讓他們產生繼承關係
Triangle.prototype = new Shape();
此時的原型關係以下:
能夠看到, 此時 Triangle.prototype.proto 已經再也不指向 Object.prototype, 而是指向 Shape.prototype.
咱們來看一下此時 new 一個 Triangle 的實例是個什麼狀況:
const triangle = new Triangle();
能夠看到, triangle 這個實例, 能夠循着原型鏈找到 values即:
console.log(triangle.values); // ['a', 'b', 'c']
但這裏存在個問題, 就是 values 是被全部實例共享的, 好比:
const triangle1 = new Triangle(); triangle1.values.push('d'); console.log(triangle1.values); // ['a', 'b', 'c', 'd']; const triangle2 = new Triangle(); console.log(triangle2.values); // ['a', 'b', 'c', 'd'];
這種共享的屬性, 會互相影響, 上邊這個繼承還不合格, 咱們來看一個進階版的繼承實現
組合式繼承這個名詞來源於 JavaScript 高級程序設計, 這裏不深究, 怎麼叫只是個名字, 咱們要了解的是它的原理
function Shape(name) { this.name = name; this.values = ['a', 'b', 'c']; } Shape.prototype.getName = function() { return this.name; } function Triangle(name) { // 如下語句借用了 Shape 的構造函數, Shape 構造函數在執行時, this 是子類的實例 // 並且這麼一借用, 咱們就能夠給父類的構造函數傳遞參數了~ Shape.call(this, name); // call 是每一個函數都有的一個方法, 他能夠執行該函數, 而且改變函數內部 this 的指向, 且給函數傳遞參數 } Triangle.prototype = new Shape(); const triangle1 = new Triangle('等腰三角形'); triangle1.values.push('d'); console.log(triangle1.values); // ['a', 'b', 'c', 'd'] const triangle2 = new Triangle('全等三角形'); console.log(triangle2.values); // ['a', 'b', 'c'] // 看, 屬性沒有互相影響 console.log(triangle1.getName()) // 等腰三角形 console.log(triangle2.getName()) // 全等三角形
組合式繼承是一個合格的繼承實現, 他解決了屬性共享的問題, 也解決了向父類構造函數傳遞參數的問題. 但其實還有一些缺陷, 細心的同窗應該發現了, 父類構造函數被執行了兩次, 一次是 call 執行, 一次是 new 執行, 而且 values 不只存在於 triangle 實例中, 也存在於 triangle.proto 中, 這形成了性能浪費.
本篇不打算細講如何一步步地將繼承實現到完美, 旨在讓你們瞭解繼承的應用與基於原型的實現, 想要充分掌握的, 建議看書瞭解, 推薦《JavaScript 高級程序設計》, 這本書被稱爲前端基礎知識的紅寶書, 這裏給剛接觸 js 的同窗強烈安利一波~
繼承給咱們提供了一種優雅可複用的編碼方式, 在一些大型應用或框架中是常常用到的, 本篇介紹了基於繼承的兩個應用, 闡述了 ES6, ES5 的繼承實現, 繼承的知識在面試中也常常被問到, 快掌握起來吧~
圖解 JavaScript 原型與原型鏈
經常使用原生JS方法總結(兼容性寫法)
JS模塊化規範總結(面試必備良藥)
在看點這裏