3 回答

TA貢獻1842條經驗 獲得超13個贊
我不明白為什么我們突然需要
Letter.prototype
所有子實例繼承的對象,而不是像上面的第一個圖那樣擁有它。
實際上那里什么都沒有改變。const letter
它仍然是同一個對象,具有與第一個示例中指定的對象相同的用途。letter 實例繼承自它,它存儲getNumber
方法,它繼承自Object.prototype
.
改變的是附加Letter
功能。
對我來說,第一個例子似乎沒有什么問題。
是的,它是:這{number: 2, __proto__: letter}
是一種非常丑陋的創建實例的方式,并且在必須執行更復雜的邏輯來初始化屬性時不起作用。
解決這個問題的方法是
// 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
);
現在我們有兩個價值觀,makeLetter并且letterPrototype在某種程度上屬于彼此。此外,在比較各種make…函數時,它們都具有相同的模式,即首先創建一個繼承自各自原型的新對象,然后在最后返回它。為了簡化,引入了通用構造:
// 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實際上是語言的一部分,即new運算符。雖然這可行,但實際選擇的語法是使constructor值傳遞給new構造函數并將原型對象存儲在.prototype構造函數上。

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

TA貢獻1963條經驗 獲得超6個贊
對我來說,第一個例子似乎沒有什么問題。
這不是(客觀上),某些人(如道格拉斯·克羅克福德)經常主張避免.prototype并this一起使用Object.create(類似于您的__proto__例子)。
那么為什么人們更喜歡使用類、繼承和.prototype呢?
原型就是重用
通常創建原型的原因是重用功能(如上所述getNumber)。為了做到這一點,使用構造函數很方便。
構造函數只是創建對象的函數。在“舊”JavaScript 中你會這樣做:
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); }
}
總結一下:你不必使用 .prototype 和類,人們這樣做是因為它很有用。請注意,兩個示例中的原型鏈都一樣大。
添加回答
舉報