W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
在編程中,我們經(jīng)常會(huì)想獲取并擴(kuò)展一些東西。
例如,我們有一個(gè) user
對(duì)象及其屬性和方法,并希望將 admin
和 guest
作為基于 user
稍加修改的變體。我們想重用 user
中的內(nèi)容,而不是復(fù)制/重新實(shí)現(xiàn)它的方法,而只是在其之上構(gòu)建一個(gè)新的對(duì)象。
原型繼承(Prototypal inheritance) 這個(gè)語(yǔ)言特性能夠幫助我們實(shí)現(xiàn)這一需求。
在 JavaScript 中,對(duì)象有一個(gè)特殊的隱藏屬性 [[Prototype]]
(如規(guī)范中所命名的),它要么為 null
,要么就是對(duì)另一個(gè)對(duì)象的引用。該對(duì)象被稱為“原型”:
當(dāng)我們從 object
中讀取一個(gè)缺失的屬性時(shí),JavaScript 會(huì)自動(dòng)從原型中獲取該屬性。在編程中,這被稱為“原型繼承”。很快,我們將通過(guò)很多示例來(lái)學(xué)習(xí)此類繼承,以及基于此類繼承的更炫酷的語(yǔ)言功能。
屬性 [[Prototype]]
是內(nèi)部的而且是隱藏的,但是這兒有很多設(shè)置它的方式。
其中之一就是使用特殊的名字 __proto__
,就像這樣:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // 設(shè)置 rabbit.[[Prototype]] = animal
現(xiàn)在,如果我們從 rabbit
中讀取一個(gè)它沒有的屬性,JavaScript 會(huì)自動(dòng)從 animal
中獲取。
例如:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// 現(xiàn)在這兩個(gè)屬性我們都能在 rabbit 中找到:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
這里的 (*)
行將 animal
設(shè)置為 rabbit
的原型。
當(dāng) alert
試圖讀取 rabbit.eats
(**)
時(shí),因?yàn)樗淮嬖谟?nbsp;rabbit
中,所以 JavaScript 會(huì)順著 [[Prototype]]
引用,在 animal
中查找(自下而上):
在這兒我們可以說(shuō) "animal
是 rabbit
的原型",或者說(shuō) "rabbit
的原型是從 animal
繼承而來(lái)的"。
因此,如果 animal
有許多有用的屬性和方法,那么它們將自動(dòng)地變?yōu)樵?nbsp;rabbit
中可用。這種屬性被稱為“繼承”。
如果我們?cè)?nbsp;animal
中有一個(gè)方法,它可以在 rabbit
中被調(diào)用:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// walk 方法是從原型中獲得的
rabbit.walk(); // Animal walk
該方法是自動(dòng)地從原型中獲得的,像這樣:
原型鏈可以很長(zhǎng):
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// walk 是通過(guò)原型鏈獲得的
longEar.walk(); // Animal walk
alert(longEar.jumps); // true(從 rabbit)
現(xiàn)在,如果我們從 longEar
中讀取一些它不存在的內(nèi)容,JavaScript 會(huì)先在 rabbit
中查找,然后在 animal
中查找。
這里只有兩個(gè)限制:
__proto__
? 賦值但會(huì)導(dǎo)致引用形成閉環(huán)時(shí),JavaScript 會(huì)拋出錯(cuò)誤。__proto__
? 的值可以是對(duì)象,也可以是 ?null
?。而其他的類型都會(huì)被忽略。當(dāng)然,這可能很顯而易見,但是仍然要強(qiáng)調(diào):只能有一個(gè) [[Prototype]]
。一個(gè)對(duì)象不能從其他兩個(gè)對(duì)象獲得繼承。
?
__proto__
? 是 ?[[Prototype]]
? 的因歷史原因而留下來(lái)的 getter/setter初學(xué)者常犯一個(gè)普遍的錯(cuò)誤,就是不知道
__proto__
和[[Prototype]]
的區(qū)別。
請(qǐng)注意,
__proto__
與內(nèi)部的[[Prototype]]
不一樣。__proto__
是[[Prototype]]
的 getter/setter。稍后,我們將看到在什么情況下理解它們很重要,在建立對(duì) JavaScript 語(yǔ)言的理解時(shí),讓我們牢記這一點(diǎn)。
__proto__
屬性有點(diǎn)過(guò)時(shí)了。它的存在是出于歷史的原因,現(xiàn)代編程語(yǔ)言建議我們應(yīng)該使用函數(shù)Object.getPrototypeOf/Object.setPrototypeOf
來(lái)取代__proto__
去 get/set 原型。稍后我們將介紹這些函數(shù)。
根據(jù)規(guī)范,
__proto__
必須僅受瀏覽器環(huán)境的支持。但實(shí)際上,包括服務(wù)端在內(nèi)的所有環(huán)境都支持它,因此我們使用它是非常安全的。
由于
__proto__
標(biāo)記在觀感上更加明顯,所以我們?cè)诤竺娴氖纠袑⑹褂盟?
原型僅用于讀取屬性。
對(duì)于寫入/刪除操作可以直接在對(duì)象上進(jìn)行。
在下面的示例中,我們將為 rabbit
的 walk
屬性賦值:
let animal = {
eats: true,
walk() {
/* rabbit 不會(huì)使用此方法 */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
從現(xiàn)在開始,rabbit.walk()
將立即在對(duì)象中找到該方法并執(zhí)行,而無(wú)需使用原型:
訪問器(accessor)屬性是一個(gè)例外,因?yàn)橘x值(assignment)操作是由 setter 函數(shù)處理的。因此,寫入此類屬性實(shí)際上與調(diào)用函數(shù)相同。
也就是這個(gè)原因,所以下面這段代碼中的 admin.fullName
能夠正常運(yùn)行:
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
alert(admin.fullName); // John Smith (*)
// setter triggers!
admin.fullName = "Alice Cooper"; // (**)
alert(admin.fullName); // Alice Cooper,admin 的內(nèi)容被修改了
alert(user.fullName); // John Smith,user 的內(nèi)容被保護(hù)了
在 (*)
行中,屬性 admin.fullName
在原型 user
中有一個(gè) getter,因此它會(huì)被調(diào)用。在 (**)
行中,屬性在原型中有一個(gè) setter,因此它會(huì)被調(diào)用。
在上面的例子中可能會(huì)出現(xiàn)一個(gè)有趣的問題:在 set fullName(value)
中 this
的值是什么?屬性 this.name
和 this.surname
被寫在哪里:在 user
還是 admin
?
答案很簡(jiǎn)單:this
根本不受原型的影響。
無(wú)論在哪里找到方法:在一個(gè)對(duì)象還是在原型中。在一個(gè)方法調(diào)用中,this
始終是點(diǎn)符號(hào) .
前面的對(duì)象。
因此,setter 調(diào)用 admin.fullName=
使用 admin
作為 this
,而不是 user
。
這是一件非常重要的事兒,因?yàn)槲覀兛赡苡幸粋€(gè)帶有很多方法的大對(duì)象,并且還有從其繼承的對(duì)象。當(dāng)繼承的對(duì)象運(yùn)行繼承的方法時(shí),它們將僅修改自己的狀態(tài),而不會(huì)修改大對(duì)象的狀態(tài)。
例如,這里的 animal
代表“方法存儲(chǔ)”,rabbit
在使用其中的方法。
調(diào)用 rabbit.sleep()
會(huì)在 rabbit
對(duì)象上設(shè)置 this.isSleeping
:
// animal 有一些方法
let animal = {
walk() {
if (!this.isSleeping) {
alert(`I walk`);
}
},
sleep() {
this.isSleeping = true;
}
};
let rabbit = {
name: "White Rabbit",
__proto__: animal
};
// 修改 rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined(原型中沒有此屬性)
結(jié)果示意圖:
如果我們還有從 animal
繼承的其他對(duì)象,像 bird
和 snake
等,它們也將可以訪問 animal
的方法。但是,每個(gè)方法調(diào)用中的 this
都是在調(diào)用時(shí)(點(diǎn)符號(hào)前)評(píng)估的對(duì)應(yīng)的對(duì)象,而不是 animal
。因此,當(dāng)我們將數(shù)據(jù)寫入 this
時(shí),會(huì)將其存儲(chǔ)到這些對(duì)象中。
所以,方法是共享的,但對(duì)象狀態(tài)不是。
for..in
循環(huán)也會(huì)迭代繼承的屬性。
例如:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
// Object.keys 只返回自己的 key
alert(Object.keys(rabbit)); // jumps
// for..in 會(huì)遍歷自己以及繼承的鍵
for(let prop in rabbit) alert(prop); // jumps,然后是 eats
如果這不是我們想要的,并且我們想排除繼承的屬性,那么這兒有一個(gè)內(nèi)建方法 obj.hasOwnProperty(key):如果 obj
具有自己的(非繼承的)名為 key
的屬性,則返回 true
。
因此,我們可以過(guò)濾掉繼承的屬性(或?qū)λ鼈冞M(jìn)行其他操作):
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
for(let prop in rabbit) {
let isOwn = rabbit.hasOwnProperty(prop);
if (isOwn) {
alert(`Our: ${prop}`); // Our: jumps
} else {
alert(`Inherited: ${prop}`); // Inherited: eats
}
}
這里我們有以下繼承鏈:rabbit
從 animal
中繼承,animal
從 Object.prototype
中繼承(因?yàn)?nbsp;animal
是對(duì)象字面量 {...}
,所以這是默認(rèn)的繼承),然后再向上是 null
:
注意,這有一件很有趣的事兒。方法 rabbit.hasOwnProperty
來(lái)自哪兒?我們并沒有定義它。從上圖中的原型鏈我們可以看到,該方法是 Object.prototype.hasOwnProperty
提供的。換句話說(shuō),它是繼承的。
……如果 for..in
循環(huán)會(huì)列出繼承的屬性,那為什么 hasOwnProperty
沒有像 eats
和 jumps
那樣出現(xiàn)在 for..in
循環(huán)中?
答案很簡(jiǎn)單:它是不可枚舉的。就像 Object.prototype
的其他屬性,hasOwnProperty
有 enumerable:false
標(biāo)志。并且 for..in
只會(huì)列出可枚舉的屬性。這就是為什么它和其余的 Object.prototype
屬性都未被列出。
幾乎所有其他鍵/值獲取方法都忽略繼承的屬性
幾乎所有其他鍵/值獲取方法,例如
Object.keys
和Object.values
等,都會(huì)忽略繼承的屬性。
它們只會(huì)對(duì)對(duì)象自身進(jìn)行操作。不考慮 繼承自原型的屬性。
[[Prototype]]
? 屬性,它要么是另一個(gè)對(duì)象,要么就是 ?null
?。obj.__proto__
? 訪問它(歷史遺留下來(lái)的 getter/setter,這兒還有其他方法,很快我們就會(huì)講到)。[[Prototype]]
? 引用的對(duì)象被稱為“原型”。obj
? 的一個(gè)屬性或者調(diào)用一個(gè)方法,并且它不存在,那么 JavaScript 就會(huì)嘗試在原型中查找它。obj.method()
?,而且 ?method
? 是從原型中獲取的,?this
? 仍然會(huì)引用 ?obj
?。因此,方法始終與當(dāng)前對(duì)象一起使用,即使方法是繼承的。for..in
? 循環(huán)在其自身和繼承的屬性上進(jìn)行迭代。所有其他的鍵/值獲取方法僅對(duì)對(duì)象本身起作用。下面這段代碼創(chuàng)建了一對(duì)對(duì)象,然后對(duì)它們進(jìn)行修改。
過(guò)程中會(huì)顯示哪些值?
let animal = {
jumps: null
};
let rabbit = {
__proto__: animal,
jumps: true
};
alert( rabbit.jumps ); // ? (1)
delete rabbit.jumps;
alert( rabbit.jumps ); // ? (2)
delete animal.jumps;
alert( rabbit.jumps ); // ? (3)
應(yīng)該有 3 個(gè)答案。
true
?,來(lái)自于 ?rabbit
?。null
?,來(lái)自于 ?animal
?。undefined
?,不再有這樣的屬性存在。本題目有兩個(gè)部分。
給定以下對(duì)象:
let head = {
glasses: 1
};
let table = {
pen: 3
};
let bed = {
sheet: 1,
pillow: 2
};
let pockets = {
money: 2000
};
__proto__
? 來(lái)分配原型,以使得任何屬性的查找都遵循以下路徑:?pockets
? → ?bed
? → ?table
? → ?head
?。例如,?pockets.pen
? 應(yīng)該是 ?3
?(在 ?table
? 中找到),?bed.glasses
? 應(yīng)該是 ?1
?(在 ?head
? 中找到)。pockets.glasses
? 或 ?head.glasses
? 獲取 ?glasses
?,哪個(gè)更快?必要時(shí)需要進(jìn)行基準(zhǔn)測(cè)試。__proto__
:let head = {
glasses: 1
};
let table = {
pen: 3,
__proto__: head
};
let bed = {
sheet: 1,
pillow: 2,
__proto__: table
};
let pockets = {
money: 2000,
__proto__: bed
};
alert( pockets.pen ); // 3
alert( bed.glasses ); // 1
alert( table.money ); // undefined
例如,對(duì)于 pockets.glasses
來(lái)說(shuō),它們(引擎)會(huì)記得在哪里找到的 glasses
(在 head
中),這樣下次就會(huì)直接在這個(gè)位置進(jìn)行搜索。并且引擎足夠聰明,一旦有內(nèi)容更改,它們就會(huì)自動(dòng)更新內(nèi)部緩存,因此,該優(yōu)化是安全的。
我們有從 animal
中繼承的 rabbit
。
如果我們調(diào)用 rabbit.eat()
,哪一個(gè)對(duì)象會(huì)接收到 full
屬性:animal
還是 rabbit
?
let animal = {
eat() {
this.full = true;
}
};
let rabbit = {
__proto__: animal
};
rabbit.eat();
答案:rabbit
。
這是因?yàn)?nbsp;this
是點(diǎn)符號(hào)前面的這個(gè)對(duì)象,因此 rabbit.eat()
修改了 rabbit
。
屬性查找和執(zhí)行是兩回事兒。
首先在原型中找到 rabbit.eat
方法,然后在 this=rabbit
的情況下執(zhí)行。
我們有兩只倉(cāng)鼠:speedy
和 lazy
都繼承自普通的 hamster
對(duì)象。
當(dāng)我們喂其中一只的時(shí)候,另一只也吃飽了。為什么?如何修復(fù)它?
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
// 這只倉(cāng)鼠找到了食物
speedy.eat("apple");
alert( speedy.stomach ); // apple
// 這只倉(cāng)鼠也找到了食物,為什么?請(qǐng)修復(fù)它。
alert( lazy.stomach ); // apple
我們仔細(xì)研究一下在調(diào)用 speedy.eat("apple")
的時(shí)候,發(fā)生了什么。
speedy.eat
? 方法在原型(?=hamster
?)中被找到,然后執(zhí)行 ?this=speedy
?(在點(diǎn)符號(hào)前面的對(duì)象)。this.stomach.push()
? 需要找到 ?stomach
? 屬性,然后對(duì)其調(diào)用 ?push
?。它在 ?this
?(?=speedy
?)中查找 ?stomach
?,但并沒有找到。hamster
? 中找到 ?stomach
?。stomach
? 調(diào)用 ?push
?,將食物添加到 ?stomach
? 的原型 中。因此,所有的倉(cāng)鼠共享了同一個(gè)胃!
對(duì)于 lazy.stomach.push(...)
和 speedy.stomach.push()
而言,屬性 stomach
被在原型中找到(不是在對(duì)象自身),然后向其中 push
了新數(shù)據(jù)。
請(qǐng)注意,在簡(jiǎn)單的賦值 this.stomach=
的情況下不會(huì)出現(xiàn)這種情況:
let hamster = {
stomach: [],
eat(food) {
// 分配給 this.stomach 而不是 this.stomach.push
this.stomach = [food];
}
};
let speedy = {
__proto__: hamster
};
let lazy = {
__proto__: hamster
};
// 倉(cāng)鼠 Speedy 找到了食物
speedy.eat("apple");
alert( speedy.stomach ); // apple
// 倉(cāng)鼠 Lazy 的胃是空的
alert( lazy.stomach ); // <nothing>
現(xiàn)在,一切都運(yùn)行正常,因?yàn)?nbsp;this.stomach=
不會(huì)執(zhí)行對(duì) stomach
的查找。該值會(huì)被直接寫入 this
對(duì)象。
此外,我們還可以通過(guò)確保每只倉(cāng)鼠都有自己的胃來(lái)完全回避這個(gè)問題:
let hamster = {
stomach: [],
eat(food) {
this.stomach.push(food);
}
};
let speedy = {
__proto__: hamster,
stomach: []
};
let lazy = {
__proto__: hamster,
stomach: []
};
// 倉(cāng)鼠 Speedy 找到了食物
speedy.eat("apple");
alert( speedy.stomach ); // apple
// 倉(cāng)鼠 Lazy 的胃是空的
alert( lazy.stomach ); // <nothing>
作為一種常見的解決方案,所有描述特定對(duì)象狀態(tài)的屬性,例如上面的 stomach
,都應(yīng)該被寫入該對(duì)象中。這樣可以避免此類問題。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: