Javascript Proxy 和 Reflect

2023-02-17 10:53 更新

一個(gè) ?Proxy? 對(duì)象包裝另一個(gè)對(duì)象并攔截諸如讀取/寫入屬性和其他操作,可以選擇自行處理它們,或者透明地允許該對(duì)象處理它們。

Proxy 被用于了許多庫(kù)和某些瀏覽器框架。在本文中,我們將看到許多實(shí)際應(yīng)用。

Proxy

語(yǔ)法:

let proxy = new Proxy(target, handler)
  1. ?target? —— 是要包裝的對(duì)象,可以是任何東西,包括函數(shù)。
  2. ?handler? —— 代理配置:帶有“捕捉器”(“traps”,即攔截操作的方法)的對(duì)象。比如 ?get? 捕捉器用于讀取 ?target? 的屬性,?set? 捕捉器用于寫入 ?target? 的屬性,等等。

對(duì) proxy 進(jìn)行操作,如果在 handler 中存在相應(yīng)的捕捉器,則它將運(yùn)行,并且 Proxy 有機(jī)會(huì)對(duì)其進(jìn)行處理,否則將直接對(duì) target 進(jìn)行處理。

首先,讓我們創(chuàng)建一個(gè)沒(méi)有任何捕捉器的代理(Proxy):

let target = {};
let proxy = new Proxy(target, {}); // 空的 handler 對(duì)象

proxy.test = 5; // 寫入 proxy 對(duì)象 (1)
alert(target.test); // 5,test 屬性出現(xiàn)在了 target 中!

alert(proxy.test); // 5,我們也可以從 proxy 對(duì)象讀取它 (2)

for(let key in proxy) alert(key); // test,迭代也正常工作 (3)

由于沒(méi)有捕捉器,所有對(duì) proxy 的操作都直接轉(zhuǎn)發(fā)給了 target。

  1. 寫入操作 ?proxy.test=? 會(huì)將值寫入 ?target?。
  2. 讀取操作 ?proxy.test? 會(huì)從 ?target? 返回對(duì)應(yīng)的值。
  3. 迭代 ?proxy? 會(huì)從 ?target? 返回對(duì)應(yīng)的值。

我們可以看到,沒(méi)有任何捕捉器,proxy 是一個(gè) target 的透明包裝器(wrapper)。


Proxy 是一種特殊的“奇異對(duì)象(exotic object)”。它沒(méi)有自己的屬性。如果 handler 為空,則透明地將操作轉(zhuǎn)發(fā)給 target。

要激活更多功能,讓我們添加捕捉器。

我們可以用它們攔截什么?

對(duì)于對(duì)象的大多數(shù)操作,JavaScript 規(guī)范中有一個(gè)所謂的“內(nèi)部方法”,它描述了最底層的工作方式。例如 [[Get]],用于讀取屬性的內(nèi)部方法,[[Set]],用于寫入屬性的內(nèi)部方法,等等。這些方法僅在規(guī)范中使用,我們不能直接通過(guò)方法名調(diào)用它們。

Proxy 捕捉器會(huì)攔截這些方法的調(diào)用。它們?cè)?nbsp;proxy 規(guī)范 和下表中被列出。

對(duì)于每個(gè)內(nèi)部方法,此表中都有一個(gè)捕捉器:可用于添加到 new Proxy 的 handler 參數(shù)中以攔截操作的方法名稱:

內(nèi)部方法 Handler 方法 何時(shí)觸發(fā)
[[Get]] get 讀取屬性
[[Set]] set 寫入屬性
[[HasProperty]] has in 操作符
[[Delete]] deleteProperty delete 操作符
[[Call]] apply 函數(shù)調(diào)用
[[Construct]] construct new 操作符
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[DefineOwnProperty]] defineProperty Object.definePropertyObject.defineProperties
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptorfor..inObject.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNamesObject.getOwnPropertySymbolsfor..inObject.keys/values/entries

不變量(Invariant)

JavaScript 強(qiáng)制執(zhí)行某些不變量 —— 內(nèi)部方法和捕捉器必須滿足的條件。

其中大多數(shù)用于返回值:

  • ?[[Set]]? 如果值已成功寫入,則必須返回 ?true?,否則返回 ?false?。
  • ?[[Delete]]? 如果已成功刪除該值,則必須返回 ?true?,否則返回 ?false?。
  • ……依此類推,我們將在下面的示例中看到更多內(nèi)容。

還有其他一些不變量,例如:

  • 應(yīng)用于代理(proxy)對(duì)象的 ?[[GetPrototypeOf]]?,必須返回與應(yīng)用于被代理對(duì)象的 ?[[GetPrototypeOf]]? 相同的值。換句話說(shuō),讀取代理對(duì)象的原型必須始終返回被代理對(duì)象的原型。

捕捉器可以攔截這些操作,但是必須遵循上面這些規(guī)則。

不變量確保語(yǔ)言功能的正確和一致的行為。完整的不變量列表在 規(guī)范 中。如果你不做奇怪的事情,你可能就不會(huì)違反它們。

讓我們來(lái)看看它們是如何在實(shí)際示例中工作的。

帶有 “get” 捕捉器的默認(rèn)值

最常見(jiàn)的捕捉器是用于讀取/寫入的屬性。

要攔截讀取操作,handler 應(yīng)該有 get(target, property, receiver) 方法。

讀取屬性時(shí)觸發(fā)該方法,參數(shù)如下:

  • ?target? —— 是目標(biāo)對(duì)象,該對(duì)象被作為第一個(gè)參數(shù)傳遞給 ?new Proxy?,
  • ?property? —— 目標(biāo)屬性名,
  • ?receiver? —— 如果目標(biāo)屬性是一個(gè) getter 訪問(wèn)器屬性,則 ?receiver? 就是本次讀取屬性所在的 ?this? 對(duì)象。通常,這就是 ?proxy? 對(duì)象本身(或者,如果我們從 proxy 繼承,則是從該 proxy 繼承的對(duì)象)。現(xiàn)在我們不需要此參數(shù),因此稍后我們將對(duì)其進(jìn)行詳細(xì)介紹。

讓我們用 get 來(lái)實(shí)現(xiàn)一個(gè)對(duì)象的默認(rèn)值。

我們將創(chuàng)建一個(gè)對(duì)不存在的數(shù)組項(xiàng)返回 0 的數(shù)組。

通常,當(dāng)人們嘗試獲取不存在的數(shù)組項(xiàng)時(shí),他們會(huì)得到 undefined,但是我們?cè)谶@將常規(guī)數(shù)組包裝到代理(proxy)中,以捕獲讀取操作,并在沒(méi)有要讀取的屬性的時(shí)返回 0

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // 默認(rèn)值
    }
  }
});

alert( numbers[1] ); // 1
alert( numbers[123] ); // 0(沒(méi)有這個(gè)數(shù)組項(xiàng))

正如我們所看到的,使用 get 捕捉器很容易實(shí)現(xiàn)。

我們可以用 Proxy 來(lái)實(shí)現(xiàn)“默認(rèn)”值的任何邏輯。

想象一下,我們有一本詞典,上面有短語(yǔ)及其翻譯:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined

現(xiàn)在,如果沒(méi)有我們要讀取的短語(yǔ),那么從 dictionary 讀取它將返回 undefined。但實(shí)際上,返回一個(gè)未翻譯的短語(yǔ)通常比 undefined 要好。因此,讓我們?cè)谶@種情況下返回一個(gè)未翻譯的短語(yǔ)來(lái)替代 undefined。

為此,我們將把 dictionary 包裝進(jìn)一個(gè)攔截讀取操作的代理:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

dictionary = new Proxy(dictionary, {
  get(target, phrase) { // 攔截讀取屬性操作
    if (phrase in target) { //如果詞典中有該短語(yǔ)
      return target[phrase]; // 返回其翻譯
    } else {
      // 否則返回未翻譯的短語(yǔ)
      return phrase;
    }
  }
});

// 在詞典中查找任意短語(yǔ)!
// 最壞的情況也只是它們沒(méi)有被翻譯。
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy(沒(méi)有被翻譯)

請(qǐng)注意:

請(qǐng)注意代理如何覆蓋變量:

dictionary = new Proxy(dictionary, ...);

代理應(yīng)該在所有地方都完全替代目標(biāo)對(duì)象。目標(biāo)對(duì)象被代理后,任何人都不應(yīng)該再引用目標(biāo)對(duì)象。否則很容易搞砸。

使用 “set” 捕捉器進(jìn)行驗(yàn)證

假設(shè)我們想要一個(gè)專門用于數(shù)字的數(shù)組。如果添加了其他類型的值,則應(yīng)該拋出一個(gè)錯(cuò)誤。

當(dāng)寫入屬性時(shí) set 捕捉器被觸發(fā)。

set(target, property, value, receiver)

  • ?target? —— 是目標(biāo)對(duì)象,該對(duì)象被作為第一個(gè)參數(shù)傳遞給 ?new Proxy?,
  • ?property? —— 目標(biāo)屬性名稱,
  • ?value? —— 目標(biāo)屬性的值,
  • ?receiver? —— 與 ?get? 捕捉器類似,僅與 setter 訪問(wèn)器屬性相關(guān)。

如果寫入操作(setting)成功,set 捕捉器應(yīng)該返回 true,否則返回 false(觸發(fā) TypeError)。

讓我們用它來(lái)驗(yàn)證新值:

let numbers = [];

numbers = new Proxy(numbers, { // (*)
  set(target, prop, val) { // 攔截寫入屬性操作
    if (typeof val == 'number') {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  }
});

numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2

numbers.push("test"); // TypeError(proxy 的 'set' 返回 false)

alert("This line is never reached (error in the line above)");

請(qǐng)注意:數(shù)組的內(nèi)建方法依然有效!值被使用 push 方法添加到數(shù)組。當(dāng)值被添加到數(shù)組后,數(shù)組的 length 屬性會(huì)自動(dòng)增加。我們的代理對(duì)象 proxy 不會(huì)破壞任何東西。

我們不必重寫諸如 push 和 unshift 等添加元素的數(shù)組方法,就可以在其中添加檢查,因?yàn)樵趦?nèi)部它們使用代理所攔截的 [[Set]] 操作。

因此,代碼簡(jiǎn)潔明了。

別忘了返回 ?true?

如上所述,要保持不變量。

對(duì)于 set 操作,它必須在成功寫入時(shí)返回 true。

如果我們忘記這樣做,或返回任何假(falsy)值,則該操作將觸發(fā) TypeError。

使用 “ownKeys” 和 “getOwnPropertyDescriptor” 進(jìn)行迭代

Object.keys,for..in 循環(huán)和大多數(shù)其他遍歷對(duì)象屬性的方法都使用內(nèi)部方法 [[OwnPropertyKeys]](由 ownKeys 捕捉器攔截) 來(lái)獲取屬性列表。

這些方法在細(xì)節(jié)上有所不同:

  • ?Object.getOwnPropertyNames(obj)? 返回非 symbol 鍵。
  • ?Object.getOwnPropertySymbols(obj)? 返回 symbol 鍵。
  • ?Object.keys/values()? 返回帶有 ?enumerable? 標(biāo)志的非 symbol 鍵/值(屬性標(biāo)志在 屬性標(biāo)志和屬性描述符 一章有詳細(xì)講解)。
  • ?for..in? 循環(huán)遍歷所有帶有 ?enumerable? 標(biāo)志的非 symbol 鍵,以及原型對(duì)象的鍵。

……但是所有這些都從該列表開(kāi)始。

在下面這個(gè)示例中,我們使用 ownKeys 捕捉器攔截 for..in 對(duì) user 的遍歷,并使用 Object.keys 和 Object.values 來(lái)跳過(guò)以下劃線 _ 開(kāi)頭的屬性:

let user = {
  name: "John",
  age: 30,
  _password: "***"
};

user = new Proxy(user, {
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "ownKeys" 過(guò)濾掉了 _password
for(let key in user) alert(key); // name,然后是 age

// 對(duì)這些方法的效果相同:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30

到目前為止,它仍然有效。

盡管如此,但如果我們返回對(duì)象中不存在的鍵,Object.keys 并不會(huì)列出這些鍵:

let user = { };

user = new Proxy(user, {
  ownKeys(target) {
    return ['a', 'b', 'c'];
  }
});

alert( Object.keys(user) ); // <empty>

為什么?原因很簡(jiǎn)單:Object.keys 僅返回帶有 enumerable 標(biāo)志的屬性。為了檢查它,該方法會(huì)對(duì)每個(gè)屬性調(diào)用內(nèi)部方法 [[GetOwnProperty]] 來(lái)獲取 它的描述符(descriptor)。在這里,由于沒(méi)有屬性,其描述符為空,沒(méi)有 enumerable 標(biāo)志,因此它被略過(guò)。

為了讓 Object.keys 返回一個(gè)屬性,我們需要它要么存在于帶有 enumerable 標(biāo)志的對(duì)象,要么我們可以攔截對(duì) [[GetOwnProperty]] 的調(diào)用(捕捉器 getOwnPropertyDescriptor 可以做到這一點(diǎn)),并返回帶有 enumerable: true 的描述符。

這是關(guān)于此的一個(gè)例子:

let user = { };

user = new Proxy(user, {
  ownKeys(target) { // 一旦要獲取屬性列表就會(huì)被調(diào)用
    return ['a', 'b', 'c'];
  },

  getOwnPropertyDescriptor(target, prop) { // 被每個(gè)屬性調(diào)用
    return {
      enumerable: true,
      configurable: true
      /* ...其他標(biāo)志,可能是 "value:..." */
    };
  }

});

alert( Object.keys(user) ); // a, b, c

讓我們?cè)俅巫⒁猓喝绻搶傩栽趯?duì)象中不存在,那么我們只需要攔截 [[GetOwnProperty]]。

具有 “deleteProperty” 和其他捕捉器的受保護(hù)屬性

有一個(gè)普遍的約定,即以下劃線 ?_? 開(kāi)頭的屬性和方法是內(nèi)部的。不應(yīng)從對(duì)象外部訪問(wèn)它們。

從技術(shù)上講,我們也是能訪問(wèn)到這樣的屬性的:

let user = {
  name: "John",
  _password: "secret"
};

alert(user._password); // secret

讓我們使用代理來(lái)防止對(duì)以 _ 開(kāi)頭的屬性的任何訪問(wèn)。

我們將需要以下捕捉器:

  • ?get? 讀取此類屬性時(shí)拋出錯(cuò)誤,
  • ?set? 寫入屬性時(shí)拋出錯(cuò)誤,
  • ?deleteProperty? 刪除屬性時(shí)拋出錯(cuò)誤,
  • ?ownKeys? 在使用 ?for..in? 和像 ?Object.keys? 這樣的的方法時(shí)排除以 ?_? 開(kāi)頭的屬性。

代碼如下:

let user = {
  name: "John",
  _password: "***"
};

user = new Proxy(user, {
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    }
    let value = target[prop];
    return (typeof value === 'function') ? value.bind(target) : value; // (*)
  },
  set(target, prop, val) { // 攔截屬性寫入
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      target[prop] = val;
      return true;
    }
  },
  deleteProperty(target, prop) { // 攔截屬性刪除
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      delete target[prop];
      return true;
    }
  },
  ownKeys(target) { // 攔截讀取屬性列表
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "get" 不允許讀取 _password
try {
  alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }

// "set" 不允許寫入 _password
try {
  user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }

// "deleteProperty" 不允許刪除 _password
try {
  delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }

// "ownKeys" 將 _password 過(guò)濾出去
for(let key in user) alert(key); // name

請(qǐng)注意在 (*) 行中 get 捕捉器的重要細(xì)節(jié):

get(target, prop) {
  // ...
  let value = target[prop];
  return (typeof value === 'function') ? value.bind(target) : value; // (*)
}

為什么我們需要一個(gè)函數(shù)去調(diào)用 value.bind(target)?

原因是對(duì)象方法(例如 user.checkPassword())必須能夠訪問(wèn) _password

user = {
  // ...
  checkPassword(value) {
    //對(duì)象方法必須能讀取 _password
    return value === this._password;
  }
}

對(duì) user.checkPassword() 的調(diào)用會(huì)將被代理的對(duì)象 user 作為 this(點(diǎn)符號(hào)之前的對(duì)象會(huì)成為 this),因此,當(dāng)它嘗試訪問(wèn) this._password 時(shí),get 捕捉器將激活(在任何屬性讀取時(shí),它都會(huì)被觸發(fā))并拋出錯(cuò)誤。

因此,我們?cè)?nbsp;(*) 行中將對(duì)象方法的上下文綁定到原始對(duì)象 target。然后,它們將來(lái)的調(diào)用將使用 target 作為 this,不會(huì)觸發(fā)任何捕捉器。

該解決方案通??尚?,但并不理想,因?yàn)橐粋€(gè)方法可能會(huì)將未被代理的對(duì)象傳遞到其他地方,然后我們就會(huì)陷入困境:原始對(duì)象在哪里,被代理的對(duì)象在哪里?

此外,一個(gè)對(duì)象可能會(huì)被代理多次(多個(gè)代理可能會(huì)對(duì)該對(duì)象添加不同的“調(diào)整”),并且如果我們將未包裝的對(duì)象傳遞給方法,則可能會(huì)產(chǎn)生意想不到的后果。

因此,在任何地方都不應(yīng)使用這種代理。

類的私有屬性

現(xiàn)代 JavaScript 引擎原生支持 class 中的私有屬性,這些私有屬性以 # 為前綴。它們?cè)?nbsp;私有的和受保護(hù)的屬性和方法 一章中有詳細(xì)描述。無(wú)需代理(proxy)。

但是,此類屬性有其自身的問(wèn)題。特別是,它們是不可繼承的。

帶有 “has” 捕捉器的 “in range”

讓我們來(lái)看更多示例。

我們有一個(gè) range 對(duì)象:

let range = {
  start: 1,
  end: 10
};

我們想使用 in 操作符來(lái)檢查一個(gè)數(shù)字是否在 range 范圍內(nèi)。

has 捕捉器會(huì)攔截 in 調(diào)用。

?has(target, property)
?

  • ?target? —— 是目標(biāo)對(duì)象,被作為第一個(gè)參數(shù)傳遞給 ?new Proxy?,
  • ?property? —— 屬性名稱。

示例如下

let range = {
  start: 1,
  end: 10
};

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end;
  }
});

alert(5 in range); // true
alert(50 in range); // false

漂亮的語(yǔ)法糖,不是嗎?而且實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單。

包裝函數(shù):"apply"

我們也可以將代理(proxy)包裝在函數(shù)周圍。

apply(target, thisArg, args) 捕捉器能使代理以函數(shù)的方式被調(diào)用:

  • ?target? 是目標(biāo)對(duì)象(在 JavaScript 中,函數(shù)就是一個(gè)對(duì)象),
  • ?thisArg? 是 ?this? 的值。
  • ?args? 是參數(shù)列表。

例如,讓我們回憶一下我們?cè)?nbsp;裝飾器模式和轉(zhuǎn)發(fā),call/apply 一章中所講的 delay(f, ms) 裝飾器。

在該章中,我們沒(méi)有用 proxy 來(lái)實(shí)現(xiàn)它。調(diào)用 delay(f, ms) 會(huì)返回一個(gè)函數(shù),該函數(shù)會(huì)在 ms 毫秒后把所有調(diào)用轉(zhuǎn)發(fā)給 f。

這是以前的基于函數(shù)的實(shí)現(xiàn):

function delay(f, ms) {
  // 返回一個(gè)包裝器(wrapper),該包裝器將在時(shí)間到了的時(shí)候?qū)⒄{(diào)用轉(zhuǎn)發(fā)給函數(shù) f
  return function() { // (*)
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

// 在進(jìn)行這個(gè)包裝后,sayHi 函數(shù)會(huì)被延遲 3 秒后被調(diào)用
sayHi = delay(sayHi, 3000);

sayHi("John"); // Hello, John! (after 3 seconds)

正如我們所看到的那樣,大多數(shù)情況下它都是可行的。包裝函數(shù) (*) 在到達(dá)延遲的時(shí)間后后執(zhí)行調(diào)用。

但是包裝函數(shù)不會(huì)轉(zhuǎn)發(fā)屬性讀取/寫入操作或者任何其他操作。進(jìn)行包裝后,就失去了對(duì)原始函數(shù)屬性的訪問(wèn),例如 name,length 和其他屬性:

function delay(f, ms) {
  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

alert(sayHi.length); // 1(函數(shù)的 length 是函數(shù)聲明中的參數(shù)個(gè)數(shù))

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 0(在包裝器聲明中,參數(shù)個(gè)數(shù)為 0)

Proxy 的功能要強(qiáng)大得多,因?yàn)樗梢詫⑺袞|西轉(zhuǎn)發(fā)到目標(biāo)對(duì)象。

讓我們使用 Proxy 來(lái)替換掉包裝函數(shù):

function delay(f, ms) {
  return new Proxy(f, {
    apply(target, thisArg, args) {
      setTimeout(() => target.apply(thisArg, args), ms);
    }
  });
}

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 1 (*) proxy 將“獲取 length”的操作轉(zhuǎn)發(fā)給目標(biāo)對(duì)象

sayHi("John"); // Hello, John!(3 秒后)

結(jié)果是相同的,但現(xiàn)在不僅僅調(diào)用,而且代理上的所有操作都能被轉(zhuǎn)發(fā)到原始函數(shù)。所以在 (*) 行包裝后的 sayHi.length 會(huì)返回正確的結(jié)果。

我們得到了一個(gè)“更豐富”的包裝器。

還存在其他捕捉器:完整列表在本文的開(kāi)頭。它們的使用模式與上述類似。

Reflect

Reflect 是一個(gè)內(nèi)建對(duì)象,可簡(jiǎn)化 Proxy 的創(chuàng)建。

前面所講過(guò)的內(nèi)部方法,例如 [[Get]] 和 [[Set]] 等,都只是規(guī)范性的,不能直接調(diào)用。

Reflect 對(duì)象使調(diào)用這些內(nèi)部方法成為了可能。它的方法是內(nèi)部方法的最小包裝。

以下是執(zhí)行相同操作和 Reflect 調(diào)用的示例:

操作 Reflect 調(diào)用 內(nèi)部方法
obj[prop] Reflect.get(obj, prop) [[Get]]
obj[prop] = value Reflect.set(obj, prop, value) [[Set]]
delete obj[prop] Reflect.deleteProperty(obj, prop) [[Delete]]
new F(value) Reflect.construct(F, value) [[Construct]]

例如:

let user = {};

Reflect.set(user, 'name', 'John');

alert(user.name); // John

尤其是,Reflect 允許我們將操作符(new,delete,……)作為函數(shù)(Reflect.constructReflect.deleteProperty,……)執(zhí)行調(diào)用。這是一個(gè)有趣的功能,但是這里還有一點(diǎn)很重要。

對(duì)于每個(gè)可被 Proxy 捕獲的內(nèi)部方法,在 Reflect 中都有一個(gè)對(duì)應(yīng)的方法,其名稱和參數(shù)與 Proxy 捕捉器相同。

所以,我們可以使用 Reflect 來(lái)將操作轉(zhuǎn)發(fā)給原始對(duì)象。

在下面這個(gè)示例中,捕捉器 get 和 set 均透明地(好像它們都不存在一樣)將讀取/寫入操作轉(zhuǎn)發(fā)到對(duì)象,并顯示一條消息:

let user = {
  name: "John",
};

user = new Proxy(user, {
  get(target, prop, receiver) {
    alert(`GET ${prop}`);
    return Reflect.get(target, prop, receiver); // (1)
  },
  set(target, prop, val, receiver) {
    alert(`SET ${prop}=${val}`);
    return Reflect.set(target, prop, val, receiver); // (2)
  }
});

let name = user.name; // 顯示 "GET name"
user.name = "Pete"; // 顯示 "SET name=Pete"

這里:

  • ?Reflect.get? 讀取一個(gè)對(duì)象屬性。
  • ?Reflect.set? 寫入一個(gè)對(duì)象屬性,如果寫入成功則返回 ?true?,否則返回 ?false?。

這樣,一切都很簡(jiǎn)單:如果一個(gè)捕捉器想要將調(diào)用轉(zhuǎn)發(fā)給對(duì)象,則只需使用相同的參數(shù)調(diào)用 Reflect.<method> 就足夠了。

在大多數(shù)情況下,我們可以不使用 Reflect 完成相同的事情,例如,用于讀取屬性的 Reflect.get(target, prop, receiver) 可以被替換為 target[prop]。盡管有一些細(xì)微的差別。

代理一個(gè) getter

讓我們看一個(gè)示例,來(lái)說(shuō)明為什么 Reflect.get 更好。此外,我們還將看到為什么 get/set 有第三個(gè)參數(shù) receiver,而且我們之前從來(lái)沒(méi)有使用過(guò)它。

我們有一個(gè)帶有 _name 屬性和 getter 的對(duì)象 user。

這是對(duì) user 對(duì)象的一個(gè)代理:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop];
  }
});

alert(userProxy.name); // Guest

其 get 捕捉器在這里是“透明的”,它返回原來(lái)的屬性,不會(huì)做任何其他的事。這對(duì)于我們的示例而言就足夠了。

一切似乎都很好。但是讓我們將示例變得稍微復(fù)雜一點(diǎn)。

另一個(gè)對(duì)象 admin 從 user 繼承后,我們可以觀察到錯(cuò)誤的行為:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
    return target[prop]; // (*) target = user
  }
});

let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

// 期望輸出:Admin
alert(admin.name); // 輸出:Guest (?!?)

讀取 admin.name 應(yīng)該返回 "Admin",而不是 "Guest"!

發(fā)生了什么?或許我們?cè)诶^承方面做錯(cuò)了什么?

但是,如果我們移除代理,那么一切都會(huì)按預(yù)期進(jìn)行。

問(wèn)題實(shí)際上出在代理中,在 (*) 行。

  1. 當(dāng)我們讀取 ?admin.name? 時(shí),由于 ?admin? 對(duì)象自身沒(méi)有對(duì)應(yīng)的的屬性,搜索將轉(zhuǎn)到其原型。
  2. 原型是 ?userProxy?。
  3. 從代理讀取 ?name? 屬性時(shí),?get? 捕捉器會(huì)被觸發(fā),并從原始對(duì)象返回 ?target[prop]? 屬性,在 ?(*)? 行。
  4. 當(dāng)調(diào)用 ?target[prop]? 時(shí),若 ?prop? 是一個(gè) getter,它將在 ?this=target? 上下文中運(yùn)行其代碼。因此,結(jié)果是來(lái)自原始對(duì)象 ?target? 的 ?this._name?,即來(lái)自 ?user?。

為了解決這種情況,我們需要 get 捕捉器的第三個(gè)參數(shù) receiver。它保證將正確的 this 傳遞給 getter。在我們的例子中是 admin。

如何把上下文傳遞給 getter?對(duì)于一個(gè)常規(guī)函數(shù),我們可以使用 call/apply,但這是一個(gè) getter,它不能“被調(diào)用”,只能被訪問(wèn)。

Reflect.get 可以做到。如果我們使用它,一切都會(huì)正常運(yùn)行。

這是更正后的變體:

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) { // receiver = admin
    return Reflect.get(target, prop, receiver); // (*)
  }
});


let admin = {
  __proto__: userProxy,
  _name: "Admin"
};

alert(admin.name); // Admin

現(xiàn)在 receiver 保留了對(duì)正確 this 的引用(即 admin),該引用是在 (*) 行中被通過(guò) Reflect.get 傳遞給 getter 的。

我們可以把捕捉器重寫得更短:

get(target, prop, receiver) {
  return Reflect.get(...arguments);
}

Reflect 調(diào)用的命名與捕捉器的命名完全相同,并且接受相同的參數(shù)。它們是以這種方式專門設(shè)計(jì)的。

因此,return Reflect... 提供了一個(gè)安全的方式,可以輕松地轉(zhuǎn)發(fā)操作,并確保我們不會(huì)忘記與此相關(guān)的任何內(nèi)容。

Proxy 的局限性

代理提供了一種獨(dú)特的方法,可以在最底層更改或調(diào)整現(xiàn)有對(duì)象的行為。但是,它并不完美。有局限性。

內(nèi)建對(duì)象:內(nèi)部插槽(Internal slot)

許多內(nèi)建對(duì)象,例如 Map,Set,Date,Promise 等,都使用了所謂的“內(nèi)部插槽”。

它們類似于屬性,但僅限于內(nèi)部使用,僅用于規(guī)范目的。例如,Map 將項(xiàng)目(item)存儲(chǔ)在 [[MapData]] 中。內(nèi)建方法可以直接訪問(wèn)它們,而不通過(guò) [[Get]]/[[Set]] 內(nèi)部方法。所以 Proxy 無(wú)法攔截它們。

為什么要在意這些呢?畢竟它們是內(nèi)部的!

好吧,問(wèn)題在這兒。在類似這樣的內(nèi)建對(duì)象被代理后,代理對(duì)象沒(méi)有這些內(nèi)部插槽,因此內(nèi)建方法將會(huì)失敗。

例如:

let map = new Map();

let proxy = new Proxy(map, {});

proxy.set('test', 1); // Error

在內(nèi)部,一個(gè) Map 將所有數(shù)據(jù)存儲(chǔ)在其 [[MapData]] 內(nèi)部插槽中。代理對(duì)象沒(méi)有這樣的插槽。內(nèi)建方法 Map.prototype.set 方法試圖訪問(wèn)內(nèi)部屬性 this.[[MapData]],但由于 this=proxy,在 proxy 中無(wú)法找到它,只能失敗。

幸運(yùn)的是,這有一種解決方法:

let map = new Map();

let proxy = new Proxy(map, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

proxy.set('test', 1);
alert(proxy.get('test')); // 1(工作了?。?/code>

現(xiàn)在它正常工作了,因?yàn)?nbsp;get 捕捉器將函數(shù)屬性(例如 map.set)綁定到了目標(biāo)對(duì)象(map)本身。

與前面的示例不同,proxy.set(...) 內(nèi)部 this 的值并不是 proxy,而是原始的 map。因此,當(dāng)set 捕捉器的內(nèi)部實(shí)現(xiàn)嘗試訪問(wèn) this.[[MapData]] 內(nèi)部插槽時(shí),它會(huì)成功。

?Array? 沒(méi)有內(nèi)部插槽

一個(gè)值得注意的例外:內(nèi)建 Array 沒(méi)有使用內(nèi)部插槽。那是出于歷史原因,因?yàn)樗霈F(xiàn)于很久以前。

所以,代理數(shù)組時(shí)沒(méi)有這種問(wèn)題。

私有字段

類的私有字段也會(huì)發(fā)生類似的情況。

例如,getName() 方法訪問(wèn)私有的 #name 屬性,并在代理后中斷:

class User {
  #name = "Guest";

  getName() {
    return this.#name;
  }
}

let user = new User();

user = new Proxy(user, {});

alert(user.getName()); // Error

原因是私有字段是通過(guò)內(nèi)部插槽實(shí)現(xiàn)的。JavaScript 在訪問(wèn)它們時(shí)不使用 [[Get]]/[[Set]]。

在調(diào)用 getName() 時(shí),this 的值是代理后的 user,它沒(méi)有帶有私有字段的插槽。

再次,帶有 bind 方法的解決方案使它恢復(fù)正常:

class User {
  #name = "Guest";

  getName() {
    return this.#name;
  }
}

let user = new User();

user = new Proxy(user, {
  get(target, prop, receiver) {
    let value = Reflect.get(...arguments);
    return typeof value == 'function' ? value.bind(target) : value;
  }
});

alert(user.getName()); // Guest

如前所述,該解決方案也有缺點(diǎn):它將原始對(duì)象暴露給該方法,可能使其進(jìn)一步傳遞并破壞其他代理功能。

Proxy != target

代理和原始對(duì)象是不同的對(duì)象。這很自然,對(duì)吧?

所以,如果我們使用原始對(duì)象作為鍵,然后對(duì)其進(jìn)行代理,之后卻無(wú)法找到代理了:

let allUsers = new Set();

class User {
  constructor(name) {
    this.name = name;
    allUsers.add(this);
  }
}

let user = new User("John");

alert(allUsers.has(user)); // true

user = new Proxy(user, {});

alert(allUsers.has(user)); // false

如我們所見(jiàn),進(jìn)行代理后,我們?cè)?nbsp;allUsers 中找不到 user,因?yàn)榇硎且粋€(gè)不同的對(duì)象。

Proxy 無(wú)法攔截嚴(yán)格相等性檢查 ?===?

Proxy 可以攔截許多操作符,例如 new(使用 construct),in(使用 has),delete(使用 deleteProperty)等。

但是沒(méi)有辦法攔截對(duì)于對(duì)象的嚴(yán)格相等性檢查。一個(gè)對(duì)象只嚴(yán)格等于其自身,沒(méi)有其他值。

因此,比較對(duì)象是否相等的所有操作和內(nèi)建類都會(huì)區(qū)分對(duì)象和代理。這里沒(méi)有透明的替代品。

可撤銷 Proxy

一個(gè) 可撤銷 的代理是可以被禁用的代理。

假設(shè)我們有一個(gè)資源,并且想隨時(shí)關(guān)閉對(duì)該資源的訪問(wèn)。

我們可以做的是將它包裝成可一個(gè)撤銷的代理,沒(méi)有任何捕捉器。這樣的代理會(huì)將操作轉(zhuǎn)發(fā)給對(duì)象,并且我們可以隨時(shí)將其禁用。

語(yǔ)法為:

let {proxy, revoke} = Proxy.revocable(target, handler)

該調(diào)用返回一個(gè)帶有 proxy 和 revoke 函數(shù)的對(duì)象以將其禁用。

這是一個(gè)例子:

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

// 將 proxy 傳遞到其他某處,而不是對(duì)象...
alert(proxy.data); // Valuable data

// 稍后,在我們的代碼中
revoke();

// proxy 不再工作(revoked)
alert(proxy.data); // Error

對(duì) revoke() 的調(diào)用會(huì)從代理中刪除對(duì)目標(biāo)對(duì)象的所有內(nèi)部引用,因此它們之間再無(wú)連接。

最初,revoke 與 proxy 是分開(kāi)的,因此我們可以傳遞 proxy,同時(shí)將 revoke 留在當(dāng)前范圍內(nèi)。

我們也可以通過(guò)設(shè)置 proxy.revoke = revoke 來(lái)將 revoke 綁定到 proxy

另一種選擇是創(chuàng)建一個(gè) WeakMap,其中 proxy 作為鍵,相應(yīng)的 revoke 作為值,這樣可以輕松找到 proxy 所對(duì)應(yīng)的 revoke

let revokes = new WeakMap();

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

revokes.set(proxy, revoke);

// ...我們代碼中的其他位置...
revoke = revokes.get(proxy);
revoke();

alert(proxy.data); // Error(revoked)

此處我們使用 WeakMap 而不是 Map,因?yàn)樗粫?huì)阻止垃圾回收。如果一個(gè)代理對(duì)象變得“不可訪問(wèn)”(例如,沒(méi)有變量再引用它),則 WeakMap 允許將其與它的 revoke 一起從內(nèi)存中清除,因?yàn)槲覀儾辉傩枰恕?

參考資料

總結(jié)

Proxy 是對(duì)象的包裝器,將代理上的操作轉(zhuǎn)發(fā)到對(duì)象,并可以選擇捕獲其中一些操作。

它可以包裝任何類型的對(duì)象,包括類和函數(shù)。

語(yǔ)法為:

let proxy = new Proxy(target, {
  /* trap */
});

……然后,我們應(yīng)該在所有地方使用 proxy 而不是 target。代理沒(méi)有自己的屬性或方法。如果提供了捕捉器(trap),它將捕獲操作,否則會(huì)將其轉(zhuǎn)發(fā)給 target 對(duì)象。

我們可以捕獲:

  • 讀?。?get?),寫入(?set?),刪除(?deleteProperty?)屬性(甚至是不存在的屬性)。
  • 函數(shù)調(diào)用(?apply? 捕捉器)。
  • ?new? 操作(?construct? 捕捉器)。
  • 許多其他操作(完整列表請(qǐng)見(jiàn)本文開(kāi)頭部分和 docs)。

這使我們能夠創(chuàng)建“虛擬”屬性和方法,實(shí)現(xiàn)默認(rèn)值,可觀察對(duì)象,函數(shù)裝飾器等。

我們還可以將對(duì)象多次包裝在不同的代理中,并用多個(gè)各個(gè)方面的功能對(duì)其進(jìn)行裝飾。

Reflect API 旨在補(bǔ)充 Proxy。對(duì)于任意 Proxy 捕捉器,都有一個(gè)帶有相同參數(shù)的 Reflect 調(diào)用。我們應(yīng)該使用它們將調(diào)用轉(zhuǎn)發(fā)給目標(biāo)對(duì)象。

Proxy 有一些局限性:

  • 內(nèi)建對(duì)象具有“內(nèi)部插槽”,對(duì)這些對(duì)象的訪問(wèn)無(wú)法被代理。請(qǐng)參閱上文中的解決方法。
  • 私有類字段也是如此,因?yàn)樗鼈円彩窃趦?nèi)部使用插槽實(shí)現(xiàn)的。因此,代理方法的調(diào)用必須具有目標(biāo)對(duì)象作為 ?this? 才能訪問(wèn)它們。
  • 對(duì)象的嚴(yán)格相等性檢查 ?===? 無(wú)法被攔截。
  • 性能:基準(zhǔn)測(cè)試(benchmark)取決于引擎,但通常使用最簡(jiǎn)單的代理訪問(wèn)屬性所需的時(shí)間也要長(zhǎng)幾倍。實(shí)際上,這僅對(duì)某些“瓶頸”對(duì)象來(lái)說(shuō)才重要。

任務(wù)


讀取不存在的屬性時(shí)出錯(cuò)

通常,嘗試讀取不存在的屬性會(huì)返回 ?undefined?。

創(chuàng)建一個(gè)代理,在嘗試讀取不存在的屬性時(shí),該代理拋出一個(gè)錯(cuò)誤。

這可以幫助及早發(fā)現(xiàn)編程錯(cuò)誤。

編寫一個(gè)函數(shù) wrap(target),該函數(shù)接受一個(gè) target 對(duì)象,并返回添加此方面功能的代理(proxy)。

其工作方式應(yīng)如下:

let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
      /* 你的代碼 */
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist: "age"

解決方案

let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      if (prop in target) {
        return Reflect.get(target, prop, receiver);
      } else {
        throw new ReferenceError(`Property doesn't exist: "${prop}"`)
      }
    }
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist: "age"

訪問(wèn) array[-1]

在某些編程語(yǔ)言中,我們可以使用從尾端算起的負(fù)值索引訪問(wèn)數(shù)組元素。

像這樣:

let array = [1, 2, 3];

array[-1]; // 3,最后一個(gè)元素
array[-2]; // 2,從尾端開(kāi)始向前移動(dòng)一步
array[-3]; // 1,從尾端開(kāi)始向前移動(dòng)兩步

換句話說(shuō),array[-N] 與 array[array.length - N] 相同。

創(chuàng)建一個(gè) proxy 來(lái)實(shí)現(xiàn)該行為。

其工作方式應(yīng)如下:

let array = [1, 2, 3];

array = new Proxy(array, {
  /* 你的代碼 */
});

alert( array[-1] ); // 3
alert( array[-2] ); // 2

// 其他數(shù)組功能應(yīng)保持“原樣”

解決方案

let array = [1, 2, 3];

array = new Proxy(array, {
  get(target, prop, receiver) {
    if (prop < 0) {
      // 即使我們像 arr[1] 這樣訪問(wèn)它
      // prop 是一個(gè)字符串,所以我們需要將其轉(zhuǎn)換成數(shù)字
      prop = +prop + target.length;
    }
    return Reflect.get(target, prop, receiver);
  }
});


alert(array[-1]); // 3
alert(array[-2]); // 2

可觀察的(Observable)

創(chuàng)建一個(gè)函數(shù) ?makeObservable(target)?,該函數(shù)通過(guò)返回一個(gè)代理“使得對(duì)象可觀察”。

其工作方式如下:

function makeObservable(target) {
  /* 你的代碼 */
}

let user = {};
user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John"; // alerts: SET name=John

換句話說(shuō),makeObservable 返回的對(duì)象就像原始對(duì)象一樣,但是具有 observe(handler) 方法,該方法可以將 handler 函數(shù)設(shè)置為在任何屬性被更改時(shí),都會(huì)被調(diào)用的函數(shù)。

每當(dāng)有屬性被更改時(shí),都會(huì)使用屬性的名稱和屬性值調(diào)用 handler(key, value) 函數(shù)。

P.S. 在本任務(wù)中,你可以只關(guān)注屬性寫入。其他的操作可以通過(guò)類似的方式實(shí)現(xiàn)。


解決方案

該解決方案包括兩部分:

  1. 無(wú)論 ?.observe(handler)? 何時(shí)被調(diào)用,我們都需要在某個(gè)地方記住 handler,以便以后可以調(diào)用它。我們可以使用 Symbol 作為屬性鍵,將 handler 直接存儲(chǔ)在對(duì)象中。
  2. 我們需要一個(gè)帶有 ?set? 陷阱的 proxy 來(lái)在發(fā)生任何更改時(shí)調(diào)用 handler。

let handlers = Symbol('handlers');

function makeObservable(target) {
  // 1. 初始化 handler 存儲(chǔ)
  target[handlers] = [];

  // 將 handler 函數(shù)存儲(chǔ)到數(shù)組中,以便于之后調(diào)用
  target.observe = function(handler) {
    this[handlers].push(handler);
  };

  // 2. 創(chuàng)建一個(gè) proxy 以處理更改
  return new Proxy(target, {
    set(target, property, value, receiver) {
      let success = Reflect.set(...arguments); // 將操作轉(zhuǎn)發(fā)給對(duì)象
      if (success) { // 如果在設(shè)置屬性時(shí)沒(méi)有出現(xiàn) error
        // 調(diào)用所有 handler
        target[handlers].forEach(handler => handler(property, value));
      }
      return success;
    }
  });
}

let user = {};

user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John";


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)