Typescript 与 Javascript Proxy 和 Reflect 搭配
2019-10-15
Javascript 的 Proxy 物件 & Reflect API
下面表格是 Proxy 物件在各种浏览器版本中的支援程度, 资料参考来源从 caniuse 网站取得
以上版本号码有删除的样式, 表示不支援
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 介面其中一部分的方法或属性.