2021 年 12 月 12 日

F.prototype

請記住,可以使用建構函式建立新物件,例如 new F()

如果 F.prototype 是物件,則 new 營運子會使用它為新物件設定 [[Prototype]]

請注意

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]] 是垂直箭頭,表示 rabbitanimal 繼承。

F.prototype 僅在 new F 時使用

F.prototype 屬性僅在呼叫 new F 時使用,它會指定新物件的 [[Prototype]]

如果在建立後,F.prototype 屬性變更(F.prototype = <另一個物件>),則由 new F 建立的新物件將具有另一個物件作為 [[Prototype]],但現有物件會保留舊物件。

預設的 F.prototype、建構函式屬性

每個函式都有 "prototype" 屬性,即使我們沒有提供它。

預設的 "prototype" 是僅有一個屬性的物件,該屬性為 constructor,指向函式本身。

如下所示

function Rabbit() {}

/* default prototype
Rabbit.prototype = { constructor: Rabbit };
*/

我們可以檢查它

function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

alert( Rabbit.prototype.constructor == Rabbit ); // true

當然,如果我們什麼都不做,則所有兔子都可以透過 [[Prototype]] 使用 constructor 屬性

function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // inherits from {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" 中,但僅此而已。之後發生什麼事 - 完全取決於我們。

特別是,如果我們將預設原型整體替換,則其中將沒有 "constructor"

例如

function Rabbit() {}
Rabbit.prototype = {
  jumps: true
};

let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false

因此,為了保留正確的 "constructor",我們可以選擇將屬性新增/移除到預設 "prototype",而不是將其整體覆寫

function Rabbit() {}

// Not overwrite Rabbit.prototype totally
// just add to it
Rabbit.prototype.jumps = true
// the default Rabbit.prototype.constructor is preserved

或者,手動重新建立 constructor 屬性

Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};

// now constructor is also correct, because we added it

摘要

在本章中,我們簡要地描述了為通過建構函式建立的物件設定 [[Prototype]] 的方法。稍後我們將看到更多依賴它的進階程式設計模式。

一切都相當簡單,只需幾個註解就能讓事情清楚

  • F.prototype 屬性(不要將它誤認為 [[Prototype]])在呼叫 new F() 時設定新物件的 [[Prototype]]
  • F.prototype 的值應該是物件或 null:其他值將無法運作。
  • "prototype" 屬性只有在設定在建構函式上,並使用 new 呼叫時才有這種特殊效果。

在一般物件上,prototype 沒有什麼特別之處

let user = {
  name: "John",
  prototype: "Bla-bla" // no magic at all
};

預設所有函式都有 F.prototype = { constructor: F },因此我們可以透過存取物件的 "constructor" 屬性來取得物件的建構函式。

任務

重要性:5

在以下程式碼中,我們建立 new Rabbit,然後嘗試修改它的原型。

一開始,我們有這段程式碼

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

alert( rabbit.eats ); // true
  1. 我們新增一個字串(加重顯示)。現在 alert 會顯示什麼?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype = {};
    
    alert( rabbit.eats ); // ?
  2. …如果程式碼像這樣(取代一行)呢?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype.eats = false;
    
    alert( rabbit.eats ); // ?
  3. 像這樣(取代一行)呢?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete rabbit.eats;
    
    alert( rabbit.eats ); // ?
  4. 最後一個變體

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete Rabbit.prototype.eats;
    
    alert( rabbit.eats ); // ?

答案

  1. true.

    指定給 Rabbit.prototype 會為新物件設定 [[Prototype]],但它不會影響現有的物件。

  2. false.

    物件是透過參考指定的。Rabbit.prototype 的物件不會被複製,它仍然是一個單一物件,由 Rabbit.prototyperabbit[[Prototype]] 參考。

    因此,當我們透過一個參考變更其內容時,它會透過另一個參考可見。

  3. true.

    所有 delete 作業都會直接套用在物件上。這裡的 delete rabbit.eats 嘗試從 rabbit 中移除 eats 屬性,但它沒有這個屬性。因此,作業不會有任何效果。

  4. undefined.

    eats 屬性已從原型中刪除,它不再存在。

重要性: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.nameundefined

以下是 new user.constructor('Pete') 的運作方式

  1. 首先,它在 user 中尋找 constructor。沒有。
  2. 然後它遵循原型鏈。user 的原型是 User.prototype,它也沒有 constructor(因為我們「忘記」正確設定它了!)。
  3. 在鏈中繼續往上,User.prototype 是個純粹的物件,它的原型是內建的 Object.prototype
  4. 最後,對於內建的 Object.prototype,有一個內建的 Object.prototype.constructor == Object。因此它被使用了。

最後,在最後,我們有 let user2 = new Object('Pete')

這可能不是我們想要的。我們想建立 new User,而不是 new Object。這是遺失 constructor 的結果。

(如果你好奇,new Object(...) 呼叫會將其引數轉換為物件。這是一個理論上的東西,在實務上,沒有人會使用值呼叫 new Object,而且我們通常根本不會使用 new Object 來建立物件)。

教學地圖

留言

在留言前先閱讀這段…
  • 如果你有改善建議,請 提交 GitHub 議題 或發起拉取請求,而不是留言。
  • 如果你無法理解文章中的某件事,請詳細說明。
  • 若要插入少數幾個字元的程式碼,請使用 <code> 標籤,若要插入多行程式碼,請將它們包覆在 <pre> 標籤中,若要插入 10 行以上的程式碼,請使用沙盒 (plnkrjsbincodepen…)