續上一篇,隨着業務愈來愈大,要考慮一些繼承的玩意了,大千世界,各類東西咱們要認識和甄別是須要靠大智慧去分門別類,生物學中把動植物按界、門、綱、目、科、屬、種進行分類的方法多是最有表明的實例之一.........javascript
說人話就是,咱們終於要學習繼承的知識了,而後用這些知識去解決老闆的問題。
繼承是 OOP 開發中的一個極爲重要的概念,而在javascript 裏面,實現繼承的方式主要依靠原型鏈來實現的。
圖片來自:https://www.lieyuncj.com/p/3087html
圖一,一環扣一環,造成了鏈條,能夠適當幫助理解原型鏈的概念,原型鏈,換言之就是原型對象構成的鏈。java
圖片來源於:https://hackernoon.com/unders...node
回顧一下,構造函數,原型和實例的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針,當咱們將原型對象等於另一個類型的實例的時候,就會出現原型對象包含一個指向另一個原型的指針,例如 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()); // 返回 水果
prototype
指向Food Prototype
)我最喜歡用《javascript 高級程序設計》第三版的圖來講明,由於他畫的比較詳細並且容易看明白(雖然我也是看了十來遍纔看懂),借用他的例子和圖來解釋咱們的例子:app
能夠看到如今這裏子對象subtype
的 原型對象是superType
,由於也是直接粗暴的塞進去的。
若是要看完整的他的原型鏈,能夠參看這個圖:less
至關詳細,這裏之因此有 Object
是由於 javascript 裏面一切皆是對象,默認的最頂級的原型就是 Object Prototype
。(怎麼看這個圖,能夠翻看以前一集介紹原型的內容)函數
下面須要注意一些原型對象的問題和技巧
沒辦法準確知道是繼承於哪個,只要是在鏈條裏面的,都會被認爲是繼承過來的。學習
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...ui
① 給原型添加方法的代碼必定要放在替換原型的語句以後
正確的例子:
// 定義一個 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
getType
,在調用的時候會覆蓋屌父類 Food的原型對象的getType
方法,直接使用子類Fruit的getType
錯誤的例子:
// 定義一個 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()
直接拋出異常,提示說方法找不到或者未定義主要就是由於子原型對象被替換的時候會被徹底覆蓋。
主要是由於對象字面量方法會重寫原型鏈,這個原理在以前章節說過,這裏只是再次提醒。
// 省略。。。 Fruit.prototype = new Food(); Fruit.prototype = { // 被重寫了原型鏈,就不屬於原來的原型鏈範圍了。 // xxxxxxx } // 省略。。。
基於以上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' ]
能夠看到大相徑庭的兩種效果,後者的實例的數組(引用類型的數據)並無跟隨其餘實例變化而變化,是互相獨立的。
爲何能夠這樣呢?
下面兩個例子分別說明了,這種繼承方式能夠傳參的,而且傳參以後也是能夠重寫超類的屬性的。
例子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); // 返回 非洲
圖片來自: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
鑑於這種問題,在小規模程序設計裏面還好,可是一旦規模稍微變得複雜以後,就無法控制代碼了,那咱們機智的工程師們還要繼續想一想辦法。