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