我們還記得,可以使用諸如 new F()
這樣的構造函數來創建壹個新對象。
如果 F.prototype
是壹個對象,那麽 new
操作符會使用它爲新對象設置 [[Prototype]]
。
請注意:
JavaScript 從壹開始就有了原型繼承。這是 JavaScript 編程語言的核心特性之壹。
但是在過去,沒有直接對其進行訪問的方式。唯壹可靠的方法是本章中會介紹的構造函數的 "prototype"
屬性。目前仍有許多腳本仍在使用它。
請注意,這裏的 F.prototype
指的是 F
的壹個名爲 "prototype"
的常規屬性。這聽起來與“原型”這個術語很類似,但這裏我們實際上指的是具有該名字的常規屬性。
下面是壹個例子:
let animal = { eats: true }; function Rabbit(name) { this.name = name; } Rabbit.prototype = animal; let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal alert( rabbit.eats ); // true
設置 Rabbit.prototype = animal
的字面意思是:“當創建了壹個 new Rabbit
時,把它的 [[Prototype]]
賦值爲 animal
”。
這是結果示意圖:
在上圖中,"prototype"
是壹個水平箭頭,表示壹個常規屬性,[[Prototype]]
是垂直的,表示 rabbit
繼承自 animal
。
F.prototype
僅用在 new F
時
F.prototype
屬性僅在 new F
被調用時使用,它爲新對象的 [[Prototype]]
賦值。
如果在創建之後,F.prototype
屬性有了變化(F.prototype = <another object>
),那麽通過 new F
創建的新對象也將隨之擁有新的對象作爲 [[Prototype]]
,但已經存在的對象將保持舊有的值。
每個函數都有 "prototype"
屬性,即使我們沒有提供它。
默認的 "prototype"
是壹個只有屬性 constructor
的對象,屬性 constructor
指向函數自身。
像這樣:
function Rabbit() {} /* 默認的 prototype Rabbit.prototype = { constructor: Rabbit }; */
我們可以檢查壹下:
function Rabbit() {} // 默認: // Rabbit.prototype = { constructor: Rabbit } alert( Rabbit.prototype.constructor == Rabbit ); // true
通常,如果我們什麽都不做,constructor
屬性可以通過 [[Prototype]]
給所有 rabbits 使用:
function Rabbit() {} // 默認: // Rabbit.prototype = { constructor: Rabbit } let rabbit = new Rabbit(); // 繼承自 {constructor: Rabbit} alert(rabbit.constructor == Rabbit); // true (from prototype)
我們可以使用 constructor
屬性來創建壹個新對象,該對象使用與現有對象相同的構造器。
像這樣:
function Rabbit(name) { this.name = name; alert(name); } let rabbit = new Rabbit("White Rabbit"); let rabbit2 = new rabbit.constructor("Black Rabbit");
當我們有壹個對象,但不知道它使用了哪個構造器(例如它來自第三方庫),並且我們需要創建另壹個類似的對象時,用這種方法就很方便。
但是,關于 "constructor"
最重要的是……
……JavaScript 自身並不能確保正確的 "constructor"
函數值。
是的,它存在于函數的默認 "prototype"
中,但僅此而已。之後會發生什麽 —— 完全取決于我們。
特別是,如果我們將整個默認 prototype 替換掉,那麽其中就不會有 "constructor"
了。
例如:
function Rabbit() {} Rabbit.prototype = { jumps: true }; let rabbit = new Rabbit(); alert(rabbit.constructor === Rabbit); // false
因此,爲了確保正確的 "constructor"
,我們可以選擇添加/刪除屬性到默認 "prototype"
,而不是將其整個覆蓋:
function Rabbit() {} // 不要將 Rabbit.prototype 整個覆蓋 // 可以向其中添加內容 Rabbit.prototype.jumps = true // 默認的 Rabbit.prototype.constructor 被保留了下來
或者,也可以手動重新創建 constructor
屬性:
Rabbit.prototype = { jumps: true, constructor: Rabbit }; // 這樣的 constructor 也是正確的,因爲我們手動添加了它
在本章中,我們簡要介紹了爲通過構造函數創建的對象設置 [[Prototype]]
的方法。稍後我們將看到更多依賴于此的高級編程模式。
壹切都很簡單,只需要記住幾條重點就可以清晰地掌握了:
F.prototype
屬性(不要把它與 [[Prototype]]
弄混了)在 new F
被調用時爲新對象的 [[Prototype]]
賦值。
F.prototype
的值要麽是壹個對象,要麽就是 null
:其他值都不起作用。
"prototype"
屬性僅當設置在壹個構造函數上,並通過 new
調用時,才具有這種特殊的影響。
在常規對象上,prototype
沒什麽特別的:
let user = { name: "John", prototype: "Bla-bla" // 這裏只是普通的屬性 };
默認情況下,所有函數都有 F.prototype = {constructor:F}
,所以我們可以通過訪問它的 "constructor"
屬性來獲取壹個對象的構造器。
重要程度: 5
在下面的代碼中,我們創建了 new Rabbit
,然後嘗試修改它的 prototype。
最初,我們有以下代碼:
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); alert( rabbit.eats ); // true
我們增加了壹行代碼(已高亮)。現在 alert
會顯示什麽?
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); Rabbit.prototype = {}; alert( rabbit.eats ); // ?
……如果代碼是這樣的(修改了壹行)?
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); Rabbit.prototype.eats = false; alert( rabbit.eats ); // ?
像這樣呢(修改了壹行)?
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); delete rabbit.eats; alert( rabbit.eats ); // ?
最後壹種變體:
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); delete Rabbit.prototype.eats; alert( rabbit.eats ); // ?
答案:
true
。
Rabbit.prototype
的賦值操作爲新對象設置了 [[Prototype]]
,但它不影響已有的對象。
false
。
對象通過引用被賦值。來自 Rabbit.prototype
的對象並沒有被賦值,它仍然是被 Rabbit.prototype
和 rabbit
的 [[Prototype]]
引用的單個對象。
所以當我們通過壹個引用更改其內容時,它對其他引用也是可見的。
true
。
所有 delete
操作都直接應用于對象。這裏的 delete rabbit.eats
試圖從 rabbit
中刪除 eats
屬性,但 rabbit
對象並沒有 eats
屬性。所以這個操作不會有任何影響。
undefined
。
屬性 eats
被從 prototype 中刪除,prototype 中就沒有這個屬性了。
重要程度: 5
想象壹下,我們有壹個由構造函數創建的對象 obj
—— 我們不知道使用的是哪個構造函數,但是我們想使用它創建壹個新對象。
我們可以這樣做嗎?
let obj2 = new obj.constructor();
請給出壹個可以使這樣的代碼正常工作的 obj
的構造函數的例子。再給出會導致這樣的代碼無法正確工作的例子。
如果我們確信 "constructor"
屬性具有正確的值,那麽就可以使用這種方法。
例如,如果我們不觸碰默認的 "prototype"
,那麽這段代碼肯定可以正常運行:
function User(name) { this.name = name; } let user = new User('John'); let user2 = new user.constructor('Pete'); alert( user2.name ); // Pete (worked!)
它起作用了,因爲 User.prototype.constructor == User
。
……但是如果有人,重寫了 User.prototype
,並忘記可重新創建 constructor
以引用 User
,那麽上面這段代碼就會運行失敗。
例如:
function User(name) { this.name = name; } User.prototype = {}; // (*) let user = new User('John'); let user2 = new user.constructor('Pete'); alert( user2.name ); // undefined
爲什麽 user2.name
是 undefined
?
這是 new user.constructor('Pete')
的工作流程:
首先,它在 user
中尋找 constructor
。沒找到。
然後它追溯原型鏈。user
的原型是 User.prototype
,它也沒有 constructor
(因爲我們“忘記”在右側設定它了)。
再向上追溯,User.prototype
是壹個普通對象 {}
,其原型是 Object.prototype
。
最終,對于內建的 Object.prototype
,有壹個內建的 Object.prototype.constructor == Object
。所以就用它了。
所以,最終我們得到了 let user2 = new Object('Pete')
。
可能這不是我們想要的。我們想創建 new User
而不是 new Object
。這就是缺少 constructor
的結果。
(以防妳好奇,new Object(...)
調用會將其參數轉換爲對象。這是理論上的,在實際中沒有人會調用 new Object
並傳入壹個值,通常我們也不會使用 new Object
來創建對象)。