噫籲嚱,js之難,難於上青天
學習js的這幾年,在原型鏈和繼承上花了不知道多少時間,每當自覺得已經吃透它的時候,老是不經意的會出現各類難以理解的幺蛾子。也許就像kyle大佬說的那樣,js的繼承真的是‘蠢弟弟’設計模式吧。javascript
閱讀本文以前先約定,本文中稱 __proto__ 爲 內置原型,稱 prototype 爲 原型對象,構造函數 SubType 和 SuperType 分別稱爲子類和父類。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } SubType.prototype = Object.create(SuperType.prototype, { constructor: { value: SubType, enumerable: false, writable: true, configurable: true } }) SubType.prototype.sayAge = function(){ alert(this.age); }; let instance = new SubType('gim', '17'); instance.sayName(); // 'gim' instance.sayAge(); // '17'
SuperType
sayName
。SubType
,並在將來將要新建立的 SubType
實例環境上調用父類 SuperType.call
,以獲取父類中的 name
和 colors
屬性。Object.create()
方法把子類的原型對象上的內置對象 __proto__
指向了父類的原型對象,並把子類構造函數從新賦值爲子類。instance
。(調用new SubType('gim', '17')
的時候會生成一個__proto__
指向SubType.prototype
的空對象,而後把this
指向這個空對象。在添加完name、colors、age
屬性以後,返回這個‘空對象’,也就是說instance
最終就是這個‘空對象’。)此時,代碼中生成的原型鏈關係以下圖所示(下面三張圖擼了一下午,喜歡的幫忙點個贊謝謝):java
__proto__
指向父類的原型對象。 圖中有兩種顏色的帶箭頭的線,紅色的線是咱們生成的實例的原型鏈,是咱們之因此能調用到 instance.sayName()
和 instance.sayAge()
的根本所在。當調用instance.sayName()
的時候,js引擎會先查找instance
對象中的自有屬性。未找到sayName
屬性,則繼續沿原型鏈查找,此時instance
經過內置原型__proto__
鏈到了SubType.prototype
對象上。但在SubType.prototype
上也未找到sayName
屬性,繼續沿原型鏈查找,此時SubType.prototype
的__proto__
鏈到了SuperType.prototype
對象上。在對象上找到了sayName
屬性,因而查找結束,開始調用。所以調用instance.sayName()
至關於調用了instance.__proto__.__proto__.sayName()
,只不過前者中sayName
函數內this
指向instance
實例對象,然後者sayName
函數內的this
指向了SuperType.prototype(instance.__proto__.__proto__ === SuperType.prototype)
對象。__proto__
直接指向的是 Function.prototype
。 黑色的帶箭頭的線則是 es5 繼承中產生的‘反作用’,使得全部的函數的 __proto__
指向了 Function.prototype
,並最終指向 Object.prototype,從而使得咱們聲明的函數能夠直接調用 toString
(定義在Function.prototype上)、hasOwnProperty
(定義在Object.prototype上) 等方法,如:SubType.toString()、SubType.hasOwnProperty()
等。下面看看es6中有哪些不一樣吧。es6
class SuperType { constructor(name) { this.name = name this.colors = ["red", "blue", "green"]; } sayName() { alert(this.name) } } class SubType extends SuperType { constructor(name, age){ super(name) this.age = age } sayAge() { alert(this.age) } } let instance = new SubType('gim', '17'); instance.sayName(); // 'gim' instance.sayAge(); // '17'
能夠明顯的發現這段代碼比以前的更加簡短和美觀。es6 class
實現繼承的核心在於使用關鍵字 extends
代表繼承自哪一個父類,而且在子類構造函數中必須調用 super
關鍵字,super(name)
至關於es5繼承實現中的 SuperType.call(this, name)
。設計模式
雖然結果可能如你所料的實現了原型鏈繼承,可是這裏仍是有個須要注意的點值得一說。數組
class
繼承存在兩條繼承鏈:prototype
屬性的__proto__
屬性,表示方法的繼承,老是指向父類的prototype
屬性。這點倒和經典繼承是一致的。
如紅線所示,子類SubType
的prototype
屬性的__proto__
指向父類SuperType
的prototype
屬性。
至關於調用Object.setPrototypeOf(SubType.prototype, SuperType.prototype)
;
由於和經典繼承相同,這裏再也不累述。閉包
__proto__
屬性,表示構造函數的繼承,老是指向父類。這是個值得注意的點,和es5中的繼承不一樣,如藍線所示,子類SubType
的__proto__
指向父類SuperType
。至關於調用了Object.setPrototypeOf(SubType, SuperType)
;
es5繼承中子類和父類的內置原型直接指向的都是Function.prototype
,因此說Function
是全部函數的爸爸。而在es6class...extends...
實現的繼承中,子類的內置原型直接指向的是父類。
之因此注意到這點,是由於看 kyle 大佬的《你不知道的javascript 下》的時候,看到了class MyArray extends Array{}
和var arr = MyArray.of(3)
這兩行代碼,很不理解爲何MyArray
上面爲何能調到of
方法。由於按照es5中繼承的經驗,MyArray.__proto__
應該指向了Function.prototype
,然後者並無of方法。當時感受世界觀都崩塌了,爲何我之前的認知失效了?次日重翻阮一峯老師的《ECMAScript6入門》才發現原來class
實現的繼承是不一樣的。函數
知道了這點,就能夠根據需求靈活運用Array
類構造本身想要的類了:學習
class MyArray extends Array { [Symbol.toPrimitive](hint){ if(hint === 'default' || hint === 'number'){ return this.reduce((prev,curr)=> prev+curr, 0) }else{ return this.toString() } } } let arr = MyArray.of(2,3,4); arr+''; // '9'
元屬性Symbol.toPrimitive
定義了MyArray
的實例發生強制類型轉換的時候應該執行的方法,hint
的值多是default/number/string
中的一種。如今,實例arr
可以在發生加減乘除的強制類型轉換的時候,數組內的每項會自動執行加性運算。this
以上就是js實現繼承的兩種模式,能夠發現class繼承和es5寄生組合繼承有類似之處,也有不一樣的地方。雖然class繼承存在一些問題(如暫不支持靜態屬性等),可是子類的內置原型指向父類這點是個不錯的改變,這樣咱們就能夠利用原生構造函數(Array等)構建本身想要的類了。es5
在讀《你不知道的javascript 上》的時候,感觸頗多。這本書真的是本良心書籍,讓我學會了LHS/RHS,讀懂了閉包,瞭解了詞法做用域,完全理解了this指向,基本懂了js的原型鏈繼承。因此當時就忍不住又從頭讀了一遍。若是說諸多感覺中最大的感覺是啥,那必定是行爲委託了。我第一次見過有大佬可以如此強悍(至少沒見過國內的大佬這麼牛叉的),強悍到直接號召讀者抵制js的繼承模式(不管寄生組合繼承仍是class繼承),而且提倡使用行爲委託模式實現對象的關聯。我真的被折服了,要知道class但是w3c委員會制定出的標準,而且已經普遍的應用到了業界中。關鍵的關鍵是,我確實認爲行爲委託確實更加清晰簡單(若有異議請指教)。
let SuperType = { initSuper(name) { this.name = name this.color = [1,2,3] }, sayName() { alert(this.name) } } let SubType = { initSub(age) { this.age = age }, sayAge() { alert(this.age) } } Object.setPrototypeOf(SubType,SuperType) SubType.initSub('17') SubType.initSuper('gim') SubType.sayAge() // 'gim' SubType.sayName() // '17'
這就是模仿上面js繼承的兩個例子,利用行爲委託實現的對象關聯。行爲委託的實現很是超級極其的簡單,就是把父對象關聯到子對象的內置原型上,這樣就能夠在子對象上直接調用父對象上的方法。行爲委託生成的原型鏈沒有class繼承生成的原型鏈的複雜關係,一目瞭然。固然class有其存在的道理,可是在些許場景下,應該是行爲委託更加合適吧。但願safari儘快實現Object.setPrototypeOf()
方法,太out了連ie都支持了。
小子愚鈍,若是行爲委託徹底可以實現實現class繼承的功能,並且更加簡單和清晰,咱們開發的過程當中爲何不愉快的嘗試用一下呢?