"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…)