"prototype"
屬性在 JavaScript 自身的核心部分中被廣泛地應用。所有的內建構造函數都用到了它。
首先,我們將看看原生原型的詳細信息,然後學習如何使用它爲內建對象添加新功能。
假如我們輸出壹個空對象:
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
及其他,都在 prototype 上挂載了方法。
例如,當我們創建壹個數組 [1, 2, 3]
,在內部會默認使用 new Array()
構造器。因此 Array.prototype
變成了這個數組的 prototype,並爲這個數組提供數組的操作方法。這樣內存的存儲效率是很高的。
按照規範,所有的內建原型頂端都是 Object.prototype
。這就是爲什麽有人說“壹切都從對象繼承而來”。
下面是完整的示意圖(3 個內建對象):
讓我們手動驗證原型:
let arr = [1, 2, 3]; // 它繼承自 Array.prototype? alert( arr.__proto__ === Array.prototype ); // true // 接下來繼承自 Object.prototype? alert( arr.__proto__.__proto__ === Object.prototype ); // true // 原型鏈的頂端爲 null。 alert( arr.__proto__.__proto__.__proto__ ); // null
壹些方法在原型上可能會發生重疊,例如,Array.prototype
有自己的 toString
方法來列舉出來數組的所有元素並用逗號分隔每壹個元素。
let arr = [1, 2, 3] alert(arr); // 1,2,3 <-- 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,繼承自 Object
最複雜的事情發生在字符串、數字和布爾值上。
正如我們記憶中的那樣,它們並不是對象。但是如果我們試圖訪問它們的屬性,那麽臨時包裝器對象將會通過內建的構造器 String
、Number
和 Boolean
被創建。它們提供給我們操作字符串、數字和布爾值的方法然後消失。
這些對象對我們來說是無形地創建出來的。大多數引擎都會對其進行優化,但是規範中描述的就是通過這種方式。這些對象的方法也駐留在它們的 prototype 中,可以通過 String.prototype
、Number.prototype
和 Boolean.prototype
進行獲取。
值 null
和 undefined
沒有對象包裝器
特殊值 null
和 undefined
比較特殊。它們沒有對象包裝器,所以它們沒有方法和屬性。並且它們也沒有相應的原型。
原生的原型是可以被修改的。例如,我們向 String.prototype
中添加壹個方法,這個方法將對所有的字符串都是可用的:
String.prototype.show = function() { alert(this); }; "BOOM!".show(); // BOOM!
在開發的過程中,我們可能會想要壹些新的內建方法,並且想把它們添加到原生原型中。但這通常是壹個很不好的想法。
重要:
原型是全局的,所以很容易造成沖突。如果有兩個庫都添加了 String.prototype.show
方法,那麽其中的壹個方法將被另壹個覆蓋。
所以,通常來說,修改原生原型被認爲是壹個很不好的想法。
在現代編程中,只有壹種情況下允許修改原生原型。那就是 polyfilling。
Polyfilling 是壹個術語,表示某個方法在 JavaScript 規範中已存在,但是特定的 JavaScript 引擎尚不支持該方法,那麽我們可以通過手動實現它,並用以填充內建原型。
例如:
if (!String.prototype.repeat) { // 如果這兒沒有這個方法 // 那就在 prototype 中添加它 String.prototype.repeat = function(n) { // 重複傳入的字符串 n 次 // 實際上,實現代碼比這個要複雜壹些(完整的方法可以在規範中找到) // 但即使是不夠完美的 polyfill 也常常被認爲是足夠好的 return new Array(n + 1).join(this); }; } alert( "La".repeat(3) ); // LaLaLa
在 裝飾器模式和轉發,call/apply 壹章中,我們討論了方法借用。
那是指我們從壹個對象獲取壹個方法,並將其複制到另壹個對象。
壹些原生原型的方法通常會被借用。
例如,如果我們要創建類數組對象,則可能需要向其中複制壹些 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
已經從另壹個對象進行了繼承,那麽這種方法就不可行了(譯注:因爲這樣會覆蓋掉已有的繼承。此處 obj
其實已經從 Object
進行了繼承,但是 Array
也繼承自 Object
,所以此處的方法借用不會影響 obj
對原有繼承的繼承,因爲 obj
通過原型鏈依舊繼承了 Object
)。請記住,我們壹次只能繼承壹個對象。
方法借用很靈活,它允許在需要時混合來自不同對象的方法。
所有的內建對象都遵循相同的模式(pattern):
方法都存儲在 prototype 中(Array.prototype
、Object.prototype
、Date.prototype
等)。
對象本身只存儲數據(數組元素、對象屬性、日期)。
原始數據類型也將方法存儲在包裝器對象的 prototype 中:Number.prototype
、String.prototype
和 Boolean.prototype
。只有 undefined
和 null
沒有包裝器對象。
內建原型可以被修改或被用新的方法填充。但是不建議更改它們。唯壹允許的情況可能是,當我們添加壹個還沒有被 JavaScript 引擎支持,但已經被加入 JavaScript 規範的新標准時,才可能允許這樣做。
重要程度: 5
在所有函數的原型中添加 defer(ms)
方法,該方法將在 ms
毫秒後運行該函數。
當妳完成添加後,下面的代碼應該是可執行的:
function f() { alert("Hello!"); } f.defer(1000); // 1 秒後顯示 "Hello!"
Function.prototype.defer = function(ms) { setTimeout(this, ms); }; function f() { alert("Hello!"); } f.defer(1000); // 1 秒後顯示 "Hello!"
重要程度: 4
在所有函數的原型中添加 defer(ms)
方法,該方法返回壹個包裝器,將函數調用延遲 ms
毫秒。
下面是它應該如何執行的例子:
function f(a, b) { alert( a + b ); } f.defer(1000)(1, 2); // 1 秒後顯示 3
請注意,參數應該被傳給原始函數。
Function.prototype.defer = function(ms) { let f = this; return function(...args) { setTimeout(() => f.apply(this, args), ms); } }; // check it function f(a, b) { alert( a + b ); } f.defer(1000)(1, 2); // 1 秒後顯示 3
請注意:我們在 f.apply
中使用 this
以使裝飾器適用于對象方法。
因此,如果將包裝器函數作爲對象方法調用,那麽 this
將會被傳遞給原始方法 f
。
Function.prototype.defer = function(ms) { let f = this; return function(...args) { setTimeout(() => f.apply(this, args), ms); } }; let user = { name: "John", sayHi() { alert(this.name); } } user.sayHi = user.sayHi.defer(1000); user.sayHi();