詳解 JS 中的 Proxy(代理)和 Reflect(反射)

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第5天,點選檢視活動詳情

詳解 JS 中的 Proxy(代理)和 Reflect(反射)

總所周知,Vue2 => Vue3 時,資料響應式方法從Object.defineProperty()方法變成了Proxy(),所以今天與大家 Proxy(代理)和 Reflect(反射)的知識。

講解 Proxy 和 Reflect 前,我們需要先了解屬性描述符的作用,所以我們線簡單解釋一下屬性描述符的知識。

1.屬性描述符

屬性描述符(Property Descriptor) 本質上是一個 JavaScript 普通物件,用於描述一個屬性的相關資訊,共有下面這幾種屬性。

  • value:屬性值
  • configurable:該屬性的描述符是否可以修改
  • enumerable:該屬性是否可以被列舉
  • writable:該屬性是否可以被重新賦值
  • 存取器屬性:屬性描述符中如果配置了 get 和 set 中的任何一個,則該屬性不再是一個普通屬性,而變成了存取器屬性
  • get()讀值函式:如果一個屬性是存取器屬性,則讀取該屬性時,會執行 get 方法,並將 get 方法得到的返回值作為屬性值
  • set(newVal)存值函式:如果給該屬性賦值,則會執行 set 方法,newVal 引數為賦值的值。

存取器屬性最大的意義,在於可以控制屬性的讀取和賦值,在函式裡可以進行各種操作。

Vue2 資料響應式就是使用了這一點,在 getter 和 setter 函式中進行了資料繫結與派發更新。

注意點:value 和 writable 屬性不能與 get 和 set 屬性二者不可共存,二者只能選其一。

檢視某個物件的屬性描述符,使用以下這兩種方法:

js Object.getOwnPropertyDescriptor(物件, 屬性名); //得到一個物件的某個屬性的屬性描述符 Object.getOwnPropertyDescriptors(物件); //得到某個物件的所有屬性描述符

為某個物件新增屬性時 或 修改屬性時,配置其屬性描述符,使用以下這兩種方法:

js Object.defineProperty(物件, 屬性名, 描述符); //設定一個物件的某個屬性 Object.defineProperties(物件, 多個屬性的描述符); //設定一個物件的多個屬性

2.Reflect

  1. Reflect 是什麼? Reflect 是一個內建的 JS 物件,它提供了一系列方法,可以讓開發者通過呼叫這些方法,訪問一些 JS 底層功能。

由於它類似於其他語言的反射,因此取名為 Reflect。

  1. 它可以做什麼? 使用 Reflect 可以實現諸如:屬性的賦值與取值、呼叫普通函式、呼叫建構函式、判斷屬性是否存在與物件中 等等功能。

  2. 這些功能不是已經存在了嗎?為什麼還需要用 Reflect 實現一次? 有一個重要的理念,在 ES5 就被提出:減少魔法、讓程式碼更加純粹(語言的方法使用 API 實現,而不使用特殊語法實現),這種理念很大程度上是受到函數語言程式設計的影響。 ES6 進一步貫徹了這種理念,它認為,對屬性記憶體的控制、原型鏈的修改、函式的呼叫等等,這些都屬於底層實現,屬於一種魔法,因此,需要將它們提取出來,形成一個正常的 API,並高度聚合到某個物件中,於是就造就了 Reflect 物件。 因此,你可以看到 Reflect 物件中有很多的 API 都可以使用過去的某種語法或其他 API 實現。

  3. Reflect 裡面提供了哪些 API 呢?

| Reflect API | 用處 | 等同於 | | :-----------------------------------------------------: | :---------------------------------------------------------------------------: | :-------------------: | | Reflect.get(target, propertyKey) | 讀取物件 target 的屬性 propertyKey | 物件的屬性值讀取操作 | | Reflect.set(target, propertyKey, value) | 設定物件 target 的屬性 propertyKey 的值為 value | 物件的屬性賦值操作 | | Reflect.has(target, propertyKey) | 判斷一個物件是否擁有一個屬性 | in 操作符 | | Reflect.defineProperty(target, propertyKey, attributes) | 類似於 Object.defineProperty,不同的是如果配置出現問題,返回 false 而不是報錯 | Object.defineProperty | | Reflect.deleteProperty(target, propertyKey) | 刪除一個物件的屬性 | delete 操作符 | | Reflect.apply(target, thisArgument, argumentsList) | 呼叫一個指定的函式,並繫結 this 和引數列表 | 函式呼叫操作 | | Reflect.construct(target, argumentsList) | 用建構函式的方式建立一個物件 | new 操作符 |

其他更多的 Reflect API

3.Proxy

ECMAScript 6 新增的代理和反射為開發者提供了攔截並向基本操作嵌入額外行為的能力

具體地說,可以給目標物件(target)定義一個關聯的代理物件,而這個代理物件可當作一個抽象的目標物件來使用

因此在對目標物件的各種操作影響到目標物件之前,我們可以在代理物件中對這些操作加以控制,並且最終也可能會改變操作返回的結果。

所以我的理解是:代理(Proxy)能使我們開發者擁有一種間接修改底層方法的能力,從而控制使用者的操作。

proxy的作用.png

3.1 建立空代理

最簡單的代理是空代理,即除了作為一個抽象的目標物件,什麼也不做。 預設情況下,在代理物件上執行的所有操作都會無障礙地傳播到目標物件。因此,在任何可以使用目標物件的地方,都可以通過同樣的方式來使用與之關聯的代理物件。

代理是使用 Proxy 建構函式建立的,這個建構函式接收兩個引數:目標物件和處理程式物件。缺 少其中任何一個引數都會丟擲 TypeError。返回一個代理物件 如:new Proxy(target, handler);

要建立空代理,可以傳一個簡單的物件字面量作為處理程式物件,從而讓所有操作暢通無阻地抵達目標物件。

```javascript const target = { id: 'target' }; //target:目標物件 const handler = {}; //handler:是一個普通物件,其中可以重寫底層實現 //建立空物件 const proxy = new Proxy(target, handler);

// id 屬性會訪問同一個值 console.log(target.id); // target console.log(proxy.id); // target

// 給目標屬性賦值會反映在兩個物件上 因為兩個物件訪問的是同一個值 target.id = 'foo'; console.log(target.id); // foo console.log(proxy.id); // foo

// 給代理屬性賦值會反映在兩個物件上 因為這個賦值會轉移到目標物件 proxy.id = 'bar'; console.log(target.id); // bar console.log(proxy.id); // bar // hasOwnProperty()方法在兩個地方 也都會應用到目標物件 console.log(target.hasOwnProperty('id')); // true console.log(proxy.hasOwnProperty('id')); // true

// Proxy.prototype 是 undefined 因此不能使用 instanceof 操作符 console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check

// 嚴格相等可以用來區分代理和目標 console.log(target === proxy); // false ```

3.2 定義捕獲器

使用代理的主要目的是可以定義捕獲器(trap)。捕獲器就是在處理程式物件中定義的“基本操作的攔截器”。

每個處理程式物件可以包含零個或多個捕獲器,每個捕獲器都對應一種基本操作,可以直接或間接在代理物件上呼叫。

每次在代理物件上呼叫這些基本操作時,代理可以在這些操作傳播到目標物件之前先呼叫捕獲器函式,從而攔截並修改相應的行為。

所有捕獲器都可以訪問相應的引數,基於這些引數可以重建被捕獲方法的原始行為。比如,get()捕獲器會接收到目標物件要查詢的屬性代理物件三個引數。

javascript const target = { foo: "bar", }; const handler = { // 捕獲器在處理程式物件中以方法名為鍵 get(trapTarget, property, receiver) { //trapTarget - 目標物件 //property - 要查詢的屬性 //receiver - 代理物件 return "handler override"; }, }; const proxy = new Proxy(target, handler); console.log(target.foo); // bar console.log(proxy.foo); // handler override

3.3 捕獲器不變式

使用捕獲器幾乎可以改變所有基本方法的行為,但也不是沒有限制。

根據 ECMAScript 規範,每個捕獲的方法都知道目標物件上下文、捕獲函式簽名,而捕獲處理程式的行為必須遵循“捕獲器不變式”(trap invariant)。捕獲器不變式因方法不同而異,但通常都會防止捕獲器定義出現過於反常的行為。

比如,如果目標物件有一個不可配置且不可寫的資料屬性,那麼在捕獲器返回一個與該屬性不同的值時,會丟擲 TypeError:

javascript const target = {}; Object.defineProperty(target, "foo", { configurable: false, writable: false, value: "bar", }); const handler = { get() { return "qux"; }, }; const proxy = new Proxy(target, handler); console.log(proxy.foo); // TypeError

3.4 可撤銷代理

有時候可能需要中斷代理物件與目標物件之間的聯絡。

Proxy 也暴露了 revocable()方法,這個方法支援撤銷代理物件與目標物件的關聯。

後續可直接呼叫撤銷函式 revoke() 來撤銷代理。

撤銷代理之後再呼叫代理會丟擲 TypeError,撤銷函式和代理物件是在例項化時同時生成的:

javascript const target = { foo: "bar", }; const handler = { get() { return "intercepted"; }, }; const { proxy, revoke } = Proxy.revocable(target, handler); console.log(proxy.foo); // intercepted console.log(target.foo); // bar revoke(); console.log(proxy.foo); // TypeError

4.代理捕獲器與反射方法

4.1 get()

get()捕獲器會在獲取屬性值的操作中被呼叫。對應的反射 API 方法為 Reflect.get()

javascript const myTarget = {}; const proxy = new Proxy(myTarget, { get(target, property, receiver) { console.log("get()"); return Reflect.get(...arguments); }, }); proxy.foo; // 觸發get()捕獲器

  1. 返回值 返回值無限制。
  2. 攔截的操作
  3. proxy.property
  4. proxy[property]
  5. Object.create(proxy)[property]
  6. Reflect.get(proxy, property, receiver)
  7. 捕獲器處理程式引數
  8. target:目標物件。
  9. property:引用的目標物件上的字串鍵屬性。① - receiver:代理物件或繼承代理物件的物件。
  10. 捕獲器不變式 如果 target.property 不可寫且不可配置,則處理程式返回的值必須與 target.property 匹配。 如果 target.property 不可配置且[[Get]]特性為 undefined,處理程式的返回值也必須是 undefined。

4.2 set()

set()捕獲器會在設定屬性值的操作中被呼叫。對應的反射 API 方法為 Reflect.set()。

javascript const myTarget = {}; const proxy = new Proxy(myTarget, { set(target, property, value, receiver) { console.log("set()"); return Reflect.set(...arguments); }, }); proxy.foo = "bar"; // 觸發set()捕獲器

  1. 返回值 返回 true 表示成功;返回 false 表示失敗,嚴格模式下會丟擲 TypeError。
  2. 攔截的操作
  3. proxy.property = value
  4. proxy[property] = value
  5. Object.create(proxy)[property] = value
  6. Reflect.set(proxy, property, value, receiver)
  7. 捕獲器處理程式引數
  8. target:目標物件。
  9. property:引用的目標物件上的字串鍵屬性。
  10. value:要賦給屬性的值。
  11. receiver:接收最初賦值的物件。
  12. 捕獲器不變式 如果 target.property 不可寫且不可配置,則不能修改目標屬性的值。 如果 target.property 不可配置且[[Set]]特性為 undefined,則不能修改目標屬性的值。 在嚴格模式下,處理程式中返回 false 會丟擲 TypeError。

4.3 has()

has()捕獲器會在 in 操作符中被呼叫。對應的反射 API 方法為 Reflect.has()。

javascript const myTarget = {}; const proxy = new Proxy(myTarget, { has(target, property) { console.log("has()"); return Reflect.has(...arguments); }, }); "foo" in proxy; //觸發 has()捕獲器

  1. 返回值 has()必須返回布林值,表示屬性是否存在。返回非布林值會被轉型為布林值。
  2. 攔截的操作
  3. property in proxy
  4. property in Object.create(proxy)
  5. with(proxy) {(property);}
  6. Reflect.has(proxy, property)
  7. 捕獲器處理程式引數
  8. target:目標物件。
  9. property:引用的目標物件上的字串鍵屬性。
  10. 捕獲器不變式 如果 target.property 存在且不可配置,則處理程式必須返回 true。 如果 target.property 存在且目標物件不可擴充套件,則處理程式必須返回 true。

4.4 deleteProperty()

deleteProperty()捕獲器會在 delete 操作符中被呼叫。對應的反射 API 方法為 Reflect.deleteProperty()。

javascript const myTarget = {}; const proxy = new Proxy(myTarget, { deleteProperty(target, property) { console.log("deleteProperty()"); return Reflect.deleteProperty(...arguments); }, }); delete proxy.foo; // 觸發deleteProperty()捕獲器

  1. 返回值 deleteProperty()必須返回布林值,表示刪除屬性是否成功。返回非布林值會被轉型為布林值。
  2. 攔截的操作
  3. delete proxy.property
  4. delete proxy[property]
  5. Reflect.deleteProperty(proxy, property)
  6. 捕獲器處理程式引數
  7. target:目標物件。
  8. property:引用的目標物件上的字串鍵屬性。
  9. 捕獲器不變式 如果自有的 target.property 存在且不可配置,則處理程式不能刪除這個屬性。

4.5 apply()

apply()捕獲器會在呼叫函式時中被呼叫。對應的反射 API 方法為 Reflect.apply()。

javascript const myTarget = () => {}; const proxy = new Proxy(myTarget, { apply(target, thisArg, ...argumentsList) { console.log("apply()"); return Reflect.apply(...arguments); }, }); proxy(); // 觸發apply()捕獲器

  1. 返回值 返回值無限制。
  2. 攔截的操作
  3. proxy(...argumentsList)
  4. Function.prototype.apply(thisArg, argumentsList)
  5. Function.prototype.call(thisArg, ...argumentsList)
  6. Reflect.apply(target, thisArgument, argumentsList)
  7. 捕獲器處理程式引數
  8. target:目標物件。
  9. thisArg:呼叫函式時的 this 引數。
  10. argumentsList:呼叫函式時的引數列表
  11. 捕獲器不變式 target 必須是一個函式物件。

4.6 construct()

construct()捕獲器會在 new 操作符中被呼叫。對應的反射 API 方法為 Reflect.construct()。

javascript const myTarget = function () {}; const proxy = new Proxy(myTarget, { construct(target, argumentsList, newTarget) { console.log("construct()"); return Reflect.construct(...arguments); }, }); new proxy(); // 觸發construct()捕獲器

  1. 返回值 construct()必須返回一個物件。
  2. 攔截的操作
  3. new proxy(...argumentsList)
  4. Reflect.construct(target, argumentsList, newTarget)
  5. 捕獲器處理程式引數
  6. target:目標建構函式
  7. argumentsList:傳給目標建構函式的引數列表。
  8. newTarget:最初被呼叫的建構函式。
  9. 捕獲器不變式 target 必須可以用作建構函式。

還有另外七種捕獲器:

  • defineProperty()捕獲器會在 Object.defineProperty()中被呼叫。
  • getOwnPropertyDescriptor()捕獲器會在 Object.getOwnPropertyDescriptor()中被調 用。
  • ownKeys()捕獲器會在 Object.keys()及類似方法中被呼叫。
  • getPrototypeOf()捕獲器會在 Object.getPrototypeOf()中被呼叫。
  • setPrototypeOf()捕獲器會在 Object.setPrototypeOf()中被呼叫。
  • isExtensible()捕獲器會在 Object.isExtensible()中被呼叫。
  • preventExtensions()捕獲器會在 Object.preventExtensions()中被呼叫。

這七種捕獲器詳細介紹可參考MDN - Proxy

參考部落格

結語

這是我目前所瞭解的知識面中最好的解答,當然也有可能存在一定的誤區。

所以如果對本文存在疑惑,可以在評論區留言,我會及時回覆的,歡迎大家指出文中的錯誤觀點。

最後碼字不易,覺得有幫助的朋友點贊、收藏、關注走一波。