2022 年 7 月 6 日

原生原型

"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

其他內建原型

其他內建物件,例如 ArrayDateFunction 等,也會在原型中保留方法。

例如,當我們建立一個陣列 [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

基本型別

最複雜的事情發生在字串、數字和布林值上。

正如我們所記得的,它們不是物件。但是,如果我們嘗試存取它們的屬性,就會使用內建建構函式 StringNumberBoolean 建立暫時的包裝器物件。它們提供方法並消失。

這些物件對我們來說是隱形的,而且大多數引擎會將它們最佳化,但規格確切地描述了它。這些物件的方法也存在於原型中,可用作 String.prototypeNumber.prototypeBoolean.prototype

nullundefined 沒有物件包裝器

特殊值 nullundefined 是獨立的。它們沒有物件包裝器,因此它們無法使用方法和屬性。而且也沒有對應的原型。

變更原生原型

原生原型可以修改。例如,如果我們新增一個方法到 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.prototypeObject.prototypeDate.prototype 等)
    • 物件本身只儲存資料(陣列項目、物件屬性、日期)
  • 基本型別也會將方法儲存在包裝物件的原型中:Number.prototypeString.prototypeBoolean.prototype。只有 undefinednull 沒有包裝物件
  • 內建原型可以修改或新增方法。但建議不要變更它們。唯一允許的情況可能是當我們新增一個新的標準,但 JavaScript 引擎還不支援時

任務

重要性:5

新增方法 defer(ms) 至所有函式的原型,此方法會在 ms 毫秒後執行函式。

執行後,以下程式碼應該可以正常運作

function f() {
  alert("Hello!");
}

f.defer(1000); // shows "Hello!" after 1 second
Function.prototype.defer = function(ms) {
  setTimeout(this, ms);
};

function f() {
  alert("Hello!");
}

f.defer(1000); // shows "Hello!" after 1 sec
重要性:4

新增方法 defer(ms) 至所有函式的原型,此方法會傳回一個包裝器,延遲 ms 毫秒後呼叫。

以下是如何運作的範例

function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // shows 3 after 1 second

請注意,參數應該傳遞給原始函式。

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); // shows 3 after 1 sec

請注意:我們在 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();
教學課程地圖

留言

留言前請先閱讀…
  • 如果你有建議要改進的地方,請 提交 GitHub 議題 或提交 pull request,而不是留言。
  • 如果你無法理解文章中的內容,請說明。
  • 要插入少量的程式碼,請使用 <code> 標籤,要插入多行程式碼,請用 <pre> 標籤包住,要插入超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)