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

引言

EcmaScript 2015 中引入了 Proxy 代理 [1] 與 Reflect 反射 [2] 兩個新的內置模塊。

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

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

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

前置知識

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

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

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

單獨使用 Proxy

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

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 究竟表示什麼意思呢?大多數同學會將它理解成爲代理對象,但這是不全面的。

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

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 代表的一種情況而已。

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

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 屬性訪問器的 “屏蔽” 效果,在這篇文章 [6] 中進行了詳細闡述。這裏我們就不展開講解了。

我們可以看到,上述的代碼同樣我在 proxy 對象的 get 陷阱上打印了 console.log(receiver === proxy); 結果卻是 false 。

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

它是爲了傳遞正確的調用者指向,你可以看看下方的代碼:

...
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 對象。

比如:

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 代表的是代理對象本身或者繼承與代理對象的對象,它表示觸發陷阱時正確的上下文。

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 時,我希望應該正確輸出對應的自身上的 name 屬性也就是所謂的 obj.value => wang.haoyu 。

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

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);

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

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

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

總結

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

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

結尾

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

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

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

參考資料

[1]

Proxy 代理: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

[2]

Reflect 反射: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

[3]

Object.defineProperty(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

[4]

Reflect: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

[5]

Proxy : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy

[6]

這篇文章: https://juejin.cn/post/7074935443355074567

作者:19 組清風 

https://juejin.cn/post/7080916820353351688

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Hu5efb5gSN6DiK5JRzRfUg