續(xù)上一集內(nèi)容,通過構(gòu)造函數(shù)的方式,成功地更新了生產(chǎn)技術(shù),老板笑呵呵,工人少奔波,只是問題總比辦法多,又遇到一個新問題,就是會造成一些資源的重復(fù)和浪費(fèi),那么經(jīng)過工程師們的智慧交流,他們產(chǎn)生了一個新技術(shù),原型模式。
一、使用原型模式
function Food() {}
Food.prototype.name = "蘋果";
Food.prototype.sayName = function() {
console.log("我是" + this.name);
};
var food1 = new Food();
food1.sayName();
var food2 = new Food();
food2.sayName();
// 創(chuàng)建無限多的 food 。。。。。。
console.log(food1.sayName == food2.sayName); // 返回 true
- 將所有屬性和方法,包括
sayName方法都放到原型Food的原型上去 - 跟之前構(gòu)造函數(shù)創(chuàng)建新對象的方式一樣,使用
new來創(chuàng)建
這樣就完成了原型模式的使用了,能夠?qū)⒑瘮?shù)進(jìn)行共享,不用每次都重復(fù)創(chuàng)建不同的函數(shù)實(shí)例了,而且所有的屬性共享,也能夠很方便節(jié)省代碼和簡化結(jié)構(gòu),其他人也可以很方便地進(jìn)行使用。
但是比較懵逼,為什么這樣就可以了呢?原型是個什么東西?怎么起作用的呢?
1.1 理解什么是原型
在《javascript 高級程序設(shè)計》里面是這樣說的,我們創(chuàng)建的每個函數(shù)都有一個 prototype,這個屬性是一個內(nèi)存指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法。
換句大白話來說:
- 原型就是根,所有東西都有根,是來自于哪里,是被誰創(chuàng)造出來的,并且能夠通過這個根去追溯父輩祖輩的信息。
- 全部東西都會由原型創(chuàng)造,所以都會帶有一個原型屬性,只是不同的原型創(chuàng)造出來的東西帶有不同的原型屬性。
例如圖1,可以粗獷地理解為蛋是雞生的,所以蛋的原型是雞。
圖片引用來自:https://hackernoon.com/understand-nodejs-javascript-object-inheritance-proto-prototype-class-9bd951700b29
或者用親屬關(guān)系來理解,原型就是你的父輩祖輩。
圖片引用自http://china.findlaw.cn/info/hy/hyfwsfb/qita/1062124.html
例如圖2,這是一個類樹狀結(jié)構(gòu)的組織:
圖片引用自:http://rohitnsit08.blogspot.com/2011/06/javascript-object-system.html
- 這里的
global object的意思在后面有解釋。 - 從這個圖可以看到,各種不同過的對象,都會有不同的原型,
string的原型就是string.prototype,function的原型就是string.prototype等等,而這些原型的原型就是Object.prototype了,所以就有那么一句話,在 javascript 里面,所有的東西都是對象
在 javascript 里面,global object 有4種:
- 在瀏覽器里面,windows 被稱作是 global object
- 在 nodejs 里面,nodejs 的運(yùn)行本身也是一個 global objec
- 在 Worker 線程下, WorkerGlobalScope 也叫 global object
- 在一般 javascript 運(yùn)行過程中,在所有對象被創(chuàng)建之前,會預(yù)先創(chuàng)建一個 global object,里面包含了所有這個 javascript 引擎里面擁有的屬性和方法,這個也叫做 global object,并且 javascript 的對象系統(tǒng)都是基于這個 global object 建立的。
1.2 理解什么是 constructor 和構(gòu)造函數(shù)的 prototype 和 [[prototype]]
其實(shí)原型是很好理解的東西,就是原來的形態(tài),例如string 的原型就是 string.prototype,字符串的原來的形態(tài)就是字符串原型,但是還有一些比較影響理解和學(xué)習(xí)的東西,例如constructor prototype [[prototype]] 這些。
① 先來說constructor:
其實(shí)構(gòu)造函數(shù)也有constructor,原型對象有constructor,實(shí)例有constructor 也有,或者更加籠統(tǒng)的說,所有對象都是有constructor的。
// 構(gòu)造函數(shù)的constructor,默認(rèn)構(gòu)造函數(shù)的原型對象是Function對象
function Food() {}
console.log(Food.constructor); // 返回[Function: Function]
// 實(shí)例的constructor是Food
var food1 = new Food("蘋果");
console.log(food1.constructor); // 返回 [Function: Food]
// 構(gòu)造函數(shù)的原型對象Food.prototype 也有constructor,指向構(gòu)造函數(shù)
console.log(Food.prototype.constructor); // 返回 [Function: Food]
- 構(gòu)造函數(shù)也是函數(shù),他的
constructor默認(rèn)會指向[Function: Function],也就是函數(shù)原型對象是他的原型對象。 - 當(dāng)使用
new來實(shí)例化對象的時候,實(shí)例的constructor會默認(rèn)指向構(gòu)造函數(shù),證明是哪一個來自于哪一個構(gòu)造函數(shù),但是僅此而已,只是一個標(biāo)識,所以是Food。 - 構(gòu)造函數(shù) 的原型對象
Food Prototype上的constructor會指向構(gòu)造函數(shù)Food。
可以看出,通過
constructor可以看到他們之間的關(guān)系,但是通過constructor連接的關(guān)系是很脆弱的(容易變化,不可靠),也因?yàn)?javascript 在設(shè)計當(dāng)初并沒有太多考慮這個情況,所以constructor比較雞肋。
另外雖然有這么多constructor,但是我們一般討論的比較多的是,原型對象的constructor,他會默認(rèn)指向構(gòu)造函數(shù)的 prototype 屬性,僅此而已,如果他沒有被改變的話,也可以充當(dāng)一種標(biāo)志,代表通過這個構(gòu)造函數(shù)生成的實(shí)例是來自哪個原型的,從而判斷對象的類型是哪一種(但不可靠)。
舉例說明:
function Food() {}
console.log(Food.constructor); // 返回[Function: Function]
// 當(dāng)重寫原型的時候,實(shí)例的constructor 就變成了[Function: Object]
Food.prototype = {
name: "蘋果"
};
var food2 = new Food("蘋果");
console.log(food2.constructor); // 返回 [Function: Object]
這里實(shí)例的constructor就被改變了,所以一般我們可以看到重寫原型的時候(原型鏈被切斷,會默認(rèn)指向默認(rèn)的原型對象),會手動加入一個 constructor 屬性來指定它的值,以方便識別。
function Food() {}
console.log(Food.constructor); // 返回[Function: Function]
// 我們需要主動標(biāo)記 constructor屬性
Food.prototype = {
constructor: Food,
name: "蘋果"
};
var food2 = new Food("蘋果");
console.log(food2.constructor); // 返回 [Function: Food]
constructor其實(shí)沒有什么用處,他是JavaScript語言設(shè)計的歷史遺留物。由于constructor屬性是可以變更的,所以未必真的指向?qū)ο蟮臉?gòu)造函數(shù),只是一個提示。不過,從編程習(xí)慣上,我們應(yīng)該盡量讓對象的constructor指向其構(gòu)造函數(shù),以維持這個慣例。--by 賀師俊
② 再來說prototype
在 javascript 里面,只要是函數(shù),都會有這樣一個屬性prototype,這個屬性指向函數(shù)的原型對象,在默認(rèn)情況下,所有原型對象都會自動獲得一個 constructor 屬性,指向prototype 所在的函數(shù)。
function Food() {}
console.log(Food.prototype) // 返回Food {}
var food1 = new Food("蘋果");
console.log(food1.prototype) // 返回 undefined
- 構(gòu)造函數(shù) Food 的
prototype屬性是一個內(nèi)存指針,最終是指向 Food 的原型對象Food Prototype的,這里需要注意我的寫法,是Food Prototype,這里雖然很相似,但其實(shí)不是。 - 實(shí)例對象上的
prototype屬性沒辦法直接查看,所以返回undefined,需要用別的方法來查看。
③ 最后說說[[prototype]]和__proto__
這是存在于實(shí)例身上的 prototype 屬性,但是沒辦法直接查看,只能通過某些方式來獲取和判斷。不同的瀏覽器有不同的叫法,的屬性名字也可能是[[prototype]] 或者_proto_。
// Object.getPrototypeOf會返回原型對象,但看起來跟普通構(gòu)造函數(shù)沒區(qū)別
console.log(Object.getPrototypeOf(food1)); // 返回 Food {}
// 所以一般會使用這個方式來判斷原型對象是否一致
console.log(Object.getPrototypeOf(food1) === Food.prototype); // 返回 true
// 或者這個方式,isPrototypeOf會直接判斷
console.log(Food.prototype.isPrototypeOf(food1)) // 返回 true
1.3 為什么能夠通過原型模式來解決問題呢?
原型的作用主流程:
- 在 javascript 里面,創(chuàng)建一個新函數(shù)(對象),都會在創(chuàng)建過程里面增加一個
prototype屬性,也就是原型屬性,這個屬性指向構(gòu)造函數(shù)的原型對象,例如food1的prototype屬性指向Food的原型對象Food prototype。 - 而這個被指向的原型對象里面也會自動獲得一個
constuctor構(gòu)造函數(shù)屬性,這個屬性里面包含了一個指向,指向之前被創(chuàng)建的對象的prototype屬性的所在位置,相當(dāng)于原型對象是母體,被創(chuàng)建的對象會關(guān)聯(lián)到母體身上, - javascript 解析器讀取到對象之后,會執(zhí)行一次搜索,如果在當(dāng)前對象上沒有搜索到目標(biāo)屬性的話,就會繼續(xù)搜索指針指向的原型對象,會不斷逐級查找(原型對象),直至找到為止。
參考前面所說的原型,constructor 和 prototype 的內(nèi)容來理解
在《javascript 高級程序設(shè)計》第三版里面的有一幅圖:
- person1 和 person2 都是實(shí)例,
Person是構(gòu)造函數(shù),Person Prototype是 Person 構(gòu)造函數(shù)的原型對象。 - person1或者 person2 的
[[Prototype]]都指向了Person Prototype。
問什么這里會有一條線指向了構(gòu)造函數(shù)?
因?yàn)?code>Person 構(gòu)造函數(shù)的prototype 指向了Person Prototype,而Person Prototype的 constructor 也指向了Person 構(gòu)造函數(shù),他們之間通過這樣來互相確認(rèn)“關(guān)聯(lián)狀態(tài)”,但僅僅互相確認(rèn)關(guān)系而已(因?yàn)?constructor 容易被改變)。
類比到我們的 Food 例子里面去,food1和
Food和Food Prototype的關(guān)系就跟 person1和 person2和Person和Person Prototype的關(guān)系是一樣的。
二、 對于原型的一些使用技巧
① 如果需要查找這個實(shí)例對象的原型的話,可以使用Object.getPrototypeOf ,他會返回整個原型對象。
function Food() {}
Food.prototype.name = "蘋果";
Food.prototype.sayName = function() {
console.log("我是" + this.name);
};
var food1 = new Food();
console.log(Object.getPrototypeOf(food1)) // 返回 Food { name: '蘋果', sayName: [Function] }
② 只能通過對象實(shí)例訪問保存在原型的值,不能通過對象實(shí)例來重寫原型中的值。
③ 對象實(shí)例可以重寫從原型對象中“繼承”過來的同名屬性,這時候會切斷對象實(shí)例和原型對象的某個同名屬性的聯(lián)系,如果想恢復(fù)聯(lián)系即恢復(fù)沒改過的同名屬性的話,可以使用delete刪除對象實(shí)例的某個屬性。
④ hasOwnProperty()方法可以檢測一個屬性是存在于實(shí)例中還是存在于原型中。
function Food() {}
Food.prototype.name = "蘋果";
Food.prototype.sayName = function() {
console.log("我是" + this.name);
};
var food1 = new Food();
console.log(food1.hasOwnProperty("name")); // 返回 false
food1.name = "bilibili"; // 設(shè)置 food1的 name 屬性(也就是改寫從原型對象繼承過來的 name 屬性)
console.log(food1.hasOwnProperty("name")); // 返回 true
console.log(food1.name); // 返回 bilibili
⑤ 更簡單的原型寫法
function Food() {}
Food.prototype = {
constructor: Food, // 這里需要注意
name: '蘋果',
};
- 如果不寫
constructor的話,Food.prototype的constructor就不再指向Food,這樣就沒辦法通過constructor來識別得到改對象實(shí)例是屬于哪個原型對象了。 - 以這種方式編寫原型的時候,如果主動設(shè)置
constructor,對象的[[Enumerable]]可遍歷屬性就會被設(shè)置為 true,代表可以被遍歷。
⑥ 在原型對象上直接編輯修改,會即時反應(yīng)到實(shí)例對象上,所以可以隨時進(jìn)行修改,很方便。
⑦ 如果重寫原型對象,要注意原型對象的指向問題:
// 原型鏈會被切斷
function Food() {
}
var food1 = new Food("蘋果"); // 繼續(xù)指向原來的 Food.prototype(最初的那個原型對象)
// 重寫Food.prototype
Food.prototype = {
constructor: Food,
name: '蘋果',
};
console.log(food1.name); // 返回 undefined
// 原型鏈不會被切斷
function Food() {
}
// 先重寫Food.prototype
Food.prototype = {
constructor: Food,
name: '蘋果',
};
// 再實(shí)例化對象
var food1 = new Food("蘋果"); // 指向新的被重寫后的Food.prototype
console.log(food1.name); // 返回 蘋果
在 new 創(chuàng)建完實(shí)例之后,實(shí)例的原型對象是構(gòu)造函數(shù)的原型對象,如果在這時候重寫了構(gòu)造函數(shù)的原型對象的話,那么原來實(shí)例跟原來構(gòu)造函數(shù)的原型對象的鏈接就會被切斷,就無法使用原型對象上的數(shù)據(jù)了。
三、文末我們又遇到了新問題了
用了原型模式之后,雖然解決了遇到的一系列問題,但也帶來了一些新的副作用(怎么副作用那么多。。。。。),原型模式的共享特性帶來了方便之余,也造成了一些困擾,如果我們需要一些不想共享的信息,例如 food1 的原產(chǎn)地是巴西,印度,非洲,food2的原產(chǎn)地是巴西,印度,俄羅斯,他們之間有一些區(qū)別,不能完全共享,那么怎么辦呢?
會通過組合使用構(gòu)造函數(shù)模式和原型模式或者動態(tài)原型模式來解決,下回分解。
參考內(nèi)容
- 紅寶書,《javascript 高級程序設(shè)計第三版》
版權(quán)信息
作者: 慫如鼠
網(wǎng)站: https://www.whynotbetter.com
本作品著作權(quán)歸作者所有,商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。