續(xù)上一篇,隨著業(yè)務越來越大需频,要考慮一些繼承的玩意了丁眼,大千世界,各種東西我們要認識和甄別是需要靠大智慧去分門別類昭殉,生物學中把動植物按界苞七、門、綱挪丢、目蹂风、科、屬乾蓬、種進行分類的方法可能是最有代表的實例之一.........
說人話就是惠啄,我們終于要學習繼承的知識了,然后用這些知識去解決老板的問題任内。
一撵渡、繼承-原型鏈
繼承是 OOP 開發(fā)中的一個極為重要的概念,而在javascript 里面死嗦,實現繼承的方式主要依靠原型鏈來實現的趋距。
圖片來自:https://www.lieyuncj.com/p/3087
圖一,一環(huán)扣一環(huán)越除,形成了鏈條节腐,可以適當幫助理解原型鏈的概念,原型鏈摘盆,換言之就是原型對象構成的鏈翼雀。
圖片來源于:https://hackernoon.com/understand-nodejs-javascript-object-inheritance-proto-prototype-class-9bd951700b29
回顧一下,構造函數骡澈,原型和實例的關系:每個構造函數都有一個原型對象锅纺,原型對象都包含一個指向構造函數的指針掷空,而實例都包含一個指向原型對象的內部指針肋殴,當我們將原型對象等于另外一個類型的實例的時候囤锉,就會出現原型對象包含一個指向另外一個原型的指針,例如 dog原型對象 指向了 animal原型對象护锤。
繼續(xù)回到現場官地,我們做了一些分類,食物下面分了水果分類:
// 定義一個 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 的原型搜索機制有關系,當訪問一個實例屬性時候蟆沫,首先會在實例中搜索該屬性叉讥,如果沒有找到該屬性,就會繼續(xù)搜索實例的原型對象饥追,在通過原型鏈實現繼承的情況下图仓,搜索過程就會一直沿著原型鏈繼續(xù)向上搜索。
類似下圖:
圖片來源于:http://www.cnblogs.com/keepfool/p/5573121.html
1.2 謹慎定義方法
① 給原型添加方法的代碼一定要放在替換原型的語句之后
正確的例子:
// 定義一個 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 在通過原型鏈實現繼承時劫窒,不能使用對象字面量方法創(chuàng)建原型
主要是因為對象字面量方法會重寫原型鏈,這個原理在之前章節(jié)說過拆座,這里只是再次提醒主巍。
// 省略冠息。。孕索。
Fruit.prototype = new Food();
Fruit.prototype = { // 被重寫了原型鏈逛艰,就不屬于原來的原型鏈范圍了。
// xxxxxxx
}
// 省略搞旭。散怖。。
1.4 原型鏈的問題
- 原型鏈最大的問題是來自包含引用類型值的原型肄渗,這種類型值的原型屬性會被所有實例共享镇眷,導致沒辦法很好隔離,所以之前也是使用構造函數和原型模式組合使用來解決這個問題翎嫡,但當時沒有觸及真正的繼承偏灿。
- 原型鏈另外一個問題是,在創(chuàng)建子類型的實例時钝的,不能向超類型的構造函數中傳遞參數翁垂,或者說,是沒辦法在不影響所有對象實例情況下硝桩,給超類型的構造函數傳遞參數沿猜。
基于以上2個問題,導致了實際環(huán)境中碗脊,很少會單獨使用原型鏈啼肩,會結合其他方式來使用原型鏈,畢竟 javascript 里衙伶,所有的繼承其實也是以原型鏈為基礎的祈坠。
二、繼承-借用構造函數矢劲、偽造對象赦拘、經典繼承
圖片來自:https://www.tvmao.com/drama/KyEwYiY=
鑒于之前原型鏈的問題兩大問題,所以機智的工程師想出來利用構造函數來搭配使用芬沉,這個技術就叫做借用構造函數 constructor stealing(很 low 有沒有L赏),有時候叫偽造對象丸逸,或者叫經典繼承(逼格瞬間飆升到完全看不懂蹋艺,但覺得很厲害,有木有;聘铡)
核心思想是在子類型構造函數的內部調用超類型改造函數捎谨。
單純使用原型鏈繼承的時候:
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' ]
可以看到截然不同的兩種效果,后者的實例的數組(引用類型的數據)并沒有跟隨其他實例變化而變化,是互相獨立的涛救。
為什么可以這樣呢畏邢?
- 利用了函數的執(zhí)行環(huán)境上下文,這里的“繼承”的目的只是為了能夠使用超類的屬性和方法(不算是真正的繼承)州叠,所以直接將超類的構造函數放到子類的構造函數里面執(zhí)行,從而將他們進行合體凶赁。
- 利用了 call(或者 apply 或者 bind 這種函數)改變了構造函數的 this 指向咧栗,才得以實現上面說到的將不同的構造函數放到同一個執(zhí)行環(huán)境中執(zhí)行。
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?v=UNiHF-Z0BM0
正如之前所說斟或,這種不是真正的繼承,只是想子類和父類進行了強行合體罷了集嵌,這種合體方式能夠滿足一般繼承的要求萝挤,但是帶了其他問題:
- 沒辦法使用超類的原型對象里面定義的方法。
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
的時候凤粗,里面的方法(函數)會重復創(chuàng)建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
鑒于這種問題蒸走,在小規(guī)模程序設計里面還好提鸟,但是一旦規(guī)模稍微變得復雜之后是整,就沒法控制代碼了庭猩,那我們機智的工程師們還要繼續(xù)想想辦法飒焦。
參考內容
- 紅寶書因妇,javascript 高級程序設計第三版