iT邦幫忙

0

Typescript 與 Javascript Proxy 和 Reflect 搭配

Typescript 與 Javascript Proxy 和 Reflect 搭配

2019-10-15

Javascript 的 Proxy 物件 & Reflect API

下面表格是 Proxy 物件在各種瀏覽器版本中的支援程度, 資料參考來源從 caniuse 網站取得

Browser Support Version
IE 6 - 10, 11
Edge 12-18, 76
Firefox 18-68, 69
Chrome 49-76, 77
Safari 10-12.1, 13
Opera 36-60, 62
iOS Safari 10-12.3, 13.1
Opera Mini All Not support
Android Browser 76
Opera Mobile 46
Chrome for Android 76
Firefox for Android 68
UC Browser for Android 12.12
Samsung Internet 5-9.2, 10.1
QQ Browser 1.2
Baidu Browser 7.12
KaiOS Browser 2.5

以上版本號碼有刪除的樣式, 表示不支援

Proxy 和Reflect 是ES6 新增的API, Proxy 是一個函式物件, 它提供一個機會讓你能介入一般物件的基本操作行為, 很像 interceptor 會做的事情一樣.

使用的方法如下

const proxyObj = new Proxy(target, handler);
  • target 就是你想要代理的對象
  • handler 則是一個物件, 定義了所有你想替 target 代為管理的操作定義

以下是一個典型的 Proxy 範例, 示範了 cat 物件被代理成 catProxy 物件, 然後不能存取 cat 私有變數的方法.

let cat = {
  _secret: 'I am Mr.Brain',
  name: 'Flash'
};

let catProxy = new Proxy(cat, {
  get: function (target, prop) {
     if( prop.startsWith('_') ) {
        console.log('不能存取私有變數');
        return false;
     }
     return target[prop];
  },
  set: function (target, prop, value) {
    if (prop.startsWith('_')) {
      console.log('不能修改私有變數');
      return false;
    }
    target[prop] = value;
  },
  has: function (target, prop) {
    return prop.startsWith('_') ? false : (prop in target);
  }
});

不是什麼都可以被代理的

如果你的物件擁有 configurable: false 與 writable: false 的屬性,那該物件就無法被 proxy 代理, 如下示範

const target = Object.defineProperties({}, {
  Cat: {
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return '???';
  }
};

const proxy = new Proxy(target, handler);
proxy.FooBar

一旦你按照上面的程式碼執行, 就會噴出這個例外錯誤

Uncaught TypeError: 'get' on proxy: property 'FooBar' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'undefined' but got '???')

Reflect 與 Proxy 的搭配

我們在 Proxy 中, 如果需要 target 物件的預設操作, 使用 Reflect 會更清楚, 如下示範

const loggedObj = new Proxy(obj, {
  get: function(target, name) {
    console.log("get", target, name);
    return Reflect.get(target, name);
  }
});

以上是property 的proxy, 下面是method proxy 示範

let cat = {
	name: "Kitty",
	method1: function(msg){
		console.log("cat: " + this.name + " say " + msg);
	},
};

var catProxy = new Proxy(cat, {
	get: function(target, propKey, receiver){
		//我只要攔截方法(method calls), 不要屬性(property access)
		var propValue = target[propKey];
		if (typeof propValue != "function"){
			return propValue;
		}
		else{
			return function() {
				console.log("intercepting call to " + propKey + " in cat " + target.name);
				//"this" 指向 proxy, 就像"receiver"
				return propValue.apply(this, arguments);
			}
		}
	}
});

以上Proxy 的概念性, 在沒有ES6 語法之前, 使用Typescript 撰寫 "裝飾者模式" 就可以做到, 如下所示

interface ICat { 
   sayHello(msg: string): void;
}

class RealCat implements ICar {
   public sayHello(msg: string): void {
      console.log('RealCat: ' + msg);
   }
}

class CatProxy implements ICat {
   private _realCat: RealCat;

   constructor(realCat: RealCat) {
      this._realCat = realCat;
   }

   public sayHello(msg: string): void {
      console.log("Hey! I am Kitty");
      this._realCat.sayHello(msg);
   }
}

let catProxy = new CatProxy(new RealCat());
catProxy.sayHello("Hello Proxy World");

Typescript 與 Proxy & Reflect 搭配

使用上述的裝飾者模式時候, 假如 ICat 介面中的方法和屬性加起來有100 個, 你就得在 CatProxy 一一實作包裝並呼叫 _realCat.

但你又是個懶人不想一一手動去實作, 你可以用 Proxy 和 Reflect 來幫忙實作每一個 ICat 的公開方法和屬性, 如下示範Typescript 程式碼是如何做到這件事情

function fakeBaseClass<T>() : new() => Pick<T, keyof T> { 
   return class {} as any;
}

class CatProxy extends fakeBaseClass<RealCat>() { 
   private _realCat: RealCat;
   constructor(cat: RealCat) {
      super();
      this._realCat = cat;
      let handler = {
         get: function(target: CatProxy, prop: keyof RealCat, receiver: any) {
            if(RealCat.prototype[prop] !== null) {
               return target._realCat[prop];
            }
            return Reflect.get(target, prop, receiver);
         }
      };
      return new Proxy(this, handler);
   }
}

如此一來你可以開始只複寫(override) ICat 介面其中一部分的方法或屬性.


尚未有邦友留言

立即登入留言