Javascript F.prototype

2023-02-17 10:52 更新

我們還記得,可以使用諸如 ?new F()? 這樣的構造函數(shù)來創(chuàng)建一個新對象。

如果 F.prototype 是一個對象,那么 new 操作符會使用它為新對象設置 [[Prototype]]。

請注意:

JavaScript 從一開始就有了原型繼承。這是 JavaScript 編程語言的核心特性之一。

但是在過去,沒有直接對其進行訪問的方式。唯一可靠的方法是本章中會介紹的構造函數(shù)的 "prototype" 屬性。目前仍有許多腳本仍在使用它。

請注意,這里的 F.prototype 指的是 F 的一個名為 "prototype" 的常規(guī)屬性。這聽起來與“原型”這個術語很類似,但這里我們實際上指的是具有該名字的常規(guī)屬性。

下面是一個例子:

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 的字面意思是:“當創(chuàng)建了一個 new Rabbit 時,把它的 [[Prototype]] 賦值為 animal”。

這是結果示意圖:


在上圖中,"prototype" 是一個水平箭頭,表示一個常規(guī)屬性,[[Prototype]] 是垂直的,表示 rabbit 繼承自 animal。

?F.prototype? 僅用在 ?new F? 時

F.prototype 屬性僅在 new F 被調用時使用,它為新對象的 [[Prototype]] 賦值。

如果在創(chuàng)建之后,F.prototype 屬性有了變化(F.prototype = <another object>),那么通過 new F 創(chuàng)建的新對象也將隨之擁有新的對象作為 [[Prototype]],但已經(jīng)存在的對象將保持舊有的值。

默認的 F.prototype,構造器屬性

每個函數(shù)都有 "prototype" 屬性,即使我們沒有提供它。

默認的 "prototype" 是一個只有屬性 constructor 的對象,屬性 constructor 指向函數(shù)自身。

像這樣:

function Rabbit() {}

/* 默認的 prototype
Rabbit.prototype = { constructor: Rabbit };
*/


我們可以檢查一下:

function Rabbit() {}
// 默認:
// Rabbit.prototype = { constructor: Rabbit }

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

通常,如果我們什么都不做,constructor 屬性可以通過 [[Prototype]] 給所有 rabbits 使用:

function Rabbit() {}
// 默認:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // 繼承自 {constructor: Rabbit}

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


我們可以使用 constructor 屬性來創(chuàng)建一個新對象,該對象使用與現(xiàn)有對象相同的構造器。

像這樣:

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("White Rabbit");

let rabbit2 = new rabbit.constructor("Black Rabbit");

當我們有一個對象,但不知道它使用了哪個構造器(例如它來自第三方庫),并且我們需要創(chuàng)建另一個類似的對象時,用這種方法就很方便。

但是,關于 "constructor" 最重要的是……

……JavaScript 自身并不能確保正確的 "constructor" 函數(shù)值。

是的,它存在于函數(shù)的默認 "prototype" 中,但僅此而已。之后會發(fā)生什么 —— 完全取決于我們。

特別是,如果我們將整個默認 prototype 替換掉,那么其中就不會有 "constructor" 了。

例如:

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

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

因此,為了確保正確的 "constructor",我們可以選擇添加/刪除屬性到默認 "prototype",而不是將其整個覆蓋:

function Rabbit() {}

// 不要將 Rabbit.prototype 整個覆蓋
// 可以向其中添加內容
Rabbit.prototype.jumps = true
// 默認的 Rabbit.prototype.constructor 被保留了下來

或者,也可以手動重新創(chuàng)建 constructor 屬性:

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

// 這樣的 constructor 也是正確的,因為我們手動添加了它

總結

在本章中,我們簡要介紹了為通過構造函數(shù)創(chuàng)建的對象設置 [[Prototype]] 的方法。稍后我們將看到更多依賴于此的高級編程模式。

一切都很簡單,只需要記住幾條重點就可以清晰地掌握了:

  • ?F.prototype? 屬性(不要把它與 ?[[Prototype]]? 弄混了)在 ?new F? 被調用時為新對象的 ?[[Prototype]]? 賦值。
  • ?F.prototype? 的值要么是一個對象,要么就是 ?null?:其他值都不起作用。
  • ?"prototype"? 屬性僅當設置在一個構造函數(shù)上,并通過 ?new? 調用時,才具有這種特殊的影響。

在常規(guī)對象上,prototype 沒什么特別的:

let user = {
  name: "John",
  prototype: "Bla-bla" // 這里只是普通的屬性
};

默認情況下,所有函數(shù)都有 F.prototype = {constructor:F},所以我們可以通過訪問它的 "constructor" 屬性來獲取一個對象的構造器。

任務


修改 "prototype"

重要程度: 5

在下面的代碼中,我們創(chuàng)建了 new Rabbit,然后嘗試修改它的 prototype。

最初,我們有以下代碼:

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

let rabbit = new Rabbit();

alert( rabbit.eats ); // true
  1. 我們增加了一行代碼(* 處)。現(xiàn)在 alert 會顯示什么?
  2. function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype = {}; // *
    
    alert( rabbit.eats ); // ?
  3. ……如果代碼是這樣的(修改了一行)?
  4. function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype.eats = false; // *
    
    alert( rabbit.eats ); // ?
  5. 像這樣呢(修改了一行)?
  6. function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete rabbit.eats; // *
    
    alert( rabbit.eats ); // ?
  7. 最后一種變體:
  8. function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete Rabbit.prototype.eats; // *
    
    alert( rabbit.eats ); // ?

解決方案

  1. ?true?
  2. Rabbit.prototype 的賦值操作為新對象設置了 [[Prototype]],但它不影響已有的對象。

  3. ?false?
  4. 對象通過引用被賦值。來自 Rabbit.prototype 的對象并沒有被賦值,它仍然是被 Rabbit.prototype 和 rabbit 的 [[Prototype]] 引用的單個對象。

    所以當我們通過一個引用更改其內容時,它對其他引用也是可見的。

  5. ?true?
  6. 所有 delete 操作都直接應用于對象。這里的 delete rabbit.eats 試圖從 rabbit 中刪除 eats 屬性,但 rabbit 對象并沒有 eats 屬性。所以這個操作不會有任何影響。

  7. ?undefined?
  8. 屬性 eats 被從 prototype 中刪除,prototype 中就沒有這個屬性了。


使用相同的構造函數(shù)創(chuàng)建一個對象

重要程度: 5

想象一下,我們有一個由構造函數(shù)創(chuàng)建的對象 ?obj? —— 我們不知道使用的是哪個構造函數(shù),但是我們想使用它創(chuàng)建一個新對象。

我們可以這樣做嗎?

let obj2 = new obj.constructor();

請給出一個可以使這樣的代碼正常工作的 obj 的構造函數(shù)的例子。再給出會導致這樣的代碼無法正確工作的例子。


解決方案

如果我們確信 "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,并忘記可重新創(chuàng)建 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.name 是 undefined?

這是 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')。

可能這不是我們想要的。我們想創(chuàng)建 new User 而不是 new Object。這就是缺少 constructor 的結果。

(以防你好奇,new Object(...) 調用會將其參數(shù)轉換為對象。這是理論上的,在實際中沒有人會調用 new Object 并傳入一個值,通常我們也不會使用 new Object 來創(chuàng)建對象)。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號