[譯] 使用 Proxy 來監測 Javascript 中的類

Photo by Fabian Grohs on Unsplashjavascript

Proxy 對象(Proxy)是 ES6 的一個很是酷卻不爲人知的特性。雖然這個特性存在已久,可是我仍是想在本文中對其稍做解釋,並用一個例子說明一下它的用法。前端

什麼是 Proxy

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

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

雖然這是一個不錯的總結,可是我卻並無從中搞清楚 Proxy 能作什麼,以及它能幫咱們實現什麼。ios

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

Proxy API 容許咱們在對象和其消費實體中建立中間層,這種特性爲咱們提供了控制該對象的能力,好比能夠決定怎樣去進行它的 getset,甚至能夠自定義當訪問這個對象上不存在的屬性的時候咱們能夠作些什麼。github

Proxy 的 API

var p = new Proxy(target, handler);
複製代碼

Proxy 構造函數獲取一個 target 對象,和一個用來攔截 target 對象不一樣行爲的 handler 對象。你能夠設置下面這些攔截項:編程

  • has — 攔截 in 操做。好比,你能夠用它來隱藏對象上某些屬性。
  • get — 用來攔截讀取操做。好比當試圖讀取不存在的屬性時,你能夠用它來返回默認值。
  • set — 用來攔截賦值操做。好比給屬性賦值的時候你能夠增長驗證的邏輯,若是驗證不經過能夠拋出錯誤。
  • apply — 用來攔截函數調用操做。好比,你能夠把全部的函數調用都包裹在 try/catch 語句塊中。

這只是一部分攔截項,你能夠在 MDN 上找到完整的列表後端

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

const Car = {
  maker: 'BMW',
  year: 2018,
};

const proxyCar = new Proxy(Car, {
  set(obj, prop, value) {
    if (prop === 'maker' && value.length < 1) {
      throw new Error('Invalid maker');
    }

    if (prop === 'year' && typeof value !== 'number') {
      throw new Error('Invalid year');
    }
    obj[prop] = value;
    return true;
  }

});

proxyCar.maker = ''; // throw exception
proxyCar.year = '1999'; // throw exception
複製代碼

能夠看到,咱們能夠用 Proxy 來驗證賦給被代理對象的值。

使用 Proxy 來調試

爲了在實踐中展現 Proxy 的能力,我建立了一個簡單的監測庫,用來監測給定的對象或類,監測項以下:

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

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

若是你但願監測是誰給一個對象的屬性賦的值,或者一個函數執行了多久、執行了多少次、誰執行的,這個庫將很是有用。我知道可能還有其餘更好的工具來實現上面的功能,可是在這裏我建立這個庫就是爲了用一用這個 API。

使用 proxyTrack

首先,咱們看看怎麼用:

function MyClass() {}

MyClass.prototype = {
    isPrime: function() {
        const num = this.num;
        for(var i = 2; i < num; i++)
            if(num % i === 0) return false;
        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 time
MyClass.num is being get by isPrime for the 1 time
MyClass.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 來實時監控它。好比:

class MyComponent extends Component{...}

export default connect(mapStateToProps)(proxyTrack(MyComponent, {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    filter: (data) => {
        if( data.type === 'get' && data.prop === 'componentDidUpdate') return false;
        return true;
    }
}));
複製代碼

能夠看到,你能夠將你不關心的信息過濾掉,不然輸出將會變得雜亂無章。

實現 proxyTrack

咱們來看看 proxyTrack 的實現。

首先是這個函數自己:

export function 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 = new Proxy(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] = new Proxy(trackedEntity[name], {
                apply: trackFunctionCall(options),
            });
        }
    });
}
複製代碼

能夠看到,假如咱們但願監測對象的屬性,咱們建立了一個帶有 getset 攔截器的被監測對象。下面是 set 攔截器的實現:

function trackPropertySet(options = {}) {
    return function set(target, prop, value, receiver) {
        const { trackCaller, trackCount, stdout, filter } = options;
        const error = trackCaller && new Error();
        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);
            }
        }
        return Reflect.set(target, prop, value, receiver);
    };
}
複製代碼

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

function trackClass(cls, options = {}) {
    cls.prototype = trackObject(cls.prototype, options);
    cls.prototype.constructor = cls;

    return new Proxy(cls, {
        construct(target, args) {
            const obj = new target(...args);
            return new Proxy(obj, {
                get: trackPropertyGet(options),
                set: trackPropertySet(options),
            });
        },
        apply: trackFunctionCall(options),
    });
}
複製代碼

在這個案例中,由於咱們但願攔截這個類上不屬於原型上的屬性,因此咱們給這個類的原型建立了個代理,而且建立了個構造函數攔截器。

別忘了,即便你在原型上定義了一個屬性,但若是你再給這個對象賦值一個同名屬性,JavaScript 將會建立一個這個屬性的本地副本,因此賦值的改動並不會改變這個類其餘實例的行爲。這就是爲什麼只對原型作代理並不能知足要求的緣由。

戳這裏查看完整代碼。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~

另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~

相關文章
相關標籤/搜索