天天看點

JS中的代理與反射 — Proxy和Reflect

一提到ES6,相信大家不會陌生。自2015年正式釋出至今一直備受追捧。它所帶來的很多特性提高了日常開發效率,改變了建構代碼的思維方式。作為一個規範,起到了推動語言發展的作用。

最近在和同組同學聊天的時候,偶然得知他們準備使用proxy來解決目前架構的一些不足。突然發現Proxy是一個不太容易被人提起的東西。在好多ES6文法用的非常熟的情況下,依然會有一些東西被忽略掉。例如接下來要提到的Proxy和Reflect。

Proxy和Reflect是什麼

Proxy正常翻譯做代理,可以作用與對象或者函數上,資料ES6的新特性之一,用以實作js的元程式設計。

const proxy = new Proxy();           

使用Proxy,可以對函數或者對象的一些操作進行“代理”,或者這個地方叫做攔截會更合适一些。也就是說,使用代理,可以攔截對象或者函數的一些操作,這些操作包括但不限于:讀、寫、in等等操作。

有一個使用的場景:如何能夠打日志記錄下來某一個類的執行個體被讀取過什麼字段以及什麼字段被指派過?

我們先來看Proxy的構造函數

<T extends object>(target: T, handler: ProxyHandler<T>): T           

從函數定義可以看出,Proxy在建構的時候,需要傳入被代理的對象target以及一個handler。

被代理的對象可以是一個空對象{},那handler是什麼呢?

handler實際上是一個對象,其記錄了需要代理的行為,以及代理的方法。一個典型的handler定義如下

const handler = {
  get(target, propKey, receiver) {
    console.log('GET '+ propKey);
       // do other thing...
  },
  set(target, propKey, value, receiver) {
       console.log('SET ' + propKey);
       // do other thing...
   },
}           

使用這個代理就能夠在對象被讀取或者被寫入的情況下,列印出具體被操作的key。

接下來我們做一個實驗。假設我想把上面handler的規則,運用到某個對象上。

const obj = {};
const objP = new Proxy(obj, handler);
objP.hello = 'hello';           

控制台會列印出下面的内容

SET hello
"hello"           

然而假設這個時候,你直接在控制台中輸入objP.hello,并期望得到objP.hello的值hello的時候,你得到的隻有

GET hello
undefined           

而并沒有"hello"。這是因為上面的handler對get和set進行了攔截,并沒有去執行真正的get、set的代碼。是以變量沒有被指派。而這個時候就需要Reflect出場了。

Reflect是ES6的另一個新特性,主要使用者對象上,實作了JS的反射

看到這裡,會JAVA的同學要開心了:反射?我熟啊!!

恩,這裡的反射和JAVA的了解差不多。可以把對象的結構及其他相關資訊“反射出來”。通過這個新對象,可以友善的擷取到對象的結構、props、參數函數等等,并且可以以更優雅的方式來調用props裡的方法而避免一些問題的發生。

回到上面那個例子,需要把handler給改一下:

const handler = {
  get(target, propKey, receiver) {
    console.log('GET '+ propKey);
       // do other thing...
    return Reflect.get(target, propKey, receiver);
  },
  set(target, propKey, value, receiver) {
       console.log('SET ' + propKey);
       // do other thing...
    return Reflect.set(target, propKey, value, receiver);
   },
}           

與上面例子不同的是,在get和set的最後,加上了Reflect.get、Reflect.set,使用這樣的方式,能夠讓get、set繼續執行它之前應該執行的行為。

再執行一遍上面的執行個體,最後在敲入objP.hello的時候,就能得到想要的結果。

GET hello
"hello"           

使用場景

說了這麼多用法,舉幾個可以使用的場景。

1 通過對set、get函數的代理,我們可以對通路不存在的對象屬性進行單獨的處理。

get(target, propKey, receiver) {
    console.log('GET '+ propKey);
    if(!(propKey in target)){
      throw new Exception('null target');
    }
    return Reflect.get(target, propKey, receiver);
  },           

2 MVVM的資料雙綁。

function observedArray(cb) {
    const array = [];
    return new Proxy(array, {
        set(target, propertyKey, value, receiver) {
            cb(propertyKey, value);
            return Reflect.set(target, propertyKey, value, receiver);
        }
    });    
}
const array = observedArray(
    (key, value) => console.log(`${key}: ${value}`));

array.push('a');           

這個時候當array數組元素改變的時候,會調用對應的回調。

3 做

Type checking

Proxy支援的代理方法清單

以下清單摘選自

EXPLORING ES6

對象類:

  • defineProperty(target, propKey, propDesc) : boolean           
  • deleteProperty(target, propKey) : boolean           
  • get(target, propKey, receiver) : any           
  • getOwnPropertyDescriptor(target, propKey) : PropDesc|Undefined           
  • getPrototypeOf(target) : Object|Null           
  • has(target, propKey) : boolean           
  • isExtensible(target) : boolean           
  • ownKeys(target) : Array<PropertyKey>           
  • preventExtensions(target) : boolean           
  • set(target, propKey, value, receiver) : boolean           
  • setPrototypeOf(target, proto) : boolean           
    函數類
  • apply(target, thisArgument, argumentsList) : any