為什麼Proxy一定要配合Reflect使用?

語言: CN / TW / HK

theme: awesome-green

引言

EcmaScript 2015 中引入了 Proxy 代理Reflect 反射 兩個新的內建模組。

我們可以利用 Proxy 和 Reflect 來實現對於物件的代理劫持操作,類似於 Es 5 中 Object.defineProperty()的效果,不過 Reflect & Proxy 遠遠比它強大。

大多數開發者都瞭解這兩個 Es6 中的新增內建模組,可是你也許並不清楚為什麼 Proxy 一定要配合 Reflect 使用。

這裡,文章通過幾個通俗易懂的例子來講述它們之間相輔相成的關係。

前置知識

  • Proxy 代理,它內建了一系列”陷阱“用於建立一個物件的代理,從而實現基本操作的攔截和自定義(如屬性查詢、賦值、列舉、函式呼叫等)。

  • Reflect 反射,它提供攔截 JavaScript 操作的方法。這些方法與 Proxy 的方法相同。

簡單來說,我們可以通過 Proxy 建立對於原始物件的代理物件,從而在代理物件中使用 Reflect 達到對於 JavaScript 原始操作的攔截。

如果你還不瞭解 & ,那麼趕快去 MDN 上去補習他們的知識吧。

畢竟大名鼎鼎的 VueJs/Core 中核心的響應式模組就是基於這兩個 Api 來實現的。

單獨使用 Proxy

開始的第一個例子,我們先單獨使用 Proxy 來烹飪一道簡單的開胃小菜:

```js const obj = { name: 'wang.haoyu', };

const proxy = new Proxy(obj, { // get陷阱中target表示原物件 key表示訪問的屬性名 get(target, key) { console.log('劫持你的資料訪問' + key); return target[key] }, });

proxy.name // 劫持你的資料訪問name -> wang.haoyu ```

看起來很簡單對吧,我們通過 Proxy 建立了一個基於 obj 物件的代理,同時在 Proxy 中聲明瞭一個 get 陷阱。

當訪問我們訪問 proxy.name 時實際觸發了對應的 get 陷阱,它會執行 get 陷阱中的邏輯,同時會執行對應陷阱中的邏輯,最終返回對應的 target[key] 也就是所謂的 wang.haoyu .

Proxy 中的 receiver

上邊的 Demo 中一切都看起來順風順水沒錯吧,細心的同學在閱讀 Proxy 的 MDN 文件上可能會發現其實 Proxy 中 get 陷阱中還會存在一個額外的引數 receiver 。

那麼這裡的 receiver 究竟表示什麼意思呢?大多數同學會將它理解成為代理物件,但這是不全面的。

接下來同樣讓我們以一個簡單的例子來作為切入點:

```js const obj = { name: 'wang.haoyu', };

const proxy = new Proxy(obj, { // get陷阱中target表示原物件 key表示訪問的屬性名 get(target, key, receiver) { console.log(receiver === proxy); return target[key]; }, });

// log: true proxy.name; ```

上述的例子中,我們在 Proxy 例項物件的 get 陷阱上接收了 receiver 這個引數。

同時,我們在陷阱內部列印 console.log(receiver === proxy); 它會打印出 true ,表示這裡 receiver 的確是和代理物件相等的。

所以 receiver 的確是可以表示代理物件,但是這僅僅是 receiver 代表的一種情況而已。

接下來我們來看另外一個例子:

```js const parent = { get value() { return '19Qingfeng'; }, };

const proxy = new Proxy(parent, { // get陷阱中target表示原物件 key表示訪問的屬性名 get(target, key, receiver) { console.log(receiver === proxy); return target[key]; }, });

const obj = { name: 'wang.haoyu', };

// 設定obj繼承與parent的代理物件proxy Object.setPrototypeOf(obj, proxy);

// log: false obj.value ```

關於原型上出現的 get/set 屬性訪問器的“遮蔽”效果,我在這篇文章中進行了詳細闡述。這裡我就不展開講解了。

我們可以看到,上述的程式碼同樣我在 proxy 物件的 get 陷阱上列印了 console.log(receiver === proxy); 結果卻是 false 。

那麼你可以稍微思考下這裡的 receiver 究竟是什麼呢? 其實這也是 proxy 中 get 陷阱第三個 receiver 存在的意義。

它是為了傳遞正確的呼叫者指向,你可以看看下方的程式碼:

js ... const proxy = new Proxy(parent, { // get陷阱中target表示原物件 key表示訪問的屬性名 get(target, key, receiver) { - console.log(receiver === proxy) // log:false + console.log(receiver === obj) // log:true return target[key]; }, }); ...

其實簡單來說,get 陷阱中的 receiver 存在的意義就是為了正確的在陷阱中傳遞上下文。

涉及到屬性訪問時,不要忘記 get 陷阱還會觸發對應的屬性訪問器,也就是所謂的 get 訪問器方法。

我們可以清楚的看到上述的 receiver 代表的是繼承與 Proxy 的物件,也就是 obj。

看到這裡,我們明白了 Proxy 中 get 陷阱的 receiver 不僅僅代表的是 Proxy 代理物件本身,同時也許他會代表繼承 Proxy 的那個物件。

其實本質上來說它還是為了確保陷阱函式中呼叫者的正確的上下文訪問,比如這裡的 receiver 指向的是 obj 。

當然,你不要將 revceiver 和 get 陷阱中的 this 弄混了,陷阱中的 this 關鍵字表示的是代理的 handler 物件。

比如:

```js const parent = { get value() { return '19Qingfeng'; }, };

const handler = { get(target, key, receiver) { console.log(this === handler); // log: true console.log(receiver === obj); // log: true return target[key]; }, };

const proxy = new Proxy(parent, handler);

const obj = { name: 'wang.haoyu', };

// 設定obj繼承與parent的代理物件proxy Object.setPrototypeOf(obj, proxy);

// log: false obj.value ```

Reflect 中的 receiver

在清楚了 Proxy 中 get 陷阱的 receiver 後,趁熱打鐵我們來聊聊 Reflect 反射 API 中 get 陷阱的 receiver。

我們知道在 Proxy 中(以下我們都以 get 陷阱為例)第三個引數 receiver 代表的是代理物件本身或者繼承與代理物件的物件,它表示觸發陷阱時正確的上下文。

```js const parent = { name: '19Qingfeng', get value() { return this.name; }, };

const handler = { get(target, key, receiver) { return Reflect.get(target, key); // 這裡相當於 return target[key] }, };

const proxy = new Proxy(parent, handler);

const obj = { name: 'wang.haoyu', };

// 設定obj繼承與parent的代理物件proxy Object.setPrototypeOf(obj, proxy);

// log: false console.log(obj.value); ```

我們稍微分析下上邊的程式碼:

  • 當我們呼叫 obj.value 時,由於 obj 本身不存在 value 屬性。

  • 它繼承的 proxy 物件中存在 value 的屬性訪問操作符,所以會發生遮蔽效果。

  • 此時會觸發 proxy 上的 get value() 屬性訪問操作。

  • 同時由於訪問了 proxy 上的 value 屬性訪問器,所以此時會觸發 get 陷阱。

  • 進入陷阱時,target 為源物件也就是 parent ,key 為 value 。

  • 陷阱中返回 Reflect.get(target,key) 相當於 target[key]

  • 此時,不知不覺中 this 指向在 get 陷阱中被偷偷修改掉了!!

  • 原本呼叫方的 obj 在陷阱中被修改成為了對應的 target 也就是 parent 。

  • 自然而然打印出了對應的 parent[value] 也就是 19Qingfeng 。

這顯然不是我們期望的結果,當我訪問 obj.value 時,我希望應該正確輸出對應的自身上的 name 屬性也就是所謂的 obj.value => wang.haoyu 。

那麼,Relfect 中 get 陷阱的 receiver 就大顯神通了。

```js const parent = { name: '19Qingfeng', get value() { return this.name; }, };

const handler = { get(target, key, receiver) { - return Reflect.get(target, key); + return Reflect.get(target, key, receiver); }, };

const proxy = new Proxy(parent, handler);

const obj = { name: 'wang.haoyu', };

// 設定obj繼承與parent的代理物件proxy Object.setPrototypeOf(obj, proxy);

// log: wang.haoyu console.log(obj.value); ```

上述程式碼原理其實非常簡單:

  • 首先,之前我們提到過在 Proxy 中 get 陷阱的 receiver 不僅僅會表示代理物件本身同時也還有可能表示繼承於代理物件的物件,具體需要區別與呼叫方。這裡顯然它是指向繼承與代理物件的 obj 。

  • 其次,我們在 Reflect 中 get 陷阱中第三個引數傳遞了 Proxy 中的 receiver 也就是 obj 作為形參,它會修改呼叫時的 this 指向。

你可以簡單的將 Reflect.get(target, key, receiver) 理解成為 target[key].call(receiver),不過這是一段虛擬碼,但是這樣你可能更好理解。

相信看到這裡你已經明白 Relfect 中的 receiver 代表的含義是什麼了,沒錯它正是可以修改屬性訪問中的 this 指向為傳入的 receiver 物件。

image.png

總結

相信看到這裡大家都已經明白了,為什麼Proxy一定要配合Reflect使用。恰恰是為什麼觸發代理物件的劫持時保證正確的 this 上下文指向。

我們再來稍稍回憶一下,針對於 get 陷阱(當然 set 其他之類涉及到 receiver 的陷阱同理):

  • Proxy 中接受的 Receiver 形參表示代理物件本身或者繼承與代理物件的物件。

  • Reflect 中傳遞的 Receiver 實參表示修改執行原始操作時的 this 指向。

結尾

這裡就到了文章的結尾了,至於為什麼會突然提到 Proxy & Reflect 的話題。

其實是筆者最近在閱讀 Vue/corejs 的原始碼內容,剛好它內部大量應用於 Proxy & Reflect 所以就產生了這篇文章。

關於 Proxy 為什麼一定要配合 Reflect 使用,具體結合 VueJs 中響應式模組的依賴收集其實會更好理解一些。不過這裡為了照顧不太熟悉 VueJs 的同學所以就沒有展開了。

當然,最近我也在閱讀 VueJs 的過程中嘗試書寫一些階段性總結文章。之後在文章中也會詳細講解這一過程,有興趣的同學可以持續關注我的最新動態~

結尾,謝謝每一個小夥伴。我們一起加油~