Proxy 來代理 JavaScript 裡的類

Proxy 對象(Proxy)是 ES6 的一個非常酷卻鮮為人知的特性。雖然這個特性存在已久,但是我還是想在本文中對其稍作解釋,並用一個例子說明一下它的用法。

什麼是 Proxy

正如 MDN 上簡單而枯燥的定義:

Proxy 對象用於定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數調用等)。

雖然這是一個不錯的總結,但是我卻並沒有從中搞清楚 Proxy 能做什麼,以及它能幫我們實現什麼。

首先,Proxy 的概念來源於元編程。簡單的說,元編程是允許我們運行我們編寫的應用程序(或核心)代碼的代碼。例如,臭名昭著的 eval 函數允許我們將字符串代碼當做可執行代碼來執行,它是就屬於元編程領域。

Proxy API 允許我們在對象和其消費實體中創建中間層,這種特性為我們提供了控制該對象的能力,比如可以決定怎樣去進行它的 get 和 set,甚至可以自定義當訪問這個對象上不存在的屬性的時候我們可以做些什麼。

Proxy 的 API

var p = newProxy(target, handler);

Proxy 構造函數獲取一個 target 對象,和一個用來攔截 target 對象不同行為的 handler 對象。你可以設置下面這些攔截項:

  • has — 攔截 in 操作。比如,你可以用它來隱藏對象上某些屬性。
  • get — 用來攔截讀取操作。比如當試圖讀取不存在的屬性時,你可以用它來返回默認值。
  • set — 用來攔截賦值操作。比如給屬性賦值的時候你可以增加驗證的邏輯,如果驗證不通過可以拋出錯誤。
  • apply — 用來攔截函數調用操作。比如,你可以把所有的函數調用都包裹在 try/catch 語句塊中。

這只是一部分攔截項,你可以在 MDN 上找到完整的列表。

下面是將 Proxy 用在驗證上的一個簡單的例子:

constCar= { maker: 'BMW', year: 2018,};const proxyCar = newProxy(Car, {set(obj, prop, value) {if(prop === 'maker'&& value.length < 1) {thrownewError('Invalid maker');}
if(prop === 'year'&& typeof value !== 'number') {thrownewError('Invalid year');} obj[prop] = value;returntrue;}
});
proxyCar.maker = ''; // throw exceptionproxyCar.year = '1999'; // throw exception

可以看到,我們可以用 Proxy 來驗證賦給被代理對象的值。

使用 Proxy 來調試

為了在實踐中展示 Proxy 的能力,我創建了一個簡單的監測庫,用來監測給定的對象或類,監測項如下:

  • 函數執行時間
  • 函數的調用者或屬性的訪問者
  • 統計每個函數或屬性的被訪問次數。

這是通過在訪問任意對象、類、甚至是函數時,調用一個名為 proxyTrack 的函數來完成的。

如果你希望監測是誰給一個對象的屬性賦的值,或者一個函數執行了多久、執行了多少次、誰執行的,這個庫將非常有用。我知道可能還有其他更好的工具來實現上面的功能,但是在這裡我創建這個庫就是為了用一用這個 API。

使用 proxyTrack

首先,我們看看怎麼用:

functionMyClass() {}MyClass.prototype = { isPrime: function() {const num = this.num;for(var i = 2; i < num; i++)if(num % i === 0) returnfalse;return num !== 1&& num !== 0;},
num: null,};
MyClass.prototype.constructor = MyClass;

const trackedClass = proxyTrack(MyClass);
function start() {const my = new trackedClass(); my.num = 573723653;if(!my.isPrime()) {return`${my.num} is not prime`;}}
function main() { start();}
main();

如果我們運行這段代碼,控制檯將會輸出:

MyClass.num is being set by start for the 1 timeMyClass.num is being get by isPrime for the 1 timeMyClass.isPrime was called by start for the 1 time and took 0 mils.MyClass.num is being get by start for the 2 time

proxyTrack 接受 2 個參數:第一個是要監測的對象/類,第二個是一個配置項對象,如果沒傳遞的話將被置為默認值。我們看看這個配置項默認值長啥樣:

const defaultOptions = { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, stdout: null, filter: null,};

可以看到,你可以通過配置你關心的監測項來監測你的目標。比如你希望將結果輸出出來,那麼你可以將 console.log 賦給 stdout。

還可以通過賦給 filter 的回調函數來自定義地控制輸出哪些信息。你將會得到一個包括有監測信息的對象,並且如果你希望保留這個信息就返回 true,反之返回 false。

在 React 中使用 proxyTrack

因為 React 的組件實際上也是類,所以你可以通過 proxyTrack 來實時監控它。比如:

classMyComponent extends Component{...}
exportdefault connect(mapStateToProps)(proxyTrack(MyComponent, { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, filter: (data) => {if( data.type === 'get'&& data.prop === 'componentDidUpdate') returnfalse;returntrue;}}));

可以看到,你可以將你不關心的信息過濾掉,否則輸出將會變得雜亂無章。

實現 proxyTrack

我們來看看 proxyTrack 的實現。

首先是這個函數本身:

exportfunction proxyTrack(entity, options = defaultOptions) {if(typeof entity === 'function') return trackClass(entity, options);return trackObject(entity, options);}

沒什麼特別的嘛,這裡只是調用相關函數。

再看看 trackObject:

function trackObject(obj, options = {}) {const{ trackFunctions, trackProps } = options;
let resultObj = obj;if(trackFunctions) { proxyFunctions(resultObj, options);}if(trackProps) { resultObj = newProxy(resultObj, {get: trackPropertyGet(options),set: trackPropertySet(options),});}return resultObj;}function proxyFunctions(trackedEntity, options) {if(typeof trackedEntity === 'function') return;Object.getOwnPropertyNames(trackedEntity).forEach((name) => {if(typeof trackedEntity[name] === 'function') { trackedEntity[name] = newProxy(trackedEntity[name], { apply: trackFunctionCall(options),});}});}

可以看到,假如我們希望監測對象的屬性,我們創建了一個帶有 get 和 set 攔截器的被監測對象。下面是 set 攔截器的實現:

function trackPropertySet(options = {}) {returnfunctionset(target, prop, value, receiver) {const{ trackCaller, trackCount, stdout, filter } = options;const error = trackCaller && newError();const caller = getCaller(error);const contextName = target.constructor.name === 'Object'? '': `${target.constructor.name}.`;const name = `${contextName}${prop}`;const hashKey = `set_${name}`;if(trackCount) {if(!callerMap[hashKey]) { callerMap[hashKey] = 1;} else{ callerMap[hashKey]++;}} let output = `${name} is being set`;if(trackCaller) { output += ` by ${caller.name}`;}if(trackCount) { output += ` for the ${callerMap[hashKey]} time`;} let canReport = true;if(filter) { canReport = filter({ type: 'get', prop, name, caller, count: callerMap[hashKey], value,});}if(canReport) {if(stdout) { stdout(output);} else{ console.log(output);}}returnReflect.set(target, prop, value, receiver);};}

更有趣的是 trackClass 函數(至少對我來說是這樣):

function trackClass(cls, options = {}) { cls.prototype = trackObject(cls.prototype, options); cls.prototype.constructor = cls;
returnnewProxy(cls, { construct(target, args) {const obj = new target(...args);returnnewProxy(obj, {get: trackPropertyGet(options),set: trackPropertySet(options),});}, apply: trackFunctionCall(options),});}

在這個案例中,因為我們希望攔截這個類上不屬於原型上的屬性,所以我們給這個類的原型創建了個代理,並且創建了個構造函數攔截器。

別忘了,即使你在原型上定義了一個屬性,但如果你再給這個對象賦值一個同名屬性,JavaScript 將會創建一個這個屬性的本地副本,所以賦值的改動並不會改變這個類其他實例的行為。這就是為何只對原型做代理並不能滿足要求的原因。


分享到:


相關文章: