javascript
你真的理解JS的继承了吗?
噫吁嚱,js之難,難于上青天
學(xué)習(xí)js的這幾年,在原型鏈和繼承上花了不知道多少時間,每當(dāng)自以為已經(jīng)吃透它的時候,總是不經(jīng)意的會出現(xiàn)各種難以理解的幺蛾子。也許就像kyle大佬說的那樣,js的繼承真的是‘蠢弟弟’設(shè)計模式吧。
本文小綱介紹
- es5寄生組合繼承
- es6的class ... extends ...繼承
- kyle大佬倡導(dǎo)的行為委托
閱讀本文之前先約定,本文中稱 __proto__ 為 內(nèi)置原型,稱 prototype 為 原型對象,構(gòu)造函數(shù) SubType 和 SuperType 分別稱為子類和父類。
請先看下圖,如果各位覺得soeasy,請直接 插隊這里 。
es5寄生組合繼承
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' 復(fù)制代碼此時,代碼中生成的原型鏈關(guān)系如下圖所示(下面三張圖擼了一下午,喜歡的幫忙點個贊謝謝):
-
子類的原型對象的 __proto__ 指向父類的原型對象。 圖中有兩種顏色的帶箭頭的線,紅色的線是我們生成的實例的原型鏈,是我們之所以能調(diào)用到 instance.sayName() 和 instance.sayAge() 的根本所在。當(dāng)調(diào)用instance.sayName()的時候,js引擎會先查找instance對象中的自有屬性。未找到sayName屬性,則繼續(xù)沿原型鏈查找,此時instance通過內(nèi)置原型__proto__鏈到了SubType.prototype對象上。但在SubType.prototype上也未找到sayName屬性,繼續(xù)沿原型鏈查找,此時SubType.prototype的__proto__鏈到了SuperType.prototype對象上。在對象上找到了sayName屬性,于是查找結(jié)束,開始調(diào)用。因此調(diào)用instance.sayName()相當(dāng)于調(diào)用了instance.__proto__.__proto__.sayName(),只不過前者中sayName函數(shù)內(nèi)this指向instance實例對象,而后者sayName函數(shù)內(nèi)的this指向了SuperType.prototype(instance.__proto__.__proto__ === SuperType.prototype)對象。
-
在 es5 的實現(xiàn)中,子類的 __proto__ 直接指向的是 Function.prototype。 黑色的帶箭頭的線則是 es5 繼承中產(chǎn)生的‘副作用’,使得所有的函數(shù)的 __proto__ 指向了 Function.prototype,并最終指向 Object.prototype,從而使得我們聲明的函數(shù)可以直接調(diào)用 toString(定義在Function.prototype上)、hasOwnProperty(定義在Object.prototype上) 等方法,如:SubType.toString()、SubType.hasOwnProperty()等。
下面看看es6中有哪些不同吧。
es6的class ... extends ...
class SuperType {constructor(name) {this.name = namethis.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' 復(fù)制代碼可以明顯的發(fā)現(xiàn)這段代碼比之前的更加簡短和美觀。es6 class 實現(xiàn)繼承的核心在于使用關(guān)鍵字 extends 表明繼承自哪個父類,并且在子類構(gòu)造函數(shù)中必須調(diào)用 super 關(guān)鍵字,super(name)相當(dāng)于es5繼承實現(xiàn)中的 SuperType.call(this, name)。
雖然結(jié)果可能如你所料的實現(xiàn)了原型鏈繼承,但是這里還是有個需要注意的點值得一說。
如圖,es6中的 class 繼承存在兩條繼承鏈:
子類prototype屬性的__proto__屬性,表示方法的繼承,總是指向父類的prototype屬性。 這點倒和經(jīng)典繼承是一致的。 如紅線所示,子類SubType的prototype屬性的__proto__指向父類SuperType的prototype屬性。 相當(dāng)于調(diào)用Object.setPrototypeOf(SubType.prototype, SuperType.prototype); 因為和經(jīng)典繼承相同,這里不再累述。
子類的__proto__屬性,表示構(gòu)造函數(shù)的繼承,總是指向父類。 這是個值得注意的點,和es5中的繼承不同,如藍(lán)線所示,子類SubType的__proto__指向父類SuperType。相當(dāng)于調(diào)用了Object.setPrototypeOf(SubType, SuperType); es5繼承中子類和父類的內(nèi)置原型直接指向的都是Function.prototype,所以說Function是所有函數(shù)的爸爸。而在es6class...extends...實現(xiàn)的繼承中,子類的內(nèi)置原型直接指向的是父類。 之所以注意到這點,是因為看 kyle 大佬的《你不知道的javascript 下》的時候,看到了class MyArray extends Array{}和var arr = MyArray.of(3)這兩行代碼,很不理解為什么MyArray上面為什么能調(diào)到of方法。因為按照es5中繼承的經(jīng)驗,MyArray.__proto__應(yīng)該指向了Function.prototype,而后者并沒有of方法。當(dāng)時感覺世界觀都崩塌了,為什么我以前的認(rèn)知失效了?第二天重翻阮一峰老師的《ECMAScript6入門》才發(fā)現(xiàn)原來class實現(xiàn)的繼承是不同的。
知道了這點,就可以根據(jù)需求靈活運用Array類構(gòu)造自己想要的類了:
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' 復(fù)制代碼元屬性Symbol.toPrimitive定義了MyArray的實例發(fā)生強(qiáng)制類型轉(zhuǎn)換的時候應(yīng)該執(zhí)行的方法,hint的值可能是default/number/string中的一種。現(xiàn)在,實例arr能夠在發(fā)生加減乘除的強(qiáng)制類型轉(zhuǎn)換的時候,數(shù)組內(nèi)的每項會自動執(zhí)行加性運算。
以上就是js實現(xiàn)繼承的兩種模式,可以發(fā)現(xiàn)class繼承和es5寄生組合繼承有相似之處,也有不同的地方。雖然class繼承存在一些問題(如暫不支持靜態(tài)屬性等),但是子類的內(nèi)置原型指向父類這點是個不錯的改變,這樣我們就可以利用原生構(gòu)造函數(shù)(Array等)構(gòu)建自己想要的類了。
kyle大佬提到的行為委托
在讀《你不知道的javascript 上》的時候,感觸頗多。這本書真的是本良心書籍,讓我學(xué)會了LHS/RHS,讀懂了閉包,了解了詞法作用域,徹底理解了this指向,基本懂了js的原型鏈繼承。所以當(dāng)時就忍不住又從頭讀了一遍。如果說諸多感受中最大的感受是啥,那一定是行為委托了。我第一次見過有大佬能夠如此強(qiáng)悍(至少沒見過國內(nèi)的大佬這么牛叉的),強(qiáng)悍到直接號召讀者抵制js的繼承模式(無論寄生組合繼承還是class繼承),并且提倡使用行為委托模式實現(xiàn)對象的關(guān)聯(lián)。我真的被折服了,要知道class可是w3c委員會制定出的標(biāo)準(zhǔn),并且已經(jīng)廣泛的應(yīng)用到了業(yè)界中。關(guān)鍵的關(guān)鍵是,我確實認(rèn)為行為委托確實更加清晰簡單(如有異議請指教)。
let SuperType = {initSuper(name) {this.name = namethis.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' 復(fù)制代碼這就是模仿上面js繼承的兩個例子,利用行為委托實現(xiàn)的對象關(guān)聯(lián)。行為委托的實現(xiàn)非常超級極其的簡單,就是把父對象關(guān)聯(lián)到子對象的內(nèi)置原型上,這樣就可以在子對象上直接調(diào)用父對象上的方法。行為委托生成的原型鏈沒有class繼承生成的原型鏈的復(fù)雜關(guān)系,一目了然。當(dāng)然class有其存在的道理,但是在些許場景下,應(yīng)該是行為委托更加合適吧。希望safari盡快實現(xiàn)Object.setPrototypeOf()方法,太out了連ie都支持了。
小子愚鈍,如果行為委托完全能夠?qū)崿F(xiàn)實現(xiàn)class繼承的功能,而且更加簡單和清晰,我們開發(fā)的過程中為什么不愉快的嘗試用一下呢?
總結(jié)
以上是生活随笔為你收集整理的你真的理解JS的继承了吗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构之「堆」
- 下一篇: python json模块使用详情