摘要:目錄無繼承簡單的字段聲明無繼承簡單的方法聲明簡單繼承一層繼承字段覆蓋無繼承靜態函數無繼承靜態變量神秘的類無繼承簡單的字段聲明先來看個最簡單的例子,我們僅僅使用了關鍵字并定義了一個變量最后編譯出來的代碼如下。無繼承靜態變量還有個小例子。
在[上一篇文章][]中,我們提到 ES6 的 class 語法糖是個近乎完美的方案,并且講解了實現繼承的許多內部機制,如 prototype/__proto__/constructor 等等。這篇,我們就以實際的 babel 代碼為例子,來驗證上節所言不虛。此外,本文還解釋了 React 組件中你需要 bind 一下類方法的原理所在。
目錄無繼承——簡單的 class + 字段聲明
無繼承——簡單的 class + 方法聲明
簡單繼承——一層繼承 + 字段覆蓋
無繼承——靜態函數
無繼承——靜態變量
神秘的類 arrow function
無繼承——簡單的 class + 字段聲明先來看個最簡單的例子,我們僅僅使用了 class 關鍵字并定義了一個變量:
class Animal { constructor(name) { this.name = name || "Kat" } }
最后 babel 編譯出來的代碼如下。這里筆者用的是 Babel 6 的穩定版 6.26,不同版本編譯出來可能有差異,但不至于有大的結構變動。
"use strict" function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function") } } var Animal = function Animal(name) { _classCallCheck(this, Animal) this.name = name || "Kat" }
確實十分簡單,對吧。這段代碼值得留意的點有兩個:
一個是,使用 class 聲明的 Animal 最后其實是被編譯為一個函數。證明 class 跟類沒關系,只是個語法糖。
另一個地方是,編譯器幫我們插入了一個 _classCallCheck 函數調用,它會檢查你有沒有用 new Animal() 操作符來初始化這個函數。若有,則 this 會是被實例化的 Animal 對象,自然能通過 animal instanceof Animal 檢查;若是直接調用函數,this 會被初始化為全局對象,自然不會是 Animal 實例,從而拋出運行時錯誤。這個檢查,正解決了[上一篇文章][]提到的問題:如果忘記使用 new 去調用一個被設計構造函數的函數,沒有任何運行時錯誤的毛病。
無繼承——簡單的 class + 方法聲明讓我們再擴展一下例子,給它加兩個方法。
class Animal { constructor(name) { this.name = name || "Kat" } move() {} getName() { return this.name } }
"use strict" var _createClass = (function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i] descriptor.enumerable = descriptor.enumerable || false descriptor.configurable = true if ("value" in descriptor) descriptor.writable = true Object.defineProperty(target, descriptor.key, descriptor) } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps) if (staticProps) defineProperties(Constructor, staticProps) return Constructor } })() function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function") } } var Animal = (function() { function Animal(name) { _classCallCheck(this, Animal) this.name = name || "Kat" } _createClass(Animal, [ { key: "move", value: function move() {}, }, { key: "getName", value: function getName() { return this.name }, }, ]) return Animal })()
例子長了不少,但其實主要的變化只有兩個:一是 Animal 被包了一層而不是直接返回;二是新增的方法 move 和 getName 是通過一個 _createClass() 方法來實現的。它將兩個方法以 key/value 的形式作為數組傳入,看起來,是要把它們設置到 Animal 的原型鏈上面,以便后續繼承之用。
為啥 Animal 被包了一層呢,這是個好問題,但答案我們將留到后文揭曉。現在,我們先看一下這個長長的 _createClass 實現是什么:
var _createClass = (function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i] descriptor.enumerable = descriptor.enumerable || false descriptor.configurable = true if ("value" in descriptor) descriptor.writable = true Object.defineProperty(target, descriptor.key, descriptor) } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps) if (staticProps) defineProperties(Constructor, staticProps) return Constructor } })()
它是個立即執行函數,執行又返回了另一個函數。說明啥,一定用了閉包,說明里面要封裝些「私有」變量,那就是 defineProperties 這個函數。這很好,一是這個函數只會生成一次,二是明確了這個函數只與 _createClass 這個事情相關。
再細看這個返回的函數,接受 Constructor、protoProps 和 staticProps 三個參數。staticProps 我們暫時不會用到,回頭再講;我們傳入的數組是通過 protoProps 接受的。接下來,看一下 defineProperties 做了啥事。
它將每一個傳進來的 props 做了如下處理:分別設置了他們的 enumerable、configurable、writable 屬性。而傳進來的 target 是 Animal.prototype,相當于,這個函數最后的執行效果會是這樣:
function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { // 前面處理其實得到這樣這個 descriptor 對象: var descriptor = { ...props[i], enumerable: false, configurable: true, writable: true, } Object.defineProperty(target, descriptor.key, descriptor) } }
看到這里就很明白了,它就是把你定義的 move、getName 方法通過 Object.defineProperty 方法設置到 Animal.prototype 上去。前面我們說過,prototype 是用來存儲公共屬性的。也就是說,這兩個方法在你使用繼承的時候,可以被子對象通過原型鏈上溯訪問到。也就是說,我們這個小小的例子里,聲明的兩個方法已經具備了繼承能力了。
至于 enumerable、configurable、writable 屬性是什么東西呢,查一下語言規范就知道了。簡單來說,writable 為 false 時,其值不能通過 setter 改變;enumerable 為 false 時,不能出現在 for-in 循環中。當然,這里是粗淺的理解,暫時不是這篇文章的重點。
簡單繼承——一層繼承 + 字段覆蓋class Animal { constructor(name) { this.name = name || "Kat" } } class Tiger extends Animal { constructor(name, type) { super(name) this.type = type || "Paper" } }
加一層繼承和字段覆蓋能看到啥東西呢?能看到繼承底下的實現機制是怎么樣的,以及它的 constructor 和 __proto__ 屬性將如何被正確設置。帶著這兩個問題,我們一起來看下編譯后的源碼:
"use strict" function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError( "this hasn"t been initialised - super() hasn"t been called" ) } return call && (typeof call === "object" || typeof call === "function") ? call : self } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError( "Super expression must either be null or a function, not " + typeof 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) } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function") } } var Animal = function Animal(name) { _classCallCheck(this, Animal) this.name = name || "Kat" } var Tiger = (function(_Animal) { _inherits(Tiger, _Animal) function Tiger(name, type) { _classCallCheck(this, Tiger) var _this = _possibleConstructorReturn( this, (Tiger.__proto__ || Object.getPrototypeOf(Tiger)).call(this, name) ) _this.type = type || "Paper" return _this } return Tiger })(Animal)
相比無繼承的代碼,這里主要增加了幾個函數。_possibleConstructorReturn 顧名思義,可能不是很重要,回頭再讀。精華在 _inherits(Tiger, Animal) 這個函數,我們按順序來讀一下。
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError( "Super expression must either be null or a function, not " + typeof 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) }
首先是一段異常處理,簡單地檢查了 superClass 要么是個函數,要么得是個 null。也就是說,如果你這樣寫那是不行的:
const Something = "not-a-function" class Animal extends Something {} // Error: Super expression must either be null or a function, not string
接下來這句代碼將 prototype 和 constructor 一并設置到位,是精華。注意,這個地方留個問題:為什么要用 Object.create(superClass.prototype),而不是直接這么寫:
function _inherits(subClass, superClass) { subClass.prototype = superClass && superClass.prototype subClass.prototype.constructor = { ... } }
很明顯,是為了避免任何對 subClass.prototype 的修改影響到 superClass.prototype。使用 Object.create(asPrototype) 出來的對象,其實上是將 subClass.prototype.__proto__ = superClass.prototype,這樣 subClass 也就繼承了 superClass,可以達到這樣兩個目的:
superClass.prototype 原型上發生的修改都能實時反映到 subClass 的實例上
subClass.prototype 上的任何修改不會影響到 superClass.prototype
最后,如果 superClass 不為空,那么將 subClass.__proto__ 設置為 superClass。這是為了繼承 superClass 的靜態方法和屬性。如以下的例子中,Cat.TYPE 能獲取到 Animal.TYPE:
class Animal { static TYPE = "PAPER" static createTyping() { return Animal.TYPE } } class Cat extends Animal {} console.log(Cat.TYPE) // PAPER console.log(Cat.createTyping()) // PAPER
至此,一個簡單的繼承就完成了。在使用了 extends 關鍵字后,實際上背后發生的事情是:
子「類」prototype 上的 __proto__ 被正確設置,指向父「類」的 prototype: subClass.prototype = { __proto__: superClass.prototype }
子「類」prototype 上的 constructor 被正確初始化,這樣 instanceof 關系能得到正確結果
子「類」的 __proto__ 被指向父「類」,這樣父「類」上的靜態字段和方法能被子「類」繼承
好,要點看完了。后面內容跟繼承關系不大,但既然源碼扒都扒了,我們不妨繼續深入探索一些場景:
無繼承——靜態函數看一個簡單的代碼:
class Animal { static create() { return new Animal() } }
首先要知道,這個「靜態」同樣不是強類型類繼承語言里有的「靜態」的概念。所謂靜態,就是說它跟實例是沒關系的,而跟「類」本身有關系。比如,你可以這樣調用:Animal.create(),但不能這樣用:new Animal().create。什么場景下會用到這種模式呢?比如說:
工廠模式或單例模式
Object.create、Object.keys 等常用方法
既然只有通過構造函數本身去調用,而不能通過實例來調用,期望它們被綁定到函數本身上似乎很自然。我們來看看上面這段代碼將被如何編譯:
"use strict" var _createClass = (function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i] descriptor.enumerable = descriptor.enumerable || false descriptor.configurable = true if ("value" in descriptor) descriptor.writable = true Object.defineProperty(target, descriptor.key, descriptor) } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps) if (staticProps) defineProperties(Constructor, staticProps) return Constructor } })() function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function") } } var Animal = (function() { function Animal() { _classCallCheck(this, Animal) } _createClass(Animal, null, [ { key: "create", value: function create() {}, }, ]) return Animal })()
熟悉的函數,熟悉的配方。與本文的第二個例子相比,僅有一個地方的不同:create 方法是作為 _createClass 方法的第三個參數被傳入的,這正是我們上文提到的 staticProps 參數:
var _createClass = (function() { function defineProperties(target, props) { ... } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps) if (staticProps) defineProperties(Constructor, staticProps) return Constructor } })() _createClass(Animal, null, [ { key: "create", value: function create() {}, }, ])
可以看見,create 方法是直接被創建到 Animal 上的:defineProperties(Animal, [{ key: "create", value: function() {} }]),最終會將函數賦給 Animal.create。我們的猜測并沒有錯誤。
無繼承——靜態變量class Tiger { static TYPE = "REAL" }
還有個小例子。如果是靜態變量的話,同樣因為不希望在實例對象上所使用,我們會看到編譯出來的代碼中它是直接被設置到函數上。代碼已經很熟悉,不必再講。
"use strict" function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function") } } var Tiger = function Tiger() { _classCallCheck(this, Tiger) } Tiger.TYPE = "REAL"
有趣的是,靜態變量會不會被「子類」繼承呢?這個可請讀者自己做個實驗,驗證驗證。
神秘的類 arrow function寫 React 的東西,一定遇見過這個問題:
class Button extends React.Component { constructor() { super() this.state = { isToggleOn: true, } // 畫重點
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98784.html
摘要:下面是用實現轉成抽象語法樹如下還支持繼承以下是轉換結果最終的結果還是代碼,其中包含庫中的一些函數。可以使用新的易于使用的類定義,但是它仍然會創建構造函數和分配原型。 這是專門探索 JavaScript 及其所構建的組件的系列文章的第 15 篇。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 如果你錯過了前面的章節,可以在這里找到它們: JavaScript 是...
摘要:巧前端基礎進階全方位解讀前端掘金我們在學習的過程中,由于對一些概念理解得不是很清楚,但是又想要通過一些方式把它記下來,于是就很容易草率的給這些概念定下一些方便自己記憶的有偏差的結論。 計算機程序的思維邏輯 (83) - 并發總結 - 掘金從65節到82節,我們用了18篇文章討論并發,本節進行簡要總結。 多線程開發有兩個核心問題,一個是競爭,另一個是協作。競爭會出現線程安全問題,所以,本...
摘要:巧前端基礎進階全方位解讀前端掘金我們在學習的過程中,由于對一些概念理解得不是很清楚,但是又想要通過一些方式把它記下來,于是就很容易草率的給這些概念定下一些方便自己記憶的有偏差的結論。 計算機程序的思維邏輯 (83) - 并發總結 - 掘金從65節到82節,我們用了18篇文章討論并發,本節進行簡要總結。 多線程開發有兩個核心問題,一個是競爭,另一個是協作。競爭會出現線程安全問題,所以,本...
摘要:年,很多人已經開始接觸環境,并且早已經用在了生產當中。我們發現,關鍵字會被編譯成構造函數,于是我們便可以通過來實現實例的生成。下一篇文章我會繼續介紹如何處理子類的并會通過一段函數橋梁,使得環境下也能夠繼承定義的。 2017年,很多人已經開始接觸ES6環境,并且早已經用在了生產當中。我們知道ES6在大部分瀏覽器還是跑不通的,因此我們使用了偉大的Babel來進行編譯。很多人可能沒有關心過,...
摘要:使用新的易用的類定義,歸根結底也是要創建構造函數和修改原型。首先,它把構造函數當成單獨的函數且包含類屬性集。該節點還儲存了指向父類的指針引用,該父類也并儲存了構造函數,屬性集和及父類引用,依次類推。 原文請查閱這里,略有刪減,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第...
閱讀 2771·2021-10-11 11:08
閱讀 1489·2021-09-30 09:48
閱讀 1049·2021-09-22 15:29
閱讀 1037·2019-08-30 15:54
閱讀 976·2019-08-29 15:19
閱讀 527·2019-08-29 13:12
閱讀 3161·2019-08-26 13:53
閱讀 958·2019-08-26 13:28