"prototype"
屬性廣泛用於 JavaScript 本身的核心。所有內建建構函式都使用它。
我們先來看看詳細資訊,然後再了解如何使用它為內建物件新增新功能。
Object.prototype
假設我們輸出一個空物件
let obj = {};
alert( obj ); // "[object Object]" ?
產生字串 "[object Object]"
的程式碼在哪裡?那是一個內建的 toString
方法,但它在哪裡?obj
是空的!
…但簡短記號 obj = {}
等於 obj = new Object()
,其中 Object
是內建物件建構函式,其自己的 prototype
參照一個包含 toString
和其他方法的龐大物件。
以下是發生的事情
當呼叫 new Object()
(或建立文字物件 {...}
)時,根據我們在前一章討論的規則,它的 [[Prototype]]
會設定為 Object.prototype
因此,當呼叫 obj.toString()
時,方法會從 Object.prototype
取得。
我們可以這樣檢查
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true
請注意,在 Object.prototype
上方的鏈中沒有更多 [[Prototype]]
alert(Object.prototype.__proto__); // null
其他內建原型
其他內建物件,例如 Array
、Date
、Function
等,也會在原型中保留方法。
例如,當我們建立一個陣列 [1, 2, 3]
時,內部會使用預設的 new Array()
建構函式。因此,Array.prototype
會變成它的原型並提供方法。這非常節省記憶體。
根據規範,所有內建原型在最上方都有 Object.prototype
。這就是為什麼有些人說「所有東西都繼承自物件」。
以下是整體概觀(適用於 3 個內建函式)
讓我們手動檢查原型
let arr = [1, 2, 3];
// it inherits from Array.prototype?
alert( arr.__proto__ === Array.prototype ); // true
// then from Object.prototype?
alert( arr.__proto__.__proto__ === Object.prototype ); // true
// and null on the top.
alert( arr.__proto__.__proto__.__proto__ ); // null
原型中的一些方法可能會重疊,例如 Array.prototype
有自己的 toString
,會列出以逗號分隔的元素
let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- the result of Array.prototype.toString
正如我們之前所見,Object.prototype
也有 toString
,但 Array.prototype
在鏈中較接近,因此會使用陣列變體。
瀏覽器工具(例如 Chrome 開發人員主控台)也會顯示繼承(對於內建物件可能需要使用 console.dir
)
其他內建物件也以相同方式運作。甚至函式也是如此,它們是內建 Function
建構函式的物件,而它們的方法(call
/apply
等)則取自 Function.prototype
。函式也有自己的 toString
。
function f() {}
alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects
基本型別
最複雜的事情發生在字串、數字和布林值上。
正如我們所記得的,它們不是物件。但是,如果我們嘗試存取它們的屬性,就會使用內建建構函式 String
、Number
和 Boolean
建立暫時的包裝器物件。它們提供方法並消失。
這些物件對我們來說是隱形的,而且大多數引擎會將它們最佳化,但規格確切地描述了它。這些物件的方法也存在於原型中,可用作 String.prototype
、Number.prototype
和 Boolean.prototype
。
null
和 undefined
沒有物件包裝器特殊值 null
和 undefined
是獨立的。它們沒有物件包裝器,因此它們無法使用方法和屬性。而且也沒有對應的原型。
變更原生原型
原生原型可以修改。例如,如果我們新增一個方法到 String.prototype
,它就會對所有字串可用
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
在開發過程中,我們可能會想到一些我們想要的新內建方法,而且我們可能會想將它們新增到原生原型。但這通常是個壞主意。
原型是全域性的,因此很容易發生衝突。如果兩個函式庫都新增一個方法 String.prototype.show
,那麼其中一個會覆寫另一個的方法。
所以,通常來說,修改原生原型被認為是個壞主意。
在現代程式設計中,只有一個情況可以修改原生原型。那就是多重填補。
多重填補是一個術語,用於替換存在於 JavaScript 規格中,但尚未受到特定 JavaScript 引擎支援的方法。
然後我們可以手動實作它,並用它填充內建原型。
例如
if (!String.prototype.repeat) { // if there's no such method
// add it to the prototype
String.prototype.repeat = function(n) {
// repeat the string n times
// actually, the code should be a little bit more complex than that
// (the full algorithm is in the specification)
// but even an imperfect polyfill is often considered good enough
return new Array(n + 1).join(this);
};
}
alert( "La".repeat(3) ); // LaLaLa
從原型借用
在章節 裝飾器和轉送、呼叫/套用 中,我們討論了方法借用。
那是當我們從一個物件中取得一個方法,並將它複製到另一個物件中時。
原生原型的某些方法通常會被借用。
例如,如果我們正在製作一個類陣列的物件,我們可能想要複製一些 Array
方法到它。
例如
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Hello,world!
它之所以有效,是因為內建 join
方法的內部演算法只關心正確的索引和 length
屬性。它不會檢查物件是否真的是一個陣列。許多內建方法都像這樣。
另一個可能性是透過設定 obj.__proto__
為 Array.prototype
來繼承,這樣所有的 Array
方法都會自動在 obj
中可用。
但如果 obj
已經從另一個物件繼承,那就做不到了。請記住,我們一次只能從一個物件繼承。
借用方法很靈活,它允許在需要時混合來自不同物件的功能。
摘要
- 所有內建物件都遵循相同的模式
- 這些方法儲存在原型中(
Array.prototype
、Object.prototype
、Date.prototype
等) - 物件本身只儲存資料(陣列項目、物件屬性、日期)
- 這些方法儲存在原型中(
- 基本型別也會將方法儲存在包裝物件的原型中:
Number.prototype
、String.prototype
和Boolean.prototype
。只有undefined
和null
沒有包裝物件 - 內建原型可以修改或新增方法。但建議不要變更它們。唯一允許的情況可能是當我們新增一個新的標準,但 JavaScript 引擎還不支援時
留言
<code>
標籤,要插入多行程式碼,請用<pre>
標籤包住,要插入超過 10 行的程式碼,請使用沙盒 (plnkr、jsbin、codepen…)