Javascript 原生的原型

2023-02-17 10:52 更新

?"prototype"? 屬性在 JavaScript 自身的核心部分中被廣泛地應(yīng)用。所有的內(nèi)建構(gòu)造函數(shù)都用到了它。

首先,我們將看看原生原型的詳細(xì)信息,然后學(xué)習(xí)如何使用它為內(nèi)建對象添加新功能。

Object.prototype

假如我們輸出一個空對象:

let obj = {};
alert( obj ); // "[object Object]" ?

生成字符串 "[object Object]" 的代碼在哪里?那就是一個內(nèi)建的 toString 方法,但是它在哪里呢?obj 是空的!

……然而簡短的表達(dá)式 obj = {} 和 obj = new Object() 是一個意思,其中 Object 就是一個內(nèi)建的對象構(gòu)造函數(shù),其自身的 prototype 指向一個帶有 toString 和其他方法的一個巨大的對象。

就像這樣:


當(dāng) new Object() 被調(diào)用(或一個字面量對象 {...} 被創(chuàng)建),按照前面章節(jié)中我們學(xué)習(xí)過的規(guī)則,這個對象的 [[Prototype]] 屬性被設(shè)置為 Object.prototype


所以,之后當(dāng) obj.toString() 被調(diào)用時,這個方法是從 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

其他內(nèi)建原型

其他內(nèi)建對象,像 Array、DateFunction 及其他,都在 prototype 上掛載了方法。

例如,當(dāng)我們創(chuàng)建一個數(shù)組 [1, 2, 3],在內(nèi)部會默認(rèn)使用 new Array() 構(gòu)造器。因此 Array.prototype 變成了這個數(shù)組的 prototype,并為這個數(shù)組提供數(shù)組的操作方法。這樣內(nèi)存的存儲效率是很高的。

按照規(guī)范,所有的內(nèi)建原型頂端都是 Object.prototype。這就是為什么有人說“一切都從對象繼承而來”。

下面是完整的示意圖(3 個內(nèi)建對象):


讓我們手動驗證原型:

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

一些方法在原型上可能會發(fā)生重疊,例如,Array.prototype 有自己的 toString 方法來列舉出來數(shù)組的所有元素并用逗號分隔每一個元素。

let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- Array.prototype.toString 的結(jié)果

正如我們之前看到的那樣,Object.prototype 也有 toString 方法,但是 Array.prototype 在原型鏈上更近,所以數(shù)組對象原型上的方法會被使用。


瀏覽器內(nèi)的工具,像 Chrome 開發(fā)者控制臺也會顯示繼承性(可能需要對內(nèi)建對象使用 console.dir):


其他內(nèi)建對象也以同樣的方式運行。即使是函數(shù) —— 它們是內(nèi)建構(gòu)造器 Function 的對象,并且它們的方法(call/apply 及其他)都取自 Function.prototype。函數(shù)也有自己的 toString 方法。

function f() {}

alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true,繼承自 Object

基本數(shù)據(jù)類型

最復(fù)雜的事情發(fā)生在字符串、數(shù)字和布爾值上。

正如我們記憶中的那樣,它們并不是對象。但是如果我們試圖訪問它們的屬性,那么臨時包裝器對象將會通過內(nèi)建的構(gòu)造器 StringNumber 和 Boolean 被創(chuàng)建。它們提供給我們操作字符串、數(shù)字和布爾值的方法然后消失。

這些對象對我們來說是無形地創(chuàng)建出來的。大多數(shù)引擎都會對其進(jìn)行優(yōu)化,但是規(guī)范中描述的就是通過這種方式。這些對象的方法也駐留在它們的 prototype 中,可以通過 String.prototype、Number.prototype 和 Boolean.prototype 進(jìn)行獲取。

值 ?null? 和 ?undefined? 沒有對象包裝器

特殊值 null 和 undefined 比較特殊。它們沒有對象包裝器,所以它們沒有方法和屬性。并且它們也沒有相應(yīng)的原型。

更改原生原型

原生的原型是可以被修改的。例如,我們向 String.prototype 中添加一個方法,這個方法將對所有的字符串都是可用的:

String.prototype.show = function() {
  alert(this);
};

"BOOM!".show(); // BOOM!

在開發(fā)的過程中,我們可能會想要一些新的內(nèi)建方法,并且想把它們添加到原生原型中。但這通常是一個很不好的想法。

重要:

原型是全局的,所以很容易造成沖突。如果有兩個庫都添加了 String.prototype.show 方法,那么其中的一個方法將被另一個覆蓋。

所以,通常來說,修改原生原型被認(rèn)為是一個很不好的想法。

在現(xiàn)代編程中,只有一種情況下允許修改原生原型。那就是 polyfilling。

Polyfilling 是一個術(shù)語,表示某個方法在 JavaScript 規(guī)范中已存在,但是特定的 JavaScript 引擎尚不支持該方法,那么我們可以通過手動實現(xiàn)它,并用以填充內(nèi)建原型。

例如:

if (!String.prototype.repeat) { // 如果這兒沒有這個方法
  // 那就在 prototype 中添加它

  String.prototype.repeat = function(n) {
    // 重復(fù)傳入的字符串 n 次

    // 實際上,實現(xiàn)代碼比這個要復(fù)雜一些(完整的方法可以在規(guī)范中找到)
    // 但即使是不夠完美的 polyfill 也常常被認(rèn)為是足夠好的
    return new Array(n + 1).join(this);
  };
}

alert( "La".repeat(3) ); // LaLaLa

從原型中借用

在 裝飾器模式和轉(zhuǎn)發(fā),call/apply 一章中,我們討論了方法借用。

那是指我們從一個對象獲取一個方法,并將其復(fù)制到另一個對象。

一些原生原型的方法通常會被借用。

例如,如果我們要創(chuàng)建類數(shù)組對象,則可能需要向其中復(fù)制一些 ?Array? 方法。

例如:

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join;

alert( obj.join(',') ); // Hello,world!

上面這段代碼有效,是因為內(nèi)建的方法 join 的內(nèi)部算法只關(guān)心正確的索引和 length 屬性。它不會檢查這個對象是否是真正的數(shù)組。許多內(nèi)建方法就是這樣。

另一種方式是通過將 obj.__proto__ 設(shè)置為 Array.prototype,這樣 Array 中的所有方法都自動地可以在 obj 中使用了。

但是如果 obj 已經(jīng)從另一個對象進(jìn)行了繼承,那么這種方法就不可行了(譯注:因為這樣會覆蓋掉已有的繼承。此處 obj 其實已經(jīng)從 Object 進(jìn)行了繼承,但是 Array 也繼承自 Object,所以此處的方法借用不會影響 obj 對原有繼承的繼承,因為 obj 通過原型鏈依舊繼承了 Object)。請記住,我們一次只能繼承一個對象。

方法借用很靈活,它允許在需要時混合來自不同對象的方法。

總結(jié)

  • 所有的內(nèi)建對象都遵循相同的模式(pattern):
    • 方法都存儲在 prototype 中(?Array.prototype?、?Object.prototype?、?Date.prototype? 等)。
    • 對象本身只存儲數(shù)據(jù)(數(shù)組元素、對象屬性、日期)。
  • 原始數(shù)據(jù)類型也將方法存儲在包裝器對象的 prototype 中:?Number.prototype?、?String.prototype? 和 ?Boolean.prototype?。只有 ?undefined? 和 ?null? 沒有包裝器對象。
  • 內(nèi)建原型可以被修改或被用新的方法填充。但是不建議更改它們。唯一允許的情況可能是,當(dāng)我們添加一個還沒有被 JavaScript 引擎支持,但已經(jīng)被加入 JavaScript 規(guī)范的新標(biāo)準(zhǔn)時,才可能允許這樣做。

任務(wù)


給函數(shù)添加一個 "f.defer(ms)" 方法

重要程度: 5

在所有函數(shù)的原型中添加 ?defer(ms)? 方法,該方法將在 ?ms? 毫秒后運行該函數(shù)。

當(dāng)你完成添加后,下面的代碼應(yīng)該是可執(zhí)行的:

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!"

將裝飾器 "defer()" 添加到函數(shù)

重要程度: 4

在所有函數(shù)的原型中添加 ?defer(ms)? 方法,該方法返回一個包裝器,將函數(shù)調(diào)用延遲 ?ms? 毫秒。

下面是它應(yīng)該如何執(zhí)行的例子:

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

f.defer(1000)(1, 2); // 1 秒后顯示 3

請注意,參數(shù)應(yīng)該被傳給原始函數(shù)。


解決方案

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 以使裝飾器適用于對象方法。

因此,如果將包裝器函數(shù)作為對象方法調(diào)用,那么 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();


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號