重學 JS 系列:聊聊繼承

2021-02-14 前端真好玩

點擊上方「前端真好玩」,喜歡他就關注他

聽說,看愷哥的文章會上癮

這是重學 JS 系列的第二篇文章,寫這個系列的初衷也是為了夯實自己的 JS 基礎。既然是重學,肯定不會從零開始介紹一個知識點,如有遇到不會的內容請自行查找資料。

原型

繼承得靠原型來實現,當然原型不是這篇文章的重點,我們來複習一下即可。

其實原型的概念很簡單:

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

其實原型中最重要的內容就是這些了,完全沒有必要去看那些長篇大論什麼是原型的文章,初學者會越看越迷糊。

當然如果你想了解更多原型的深入內容,可以閱讀我 之前寫的文章[1]。

ES5 實現繼承

ES5 實現繼承總的來說就兩種辦法,之前寫過這方面的內容,就直接複製來用了。

總的來說這部分的內容我覺得在當下更多的是為了應付面試吧。

組合繼承

組合繼承是最常用的繼承方式,

function Parent(value) {  this.val = value}Parent.prototype.getValue = function() {  console.log(this.val)}function Child(value) {  Parent.call(this, value)}Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1child instanceof Parent // true

以上繼承的方式核心是在子類的構造函數中通過 Parent.call(this) 繼承父類的屬性,然後改變子類的原型為 new Parent() 來繼承父類的函數。

這種繼承方式優點在於構造函數可以傳參,不會與父類引用屬性共享,可以復用父類的函數,但是也存在一個缺點就是在繼承父類函數的時候調用了父類構造函數,導致子類的原型上多了不需要的父類屬性,存在內存上的浪費。

寄生組合繼承

這種繼承方式對組合繼承進行了優化,組合繼承缺點在於繼承父類函數時調用了構造函數,我們只需要優化掉這點就行了。

function Parent(value) {  this.val = value}Parent.prototype.getValue = function() {  console.log(this.val)}
function Child(value) { Parent.call(this, value)}Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true }})
const child = new Child(1)
child.getValue() // 1child instanceof Parent // true

以上繼承實現的核心就是將父類的原型賦值給了子類,並且將構造函數設置為子類,這樣既解決了無用的父類屬性問題,還能正確的找到子類的構造函數。

Babel 如何編譯 ES6 Class 的

為什麼在前文說 ES5 實現繼承更多的是應付面試呢,因為我們現在可以直接使用 class 來實現繼承。

但是 class 畢竟是 ES6 的東西,為了能更好地兼容瀏覽器,我們通常都會通過 Babel 去編譯 ES6 的代碼。接下來我們就來了解下通過 Babel 編譯後的代碼是怎麼樣的。


function _possibleConstructorReturn (self, call) { // ... return call && (typeof call === 'object' || typeof call === 'function') ? call : self; }
function _inherits (subClass, superClass) { // ... subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var Parent = function Parent () { // 驗證是否是 Parent 構造出來的 this _classCallCheck(this, Parent);};
var Child = (function (_Parent) { _inherits(Child, _Parent);
function Child () { _classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments)); }
return Child;}(Parent));

以上代碼就是編譯出來的部分代碼,隱去了一些非核心代碼,我們先來閱讀 _inherits 函數。

設置子類原型部分的代碼其實和寄生組合繼承是一模一樣的,側面也說明了這種實現方式是最好的。但是這部分的代碼多了一句 Object.setPrototypeOf(subClass, superClass),其實這句代碼的作用是為了繼承到父類的靜態方法,之前我們實現的兩種繼承方法都是沒有這個功能的。

然後 Child 構造函數這塊的代碼也基本和之前的實現方式類似。所以總的來說 Babel 實現繼承的方式還是寄生組合繼承,無非多實現了一步繼承父類的靜態方法。

繼承存在的問題

講了這麼些如何實現繼承,現在我們來考慮下繼承是否是一個好的選擇?

總的來說,我個人不怎麼喜歡繼承,原因呢就一個個來說。

我們先看代碼。假如說我們現在要描述幾輛不同品牌的車,車必然是一個父類,然後各個品牌的車都分別是一個子類。

class Car {    constructor (brand) {        this.brand = brand    }    wheel () {        return '4 個輪子'    }    drvie () {        return '車可以開駕駛'    }    addOil () {        return '車可以加油'    }}Class OtherCar extends Car {}

這部分代碼在當下看著沒啥毛病,實現了車的幾個基本功能,我們也可以通過子類去擴展出各種車。

但是現在出現了新能源車,新能源車是不需要加油的。當然除了加油這個功能不需要,其他幾個車的基本功能還是需要的。

如果新能源車直接繼承車這個父類的話,就出現了第一個問題 ,大猩猩與香蕉問題。這個問題的意思是我們現在只需要一根香蕉,但是卻得到了握著香蕉的大猩猩,大猩猩其實我們是不需要的,但是父類還是強塞給了子類。繼承雖然可以重寫父類的方法,但是並不能選擇需要繼承什麼東西。

另外單個父類很難描述清楚所有場景,這就導致我們可能又需要新增幾個不同的父類去描述更多的場景。隨著不斷的擴展,代碼勢必會存在重複,這也是繼承存在的問題之一。

除了以上兩個問題,繼承還存在強耦合的情況,不管怎麼樣子類都會和它的父類耦合在一起。

既然出現了強耦合,那麼這個架構必定是脆弱的。一旦我們的父類設計的有問題,就會對維護造成很大的影響。因為所有的子類都和父類耦合在一起了,假如更改父類中的任何東西,都可能會導致需要更改所有的子類。

如何解決繼承的問題

繼承更多的是去描述一個東西是什麼,描述的不好就會出現各種各樣的問題,那麼我們是否有辦法去解決這些問題呢?答案是組合。

什麼是組合呢?你可以把這個概念想成是,你擁有各種各樣的零件,可以通過這些零件去造出各種各樣的產品,組合更多的是去描述一個東西能幹什麼。

現在我們把之前那個車的案例通過組合的方式來實現。

function wheel() {  return "4 個輪子";}function drvie() {  return "車可以開駕駛";}function addOil() {  return "車可以加油";}// 油車const car = compose(wheel, drvie, addOil)// 新能源車const energyCar = compose(wheel, drive)

從上述偽代碼中想必你也發現了組合比繼承好的地方。無論你想描述任何東西,都可以通過幾個函數組合起來的方式去實現。代碼很乾淨,也很利於復用。

最後

其實這篇文章的主旨還是後面兩小節的內容,如果你還有什麼疑問歡迎在評論區與我互動。

我所有的系列文章都會在我的 Github[2] 中最先更新,有興趣的可以關注下。今年主要會著重寫以下三個專欄

•重學 JS•React 進階•重寫組件

References

[1] 之前寫的文章: https://github.com/KieSun/Dream/issues/2
[2] Github: https://github.com/KieSun/Dream

長按掃描二維碼

我們一起好好玩

這是一個很好玩的公眾號

相關焦點

  • 重學 JS:為啥 await 不能用在 forEach 中
    點擊上方「前端真好玩」,喜歡他就關注他聽說,看愷哥的文章會上癮這是重學JS 系列的第三篇文章,寫這個系列的初衷也是為了夯實自己的 JS 基礎或者了解一些之前不知道的東西。既然是重學,肯定不會從零開始介紹一個知識點,如有遇到不會的內容請自行查找資料。
  • 如何繼承 Date 對象?由一道題徹底弄懂 JS 繼承
    經典的繼承法有何問題先看看本文最開始時提到的經典繼承法實現,如下:/** * 經典的js組合寄生繼承 */function MyDate() {    Date.apply(this, arguments);    this.abc = 1;}function inherits
  • 聊聊Esri的那些開源JS項目
    今天我們就來聊聊Esri的那些JS的開源項目。bootstrap-map-js項目在bootstrap庫的基礎上開發了一個輕量級的擴展插件用來構建地圖應用(目前還只能用於3.x版本的ArcGIS API for JavaScript)通過bootstrap-map-js
  • 狼叔:聊聊 Node.js
    ,單篇33.8萬的閱讀量,還是不錯的,說明大家非常關心前端的變化趨勢,這裡再與大家分享一下我對 Node.js 相關內容的看法。整體看,Node.js 社區還是非常健康且與時俱進的。Serverless 借著雲原生這波基建升級,逐漸走入更多開發者的視野中。低運維,甚至是0運維,對前端來講是致命誘惑的。
  • JS必知的6種繼承方式
    那麼如何在JS中實現繼承呢?只繼承了父類構造函數的屬性,沒有繼承父類原型的屬性3、組合繼承(組合原型鏈繼承和借用構造函數繼承)(常用)// 組合原型鏈和構造函數繼承function SubType (name) {  Person.call(this, name); // 借用構造函數模式}SubType.prototype = new
  • JavaScript繼承方式詳解
    js繼承的概念js裡常用的如下兩種繼承方式:
  • EventLoop 系列 - 聊聊 Node.js 中的事件循環
    了解 Node.js 中的事件循環Node.js 做為 JavaScript 的服務端運行時,主要與網絡、文件打交道,沒有了瀏覽器中事件循環的渲染階段。在瀏覽器中有 HTML 規範來定義事件循環的處理模型,之後由各瀏覽器廠商實現。Node.js 中事件循環的定義與實現均來自於 Libuv。
  • 聊聊 Python 調用 JS 的幾種方式
    前言日常 Web 端爬蟲過程中,經常會遇到參數被加密的場景,因此,我們需要分析網頁原始碼通過調式,一層層剝離出關鍵的 JS 代碼,使用 Python 去執行這段代碼,得出參數加密前後的 Python 實現本文將聊聊利用 Python 調用 JS 的4種方式2.
  • Vue.js與jQuery哪個更好學?
    大家會普遍的認為,對於一新手web程式設計師來說,jQuery被認為是一個好的入門點,甚至於很多程式設計師也都是先學習jQuery,再學原生的Javascript
  • 【Hello CSS】第七章-CSS的繼承與可變性
    CSS中的 繼承 實際上是父級元素對子元素的影響。接下來我們談談 CSS中的繼承。特殊的通用屬性值CSS為處理繼承提供了四種特殊的通用屬性值,其值如下:值意義initial屬性初始值。inherit繼承的值。
  • 前端JS&CSS
    ("a").parentNode.children; console.log(b)</script>js用js遞歸的方式寫1到100求和?宏任務中的事件放在callback queue中,由事件觸發線程維護;微任務的事件放在微任務隊列中,由js引擎線程維護。說一下繼承的幾種方式及優缺點?
  • 如何用 js 獲取虛擬鍵盤高度?- 20170817 前端開發日報
    話說之前學 Android 時從不覺得寫個計算器 Demo 會有多難。然而上星期花了幾天的時間用原生 JavaScript、CSS、HTML 寫了一個計算器 Demo。然而就是這麼一個小小的項目還是能讓我學到挺多的東西,其中最讓我受益的就是明白一個良好的架構對一個軟體項目來說是有多麼的重要!
  • 聊聊 React 新文檔
    React 官方文檔改版耗時 1 年,今天已完成站點相關改版,部分文檔已初步上線。
  • node.js+express 做301重定向實驗
    我重啟項目,看一下發送的請求:劃線表示永久重定向到緩存。打開看上線了網站的案例:沒有把重定向緩存,所以它刪除綁定後,是不會再跳轉到自定義域名的。關於301跳轉的問題,我們特別邀請 Baiduspider 技術專家對此做了解答。問:我設置了 301 跳轉,多久可以生效?
  • 【專業技術】關於JS的prototype
    這就是prototype的功勞了,uw3c中prototype屬性中的name對象,在uw3c被new構造函數之後,被繼承到了對象test的屬性中。在uw3c的prototype對象中出現的任何屬性或者函數都可以在test對象中直接使用,這個就是JS中的原型繼承了。
  • Node.js幾種創建子進程方法
    衍生的 Node.js 子進程與兩者之間建立的 IPC 通信信道的異常是獨立於父進程的。每個進程都有自己的內存,使用自己的 V8 實例。由於需要額外的資源分配,因此不推薦衍生大量的 Node.js 進程。silent父子進程間stdin/stdout/stderr之間的通訊。
  • Node.js系列一 - JavaScript運行原理
    webkit內核看到這裡,學過小程序的同學有沒有感覺非常的熟悉呢?在小程序中編寫的JavaScript代碼就是被JSCore執行的;邂逅Node.js 2.1. Node.js是什麼?回顧:官方對Node.js的定義:Node.js是一個基於V8 JavaScript引擎的JavaScript運行時環境。
  • 重學網絡系列之(UDP)
    DHCP 就是一種廣播的形式,就是基於 UDP 協議的,第三,需要處理速度快,時延低,可以容忍少數丟包,但是要求即便網絡擁塞,也毫不退縮,一往無前,UDP 簡單、處理速度快,不像 TCP 那樣,操這麼多的心,各種重傳啊,保證順序啊,前面的不收到,後面的沒法處理啊。不然等這些事情做完了,時延早就上去了。
  • 最好用的流程編輯器bpmn-js系列之基本使用
    最好用的流程編輯器bpmn-js系列文章BPMN(Business Process Modeling Notation)是由業務流程管理倡議組織BPMI(The Business Process Management Initiative)開發的一套標準的業務流程建模符號規範。
  • 瘋狂Html+CSS+JS 中JS總結
    }}   var person =new Person('K');person.info(); //Kvar name = 'K_window';//由於window為調用者 ,this.name訪問的是window.namep.info.call(window); //K_window來爽一發貓學狗叫