3 回答

TA貢獻(xiàn)1842條經(jīng)驗(yàn) 獲得超13個(gè)贊
我不明白為什么我們突然需要
Letter.prototype
所有子實(shí)例繼承的對(duì)象,而不是像上面的第一個(gè)圖那樣擁有它。
實(shí)際上那里什么都沒(méi)有改變。const letter
它仍然是同一個(gè)對(duì)象,具有與第一個(gè)示例中指定的對(duì)象相同的用途。letter 實(shí)例繼承自它,它存儲(chǔ)getNumber
方法,它繼承自Object.prototype
.
改變的是附加Letter
功能。
對(duì)我來(lái)說(shuō),第一個(gè)例子似乎沒(méi)有什么問(wèn)題。
是的,它是:這{number: 2, __proto__: letter}
是一種非常丑陋的創(chuàng)建實(shí)例的方式,并且在必須執(zhí)行更復(fù)雜的邏輯來(lái)初始化屬性時(shí)不起作用。
解決這個(gè)問(wèn)題的方法是
// Generic prototype for all letters.
const letterPrototype = {
getNumber() {
return this.number;
}
};
const makeLetter = (number) => {
const letter = Object.create(letterPrototype); // {__proto__: letterPrototype}
if (number < 0) throw new RangeError("letters must be numbered positive"); // or something
letter.number = number;
return letter;
}
let a = makeLetter(1);
let b = makeLetter(2);
// ...
let z = makeLetter(26);
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);
現(xiàn)在我們有兩個(gè)價(jià)值觀,makeLetter并且letterPrototype在某種程度上屬于彼此。此外,在比較各種make…函數(shù)時(shí),它們都具有相同的模式,即首先創(chuàng)建一個(gè)繼承自各自原型的新對(duì)象,然后在最后返回它。為了簡(jiǎn)化,引入了通用構(gòu)造:
// generic object instantiation
const makeNew = (prototype, ...args) => {
const obj = Object.create(prototype);
obj.constructor(...args);
return obj;
}
// prototype for all letters.
const letter = {
constructor(number) {
if (number < 0) throw new RangeError("letters must be numbered positive"); // or something
letter.number = number;
},
getNumber() {
return this.number;
}
};
let a = makeNew(letter, 1);
let b = makeNew(letter, 2);
// ...
let z = makeNew(letter, 26);
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);
你能看到我們要去哪里嗎?makeNew實(shí)際上是語(yǔ)言的一部分,即new運(yùn)算符。雖然這可行,但實(shí)際選擇的語(yǔ)法是使constructor值傳遞給new構(gòu)造函數(shù)并將原型對(duì)象存儲(chǔ)在.prototype構(gòu)造函數(shù)上。

TA貢獻(xiàn)1808條經(jīng)驗(yàn) 獲得超4個(gè)贊
原型是 JavaScript 的基礎(chǔ)。它們可用于縮短代碼并顯著減少內(nèi)存消耗。原型還可以控制繼承的屬性,動(dòng)態(tài)更改現(xiàn)有屬性,并向從構(gòu)造函數(shù)創(chuàng)建的所有實(shí)例添加新屬性,而無(wú)需一一更新每個(gè)實(shí)例。它們還可以用于隱藏迭代中的屬性,原型有助于避免大型對(duì)象中的命名沖突。
內(nèi)存消耗
我在jsFiddle做了一個(gè)超級(jí)簡(jiǎn)單的實(shí)際示例,它使用 jQuery,看起來(lái)像這樣:
HTML:<div></div>
JS:const div = $('div'); console.log(div);
如果我們現(xiàn)在查看控制臺(tái),我們可以看到 jQuery 返回了一個(gè)對(duì)象。該對(duì)象有 3 個(gè)自己的屬性,其原型有 148 個(gè)屬性。如果沒(méi)有原型,所有這 148 個(gè)屬性都應(yīng)該被指定為對(duì)象自己的屬性。對(duì)于單個(gè) jQuery 對(duì)象來(lái)說(shuō),這可能是可以承受的內(nèi)存負(fù)載,但您可能會(huì)在一個(gè)相對(duì)簡(jiǎn)單的代碼片段中創(chuàng)建數(shù)百個(gè) jQuery 對(duì)象。
但是,這 148 個(gè)屬性只是開(kāi)始,從第一個(gè)屬性打開(kāi)記錄樹(shù)0
,查詢的元素還有很多自己的屬性div
,在列表的末尾有一個(gè)原型,HTMLDivElementPrototype
。打開(kāi)它,您會(huì)發(fā)現(xiàn)幾個(gè)屬性,以及一個(gè)原型:HTMLElementPrototype
。打開(kāi)它,會(huì)顯示一長(zhǎng)串屬性,ElementPrototype
位于列表的末尾。打開(kāi),再次揭示了很多屬性,以及一個(gè)名為 的原型NodePrototype
。在樹(shù)中打開(kāi)它,然后瀏覽該原型末尾的列表,還有一個(gè)原型, ,EventTargetPrototype
最后,鏈中的最后一個(gè)原型是Object
,它也有一些屬性。
現(xiàn)在,元素的所有這些顯示屬性中的一些屬性div
本身就是對(duì)象,例如children
,它有一個(gè)自己的屬性 (?length
) 和其原型中的一些方法。幸運(yùn)的是,該集合是空的,但如果我們?cè)谠技现刑砑恿藥讉€(gè)div
元素,則上面列出的所有屬性都將可用于該集合的每個(gè)子項(xiàng)。
如果沒(méi)有原型,并且所有屬性都是對(duì)象自己的屬性,那么當(dāng)您在閱讀時(shí)達(dá)到此答案中的這一點(diǎn)時(shí),您的瀏覽器仍將在該單個(gè) jQuery 對(duì)象上工作。您可以想象當(dāng)有數(shù)百個(gè)元素收集到 jQuery 對(duì)象時(shí)的工作量。
對(duì)象迭代
那么原型如何幫助迭代呢?JavaScript 對(duì)象有一個(gè)自有屬性的概念,即有些屬性是作為自有屬性添加的,有些屬性是在__proto__
.?這個(gè)概念使得將實(shí)際數(shù)據(jù)和元數(shù)據(jù)存儲(chǔ)到同一個(gè)對(duì)象中成為可能。
雖然使用現(xiàn)代 JS 迭代自己的屬性很簡(jiǎn)單,但情況并非Object.keys
總是Object.entries
如此。在 JS 的早期,只有for..in
循環(huán)來(lái)迭代對(duì)象的屬性(很早的時(shí)候什么也沒(méi)有)。通過(guò)in
操作符,我們還可以從原型中獲取屬性,并且我們必須通過(guò)hasOwnProperty
檢查將數(shù)據(jù)與元數(shù)據(jù)分開(kāi)。如果一切都在自己的屬性中,我們就無(wú)法在數(shù)據(jù)和元數(shù)據(jù)之間進(jìn)行任何分離。
函數(shù)內(nèi)部
為什么原型才成為函數(shù)的屬性呢?嗯,有點(diǎn)不是,函數(shù)也是對(duì)象,它們只是有一個(gè)內(nèi)部 [[Callable]] 插槽,和一個(gè)非常特殊的屬性,可執(zhí)行函數(shù)體。正如任何其他 JS 類型都有一個(gè)用于自身屬性和原型屬性的“儲(chǔ)物柜”一樣,函數(shù)也具有第三個(gè)“儲(chǔ)物柜”,并且具有接收參數(shù)的特殊能力。
函數(shù)自身的屬性通常稱為靜態(tài)屬性,但它們與常規(guī)對(duì)象的屬性一樣是動(dòng)態(tài)的。函數(shù)的可執(zhí)行主體和接收參數(shù)的能力使函數(shù)成為創(chuàng)建對(duì)象的理想選擇。與 JS 中的任何其他對(duì)象創(chuàng)建方法相比,您可以將參數(shù)傳遞給“類”(=構(gòu)造函數(shù)),并進(jìn)行非常復(fù)雜的操作來(lái)獲取屬性的值。參數(shù)也封裝在函數(shù)內(nèi)部,不需要將它們存儲(chǔ)在外部作用域中。
這兩個(gè)優(yōu)點(diǎn)是任何其他對(duì)象創(chuàng)建操作所不具備的(當(dāng)然,您可以在對(duì)象字面量中使用 IIFE ex.,但這有點(diǎn)難看)。此外,構(gòu)造函數(shù)內(nèi)部聲明的變量在函數(shù)外部無(wú)法訪問(wèn),只有在函數(shù)內(nèi)部創(chuàng)建的方法才能訪問(wèn)這些變量。這樣你就可以在“類”中擁有一些“私有字段”。
函數(shù)和陰影的默認(rèn)屬性
當(dāng)我們檢查一個(gè)新創(chuàng)建的函數(shù)時(shí),我們可以看到它有一些自己的屬性(sc 靜態(tài)屬性)。這些屬性被標(biāo)記為不可枚舉,因此它們不包含在任何迭代中。屬性包括arguments <Null>
、caller <Null>
、length <Number>
和contains ,以及函數(shù)本身的name <String>
底層。prototype <Object>
constructor <Function>
prototype <Function>
等待!函數(shù)中有兩個(gè)單獨(dú)的屬性具有相同的名稱,甚至具有不同的類型?是的,底層prototype
是__proto__
函數(shù)的 ,另一個(gè)prototype
是函數(shù)自己的屬性,它遮蔽了底層prototype
。__proto__
當(dāng)為存在于中的同名屬性分配值時(shí),所有對(duì)象都有一種隱藏 的屬性的機(jī)制__proto__
。之后,對(duì)象本身無(wú)法__proto__
直接訪問(wèn)該屬性,即被隱藏。陰影機(jī)制保留了所有屬性,并且這種方式可以處理一些命名沖突。仍然可以通過(guò)原型引用隱藏的屬性來(lái)訪問(wèn)它們。
控制繼承
由于prototype
是該函數(shù)自己的屬性,它是免費(fèi)的戰(zhàn)利品,您可以用新對(duì)象替換它,或者根據(jù)需要對(duì)其進(jìn)行編輯,從而不會(huì)對(duì)底層“ ”產(chǎn)生影響,并且不會(huì)__proto__
與“不動(dòng)__proto__
”原則。
原型繼承的強(qiáng)大之處恰恰在于編輯或替換原型的能力。你可以選擇你想要繼承的內(nèi)容,也可以選擇原型鏈,通過(guò)從其他對(duì)象繼承原型對(duì)象。
創(chuàng)建實(shí)例
使用構(gòu)造函數(shù)創(chuàng)建對(duì)象的工作原理可能在 SO 帖子中已經(jīng)解釋了數(shù)千次,但我在這里再次做了一個(gè)簡(jiǎn)短的摘要。
創(chuàng)建構(gòu)造函數(shù)的實(shí)例已經(jīng)很熟悉了。當(dāng)使用運(yùn)算符調(diào)用構(gòu)造函數(shù)時(shí)new
,會(huì)創(chuàng)建一個(gè)新對(duì)象,并將其放入this
構(gòu)造函數(shù)內(nèi)使用。分配給的每個(gè)屬性都this
成為新創(chuàng)建的實(shí)例自己的屬性,并且prototype
構(gòu)造函數(shù)的屬性中的屬性被淺復(fù)制到__proto__
實(shí)例的。
這樣,所有對(duì)象屬性都保留其原始引用,并且不會(huì)創(chuàng)建實(shí)際的新對(duì)象,只是復(fù)制引用。這提供了將對(duì)象扔掉的能力,而無(wú)需每次在其他對(duì)象中需要它們時(shí)都重新創(chuàng)建它們。當(dāng)像這樣鏈接時(shí),它還可以以最小的努力同時(shí)對(duì)所有實(shí)例進(jìn)行動(dòng)態(tài)編輯。
原型構(gòu)造函數(shù)
那么構(gòu)造函數(shù)中的constructor
in是什么意思呢?prototype
該函數(shù)最初指的是構(gòu)造函數(shù)本身,它是一個(gè)循環(huán)引用。但是當(dāng)你創(chuàng)建一個(gè)新的原型時(shí),你可以重寫其中的構(gòu)造函數(shù)。構(gòu)造函數(shù)可以從另一個(gè)函數(shù)中獲取,也可以完全省略。這樣您就可以控制實(shí)例的“類型”。當(dāng)檢查一個(gè)實(shí)例是否是特定構(gòu)造函數(shù)的實(shí)例時(shí),可以使用instanceof
運(yùn)算符。該運(yùn)算符檢查原型鏈,如果從鏈中找到另一個(gè)函數(shù)的構(gòu)造函數(shù),則將其視為該實(shí)例的構(gòu)造函數(shù)。這樣,從原型鏈中找到的所有構(gòu)造函數(shù)都是實(shí)例的構(gòu)造函數(shù),并且實(shí)例的“類型”是這些構(gòu)造函數(shù)中的任何一個(gè)。
毫無(wú)疑問(wèn),所有這一切也可以通過(guò)其他設(shè)計(jì)來(lái)實(shí)現(xiàn)。但要回答“為什么”這個(gè)問(wèn)題,我們需要深入研究 JS 的歷史。Brendan Eich 和 Allen Wirfs-Brock 最近出版的一本著作為這個(gè)問(wèn)題提供了一些線索。
每個(gè)人都同意 Mocha 將是基于對(duì)象的,但沒(méi)有類,因?yàn)橹С诸悤?huì)花費(fèi)太長(zhǎng)時(shí)間,并且存在與 Java 競(jìng)爭(zhēng)的風(fēng)險(xiǎn)。出于對(duì) Self 的欽佩,Eich 選擇從使用具有單個(gè)原型鏈接的委托的動(dòng)態(tài)對(duì)象模型開(kāi)始。
引用:JavaScript 第 8 頁(yè):前 20 年,由 Brendan Eich 和 Allen Wirfs-Brock 撰寫。
通過(guò)閱讀這本書可以獲得更深入的解釋和背景。
代碼部分
在您的編輯中,代碼注釋中出現(xiàn)了一些問(wèn)題。正如您所注意到的,ES6 類語(yǔ)法隱藏了常規(guī)構(gòu)造函數(shù)。該語(yǔ)法不僅僅是構(gòu)造函數(shù)的語(yǔ)法糖,它還添加了一種更具聲明性的方式來(lái)創(chuàng)建構(gòu)造函數(shù),并且還能夠子類化一些本機(jī)對(duì)象,例如 Array。
“?JS 不能那樣工作” 正確,
method
不是類自己的屬性(= 函數(shù))。“這就是它的工作原理” 是的,在類中創(chuàng)建的方法被分配給該類的原型。
“刪除原型對(duì)象中對(duì)它的引用”不可能,因?yàn)樵鸵驯粌鼋Y(jié)。您可以通過(guò)顯示類的描述符來(lái)看到這一點(diǎn)。
其余代碼...不做評(píng)論,不推薦。

TA貢獻(xiàn)1963條經(jīng)驗(yàn) 獲得超6個(gè)贊
對(duì)我來(lái)說(shuō),第一個(gè)例子似乎沒(méi)有什么問(wèn)題。
這不是(客觀上),某些人(如道格拉斯·克羅克福德)經(jīng)常主張避免.prototype并this一起使用Object.create(類似于您的__proto__例子)。
那么為什么人們更喜歡使用類、繼承和.prototype呢?
原型就是重用
通常創(chuàng)建原型的原因是重用功能(如上所述getNumber)。為了做到這一點(diǎn),使用構(gòu)造函數(shù)很方便。
構(gòu)造函數(shù)只是創(chuàng)建對(duì)象的函數(shù)。在“舊”JavaScript 中你會(huì)這樣做:
function Foo(x) { // easy, lets me create many Xs
this.x = x;
}
// easy, shares functionality across all objects created with new Foo
Foo.prototype.printX() {
console.log(this.x);
}
// note that printX isn't saved on every object instance but only once
(new Foo(4)).printX();
ES2015 使這變得更加容易:
class Foo { // roughly sugar for the above with subtle differences.
constructor(x) { this.x = x; }
printX() { console.log(this.x); }
}
總結(jié)一下:你不必使用 .prototype 和類,人們這樣做是因?yàn)樗苡杏?。?qǐng)注意,兩個(gè)示例中的原型鏈都一樣大。
添加回答
舉報(bào)