摘要:我是的可以改變函數的對象的指向拋出異常,沒有這個因為子類和超類都是構造函數,那么就會有之前說的,構造函數在的時候,里面的方法函數會重復創建實例,導致資源浪費。
我來重新學習js 的面向對象(part 4)
續上一篇,隨著業務越來越大,要考慮一些繼承的玩意了,大千世界,各種東西我們要認識和甄別是需要靠大智慧去分門別類,生物學中把動植物按界、門、綱、目、科、屬、種進行分類的方法可能是最有代表的實例之一.........
說人話就是,我們終于要學習繼承的知識了,然后用這些知識去解決老板的問題。一、繼承-原型鏈
繼承是 OOP 開發中的一個極為重要的概念,而在javascript 里面,實現繼承的方式主要依靠原型鏈來實現的。
圖片來自:https://www.lieyuncj.com/p/3087
圖一,一環扣一環,形成了鏈條,可以適當幫助理解原型鏈的概念,原型鏈,換言之就是原型對象構成的鏈。
圖片來源于:https://hackernoon.com/unders...
回顧一下,構造函數,原型和實例的關系:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針,當我們將原型對象等于另外一個類型的實例的時候,就會出現原型對象包含一個指向另外一個原型的指針,例如 dog原型對象 指向了 animal原型對象。
繼續回到現場,我們做了一些分類,食物下面分了水果分類:
// 定義一個 Food 的構造函數 function Food() { this.type = "食物"; } // 定義了 Food 的原型對象的一個方法 getType Food.prototype.getType = function() { return this.type; }; // 定義一個 Fruit 的構造函數 function Fruit() { this.type = "水果"; } // 將 Fruit 的原型對象指向 Food 的實例 Fruit.prototype = new Food(); // 定義 Fruit 的原型對象的一個方法 getType Fruit.prototype.getType = function() { return this.type; }; var food1 = new Fruit(); console.log(food1.getType()); // 返回 水果
前半段都是一樣的,直至將 Fruit 的原型對象指向 Food 的實例,于是Fruit原型不僅擁有了 Food 實例的全部屬性和方法,也擁有了 Food 實例的原型對象(因為 Food 實例里面有 prototype 指向Food Prototype)
這種粗暴的直接將父對象的實例塞進去子對象的原型里面的方式,直接促成了Fruit 繼承 Food。
我最喜歡用《javascript 高級程序設計》第三版的圖來說明,因為他畫的比較詳細而且容易看明白(雖然我也是看了十來遍才看懂),借用他的例子和圖來解釋我們的例子:
可以看到現在這里子對象 subtype 的 原型對象是 superType,因為也是直接粗暴的塞進去的。
如果要看完整的他的原型鏈,可以參看這個圖:
相當詳細,這里之所以有 Object 是因為 javascript 里面一切皆是對象,默認的最頂級的原型就是 Object Prototype。(怎么看這個圖,可以翻看之前一集介紹原型的內容)
下面需要注意一些原型對象的問題和技巧1.1 確定原型和實例的關系
沒辦法準確知道是繼承于哪一個,只要是在鏈條里面的,都會被認為是繼承過來的。
console.log(food1 instanceof Fruit) // 返回 true console.log(food1 instanceof Food) // 返回 true console.log(food1 instanceof Object) // 返回 true console.log(Fruit.prototype.isPrototypeOf(food1)) // 返回 true console.log(Food.prototype.isPrototypeOf(food1)) // 返回 true console.log(Object.prototype.isPrototypeOf(food1)) // 返回 true
這里也跟javascript 的原型搜索機制有關系,當訪問一個實例屬性時候,首先會在實例中搜索該屬性,如果沒有找到該屬性,就會繼續搜索實例的原型對象,在通過原型鏈實現繼承的情況下,搜索過程就會一直沿著原型鏈繼續向上搜索。
類似下圖:
圖片來源于:http://www.cnblogs.com/keepfo...
① 給原型添加方法的代碼一定要放在替換原型的語句之后
正確的例子:
// 定義一個 Food 的構造函數 function Food() { this.type = "食物"; } // 定義了 Food 的原型對象的一個方法 getType Food.prototype.getType = function() { return "food 的 getType 方法"; }; // 定義一個 Fruit 的構造函數 function Fruit() { this.type = "水果"; } // 將 Fruit 的原型對象指向 Food 的實例 Fruit.prototype = new Food(); // 給子類 Fruit 的原型添加一個新方法getSubType Fruit.prototype.getSubType = function() { return "Fruit 的getSubType"; }; // 重寫父類 Food 的方法getType Food.prototype.getType = function() { return false; }; var food1 = new Fruit(); console.log(food1.getSubType()); // 返回 Fruit 的getSubType console.log(food1.getType()); // 返回 false
子類 Fruit 重寫父類(超類)的原型對象的方法getType,在調用的時候會覆蓋屌父類 Food的原型對象的getType方法,直接使用子類Fruit的getType
子類 Fruit 添加一個方法到自己的原型對象里面,也是很正常的,能夠被直接使用。
錯誤的例子:
// 定義一個 Food 的構造函數 function Food() { this.type = "食物"; } // 定義了 Food 的原型對象的一個方法 getType Food.prototype.getType = function() { return "food 的 getType 方法"; }; // 定義一個 Fruit 的構造函數 function Fruit() { this.type = "水果"; } // 給子類 Fruit 的原型添加一個新方法getSubType Fruit.prototype.getSubType = function() { return "Fruit 的getSubType"; }; // 重寫父類 Food 的方法getType Food.prototype.getType = function() { return false; }; // 將 Fruit 的原型對象指向 Food 的實例 Fruit.prototype = new Food(); var food1 = new Fruit(); console.log(food1.getSubType()); // 拋出 error 異常 console.log(food1.getType()); // 返回 false
food1.getSubType() 直接拋出異常,提示說方法找不到或者未定義
主要就是因為子原型對象被替換的時候會被完全覆蓋。1.3 在通過原型鏈實現繼承時,不能使用對象字面量方法創建原型
主要是因為對象字面量方法會重寫原型鏈,這個原理在之前章節說過,這里只是再次提醒。
// 省略。。。 Fruit.prototype = new Food(); Fruit.prototype = { // 被重寫了原型鏈,就不屬于原來的原型鏈范圍了。 // xxxxxxx } // 省略。。。1.4 原型鏈的問題
原型鏈最大的問題是來自包含引用類型值的原型,這種類型值的原型屬性會被所有實例共享,導致沒辦法很好隔離,所以之前也是使用構造函數和原型模式組合使用來解決這個問題,但當時沒有觸及真正的繼承。
原型鏈另外一個問題是,在創建子類型的實例時,不能向超類型的構造函數中傳遞參數,或者說,是沒辦法在不影響所有對象實例情況下,給超類型的構造函數傳遞參數。
基于以上2個問題,導致了實際環境中,很少會多帶帶使用原型鏈,會結合其他方式來使用原型鏈,畢竟 javascript 里,所有的繼承其實也是以原型鏈為基礎的。二、繼承-借用構造函數、偽造對象、經典繼承
圖片來自:https://www.tvmao.com/drama/K...
鑒于之前原型鏈的問題兩大問題,所以機智的工程師想出來利用構造函數來搭配使用,這個技術就叫做借用構造函數 constructor stealing(很 low 有沒有!),有時候叫偽造對象,或者叫經典繼承(逼格瞬間飆升到完全看不懂,但覺得很厲害,有木有!)
核心思想是在子類型構造函數的內部調用超類型改造函數。
單純使用原型鏈繼承的時候:
function Food() { this.colors = ["red", "blue"]; } function Fruit() {} Fruit.prototype = new Food(); var food1 = new Fruit(); var food2 = new Fruit(); console.log(food1.colors); // 返回 [ "red", "blue" ] console.log(food2.colors); // 返回 [ "red", "blue" ] food1.colors.push("yellow"); console.log(food1.colors); // 返回 [ "red", "blue", "yellow" ] console.log(food2.colors); // 返回 [ "red", "blue", "yellow" ]
使用借用構造函數模式繼承的時候:
function Food() { this.colors = ["red", "blue"]; } function Fruit() { Food.call(this); // call 可以改變函數的this對象的指向 } var food1 = new Fruit(); console.log(food1.colors); // 返回 [ "red", "blue" ] food1.colors.push("yellow"); console.log(food1.colors); // 返回 [ "red", "blue", "yellow" ] var food2 = new Fruit(); console.log(food2.colors); // 返回 [ "red", "blue" ]
可以看到截然不同的兩種效果,后者的實例的數組(引用類型的數據)并沒有跟隨其他實例變化而變化,是互相獨立的。
為什么可以這樣呢?
利用了函數的執行環境上下文,這里的“繼承”的目的只是為了能夠使用超類的屬性和方法(不算是真正的繼承),所以直接將超類的構造函數放到子類的構造函數里面執行,從而將他們進行合體。
利用了 call(或者 apply 或者 bind 這種函數)改變了構造函數的 this 指向,才得以實現上面說到的將不同的構造函數放到同一個執行環境中執行。
2.1 傳參下面兩個例子分別說明了,這種繼承方式可以傳參的,并且傳參之后也是可以重寫超類的屬性的。
例子1:
function Food(name) { this.name = name; this.colors = ["red", "blue"]; } function Fruit() { Food.call(this, "蘋果"); // call 可以改變函數的this對象的指向 } var food1 = new Fruit(); console.log(food1.name); // 返回 蘋果
例子2:
function Food(name) { // 參數 this.name = name; this.colors = ["red", "blue"]; } function Fruit() { Food.call(this, "蘋果"); // call 可以改變函數的this對象的指向,加上了傳參 this.place = "非洲"; // 添加屬性 this.name = "香蕉"; // 重寫超類屬性 } var food1 = new Fruit(); console.log(food1.name); // 返回 蘋果 console.log(food1.place); // 返回 非洲2.2 這種方式的問題
圖片來自:https://www.youtube.com/watch...
正如之前所說,這種不是真正的繼承,只是想子類和父類進行了強行合體罷了,這種合體方式能夠滿足一般繼承的要求,但是帶了其他問題:
沒辦法使用超類的原型對象里面定義的方法。
function Food() { this.colors = ["red", "blue"]; } Food.prototype.getType = function () { console.log("我是 food 的getType"); } function Fruit() { Food.call(this); // call 可以改變函數的this對象的指向 } var food1 = new Fruit(); console.log(food1.getType()); // 拋出異常,沒有這個 function
因為子類和超類都是構造函數,那么就會有之前說的,構造函數在 new 的時候,里面的方法(函數)會重復創建 function 實例, 導致資源浪費。
function Food() { this.colors = ["red", "blue"]; } function Fruit() { Food.call(this); // call 可以改變函數的this對象的指向 this.getType = function() { console.log("我是 food 的getType"); }; } var food1 = new Fruit(); var food2 = new Fruit(); console.log(food1.getType == food2.getType); // 返回 false
鑒于這種問題,在小規模程序設計里面還好,但是一旦規模稍微變得復雜之后,就沒法控制代碼了,那我們機智的工程師們還要繼續想想辦法。參考內容
紅寶書,javascript 高級程序設計第三版
原文轉載:https://www.godblessyuan.com/...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/98325.html
摘要:其實在之前的工廠模式里面,也存在這個問題,不過工廠模式更徹底,直接完全創建一個新對象,而構造函數模式的話只是方法會被重新創建。 我來重新學習 javascript 的面向對象(part 1) 很多job 的描述都說要求精通 javascript 面向對象編程,但是根據一般的套路,寫精通其實就是熟練,寫熟練其實就是一般,寫一般其實就是懵逼! showImg(https://segment...
摘要:無限增殖返回蘋果返回香蕉返回返回使用的新語法方法會創建一個新對象,使用現有的對象來提供新創建的對象的。是新增的,用來規范原型式繼承。這里將返回的新對象放到子類的原型對象里面,這樣子類就擁有了父類的原型對象,也就實現了方法的繼承。 這是最后的最后了,我會順便總結一下各種繼承方式的學習和理解。(老板要求什么的,管他呢) 一、繼承-組合繼承、偽經典繼承 showImg(https://seg...
摘要:二動態原型模式動態原型模式的特點是,在構造函數里面增加判斷處理是否添加原型對象屬性。他依然有一個嚴重的問題,就是原型對象和實例和構造函數之間沒辦法關聯,這樣不適合在有一定規模復雜度的程序開發中使用。 續上一集內容,有一些數據不需要共享的時候,但是又想實現共享數據處理,魚與熊掌,都要兼得(老板就是這么霸氣),那么經過工程師們的智慧交流,他們發現現實并非那么殘酷,還有一些辦法可取的,也就是...
摘要:先來說其實構造函數也有,原型對象有,實例有也有,或者更加籠統的說,所有對象都是有的。構造函數的原型對象上的會指向構造函數。由于屬性是可以變更的,所以未必真的指向對象的構造函數,只是一個提示。 續上一集內容,通過構造函數的方式,成功地更新了生產技術,老板笑呵呵,工人少奔波,只是問題總比辦法多,又遇到一個新問題,就是會造成一些資源的重復和浪費,那么經過工程師們的智慧交流,他們產生了一個新技...
摘要:請記住,這些書中的一些可能不是最新的,但概念和基礎仍應適用。是最好的老師之一。的秘密由部分組成。在你完成這些書后,查看書籍和最好的本土書籍。 我看過三本,第1本,第二本,第四本。第一本買的的實體書,其他兩本看的是電子書。第一本是大名鼎鼎老道寫的,書很薄,但是非常經典。javascirpt忍者秘籍是jquery的作者寫的,也是非常經典。you dont kown js系列也是非常好。看了...
閱讀 3543·2021-11-23 10:10
閱讀 3316·2019-08-30 14:03
閱讀 2071·2019-08-30 13:09
閱讀 3400·2019-08-29 15:29
閱讀 1547·2019-08-29 11:23
閱讀 2013·2019-08-28 18:28
閱讀 2848·2019-08-26 13:34
閱讀 2173·2019-08-26 11:32