[譯] 實例解析 ES6 Proxy 使用場景

文章永久連接地址:http://pinggod.com/2016/%E5%AE%9E%E4%BE%8B%E8%A7%A3%E6%9E%90-ES6-Proxy-%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF/設計模式

ES6 中的箭頭函數、數組解構、rest 參數等特性一經實現就廣爲流傳,但相似 Proxy 這樣的特性卻不多見到有開發者在使用,一方面在於瀏覽器的兼容性,另外一方面也在於要想發揮這些特性的優點須要開發者深刻地理解其使用場景。就我我的而言是很是喜歡 ES6 的 Proxy,由於它讓咱們以簡潔易懂的方式控制了外部對對象的訪問。在下文中,首先我會介紹 Proxy 的使用方式,而後列舉具體實例解釋 Proxy 的使用場景。api

Proxy,見名知意,其功能很是相似於設計模式中的代理模式,該模式經常使用於三個方面:數組

  • 攔截和監視外部對對象的訪問瀏覽器

  • 下降函數或類的複雜度app

  • 在複雜操做前對操做進行校驗或對所需資源進行管理async

在支持 Proxy 的瀏覽器環境中,Proxy 是一個全局對象,能夠直接使用。Proxy(target, handler) 是一個構造函數,target 是被代理的對象,handlder 是聲明瞭各種代理操做的對象,最終返回一個代理對象。外界每次經過代理對象訪問 target 對象的屬性時,就會通過 handler 對象,從這個流程來看,代理對象很相似 middleware(中間件)。那麼 Proxy 能夠攔截什麼操做呢?最多見的就是 get(讀取)、set(修改)對象屬性等操做,完整的可攔截操做列表請點擊這裏。此外,Proxy 對象還提供了一個 revoke 方法,能夠隨時註銷全部的代理操做。在咱們正式介紹 Proxy 以前,建議你對 Reflect 有必定的瞭解,它也是一個 ES6 新增的全局對象,詳細信息請參考 MDN Reflect函數

Basic

const target = {  
    name: 'Billy Bob',
    age: 15
};

const handler = {  
    get(target, key, proxy) {
        const today = new Date();
        console.log(`GET request made for ${key} at ${today}`);

        return Reflect.get(target, key, proxy);
    }
};

const proxy = new Proxy(target, handler);
proxy.name;
// => "GET request made for name at Thu Jul 21 2016 15:26:20 GMT+0800 (CST)"
// => "Billy Bob"

在上面的代碼中,咱們首先定義了一個被代理的目標對象 target,而後聲明瞭包含全部代理操做的 handler 對象,接下來使用 Proxy(target, handler) 建立代理對象 proxy,此後全部使用 proxytarget 屬性的訪問都會通過 handler 的處理。性能

1. 抽離校驗模塊

讓咱們從一個簡單的類型校驗開始作起,這個示例演示瞭如何使用 Proxy 保障數據類型的準確性:this

let numericDataStore = {  
    count: 0,
    amount: 1234,
    total: 14
};

numericDataStore = new Proxy(numericDataStore, {  
    set(target, key, value, proxy) {
        if (typeof value !== 'number') {
            throw Error("Properties in numericDataStore can only be numbers");
        }
        return Reflect.set(target, key, value, proxy);
    }
});

// 拋出錯誤,由於 "foo" 不是數值
numericDataStore.count = "foo";

// 賦值成功
numericDataStore.count = 333;

若是要直接爲對象的全部屬性開發一個校驗器可能很快就會讓代碼結構變得臃腫,使用 Proxy 則能夠將校驗器從核心邏輯分離出來自成一體:設計

function createValidator(target, validator) {  
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            if (target.hasOwnProperty(key)) {
                let validator = this._validator[key];
                if (!!validator(value)) {
                    return Reflect.set(target, key, value, proxy);
                } else {
                    throw Error(`Cannot set ${key} to ${value}. Invalid.`);
                }
            } else {
                throw Error(`${key} is not a valid property`)
            }
        }
    });
}

const personValidators = {  
    name(val) {
        return typeof val === 'string';
    },
    age(val) {
        return typeof age === 'number' && age > 18;
    }
}
class Person {  
    constructor(name, age) {
        this.name = name;
        this.age = age;
        return createValidator(this, personValidators);
    }
}

const bill = new Person('Bill', 25);

// 如下操做都會報錯
bill.name = 0;  
bill.age = 'Bill';  
bill.age = 15;

經過校驗器和主邏輯的分離,你能夠無限擴展 personValidators 校驗器的內容,而不會對相關的類或函數形成直接破壞。更復雜一點,咱們還可使用 Proxy 模擬類型檢查,檢查函數是否接收了類型和數量都正確的參數:

let obj = {  
    pickyMethodOne: function(obj, str, num) { /* ... */ },
    pickyMethodTwo: function(num, obj) { /*... */ }
};

const argTypes = {  
    pickyMethodOne: ["object", "string", "number"],
    pickyMethodTwo: ["number", "object"]
};

obj = new Proxy(obj, {  
    get: function(target, key, proxy) {
        var value = target[key];
        return function(...args) {
            var checkArgs = argChecker(key, args, argTypes[key]);
            return Reflect.apply(value, target, args);
        };
    }
});

function argChecker(name, args, checkers) {  
    for (var idx = 0; idx < args.length; idx++) {
        var arg = args[idx];
        var type = checkers[idx];
        if (!arg || typeof arg !== type) {
            console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`);
        }
    }
}

obj.pickyMethodOne();  
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 1
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3

obj.pickyMethodTwo("wopdopadoo", {});  
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1

// No warnings logged
obj.pickyMethodOne({}, "a little string", 123);  
obj.pickyMethodOne(123, {});

2. 私有屬性

在 JavaScript 或其餘語言中,你們會約定俗成地在變量名以前添加下劃線 _ 來代表這是一個私有屬性(並非真正的私有),但咱們沒法保證真的沒人會去訪問或修改它。在下面的代碼中,咱們聲明瞭一個私有的 apiKey,便於 api 這個對象內部的方法調用,但不但願從外部也可以訪問 api._apiKey:

var api = {  
    _apiKey: '123abc456def',
    /* mock methods that use this._apiKey */
    getUsers: function(){}, 
    getUser: function(userId){}, 
    setUser: function(userId, config){}
};

// logs '123abc456def';
console.log("An apiKey we want to keep private", api._apiKey);

// get and mutate _apiKeys as desired
var apiKey = api._apiKey;  
api._apiKey = '987654321';

很顯然,約定俗成是沒有束縛力的。使用 ES6 Proxy 咱們就能夠實現真實的私有變量了,下面針對不一樣的讀取方式演示兩個不一樣的私有化方法。第一種方法是使用 set / get 攔截讀寫請求並返回 undefined:

let api = {  
    _apiKey: '123abc456def',
    getUsers: function(){ }, 
    getUser: function(userId){ }, 
    setUser: function(userId, config){ }
};

const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {  
    get(target, key, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, proxy);
    },
    set(target, key, value, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, value, proxy);
    }
});

// 如下操做都會拋出錯誤
console.log(api._apiKey);
api._apiKey = '987654321';

第二種方法是使用 has 攔截 in 操做:

var api = {  
    _apiKey: '123abc456def',
    getUsers: function(){ }, 
    getUser: function(userId){ }, 
    setUser: function(userId, config){ }
};

const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {  
    has(target, key) {
        return (RESTRICTED.indexOf(key) > -1) ?
            false :
            Reflect.has(target, key);
    }
});

// these log false, and `for in` iterators will ignore _apiKey
console.log("_apiKey" in api);

for (var key in api) {  
    if (api.hasOwnProperty(key) && key === "_apiKey") {
        console.log("This will never be logged because the proxy obscures _apiKey...")
    }
}

3. 訪問日誌

對於那些調用頻繁、運行緩慢或佔用執行環境資源較多的屬性或接口,開發者會但願記錄它們的使用狀況或性能表現,這個時候就可使用 Proxy 充當中間件的角色,垂手可得實現日誌功能:

let api = {  
    _apiKey: '123abc456def',
    getUsers: function() { /* ... */ },
    getUser: function(userId) { /* ... */ },
    setUser: function(userId, config) { /* ... */ }
};

function logMethodAsync(timestamp, method) {  
    setTimeout(function() {
        console.log(`${timestamp} - Logging ${method} request asynchronously.`);
    }, 0)
}

api = new Proxy(api, {  
    get: function(target, key, proxy) {
        var value = target[key];
        return function(...arguments) {
            logMethodAsync(new Date(), key);
            return Reflect.apply(value, target, arguments);
        };
    }
});

api.getUsers();

4. 預警和攔截

假設你不想讓其餘開發者刪除 noDelete 屬性,還想讓調用 oldMethod 的開發者瞭解到這個方法已經被廢棄了,或者告訴開發者不要修改 doNotChange 屬性,那麼就可使用 Proxy 來實現:

let dataStore = {  
    noDelete: 1235,
    oldMethod: function() {/*...*/ },
    doNotChange: "tried and true"
};

const NODELETE = ['noDelete'];  
const NOCHANGE = ['doNotChange'];
const DEPRECATED = ['oldMethod'];  

dataStore = new Proxy(dataStore, {  
    set(target, key, value, proxy) {
        if (NOCHANGE.includes(key)) {
            throw Error(`Error! ${key} is immutable.`);
        }
        return Reflect.set(target, key, value, proxy);
    },
    deleteProperty(target, key) {
        if (NODELETE.includes(key)) {
            throw Error(`Error! ${key} cannot be deleted.`);
        }
        return Reflect.deleteProperty(target, key);

    },
    get(target, key, proxy) {
        if (DEPRECATED.includes(key)) {
            console.warn(`Warning! ${key} is deprecated.`);
        }
        var val = target[key];

        return typeof val === 'function' ?
            function(...args) {
                Reflect.apply(target[key], target, args);
            } :
            val;
    }
});

// these will throw errors or log warnings, respectively
dataStore.doNotChange = "foo";  
delete dataStore.noDelete;  
dataStore.oldMethod();

5. 過濾操做

某些操做會很是佔用資源,好比傳輸大文件,這個時候若是文件已經在分塊發送了,就不須要在對新的請求做出相應(非絕對),這個時候就可使用 Proxy 對當請求進行特徵檢測,並根據特徵過濾出哪些是不須要響應的,哪些是須要響應的。下面的代碼簡單演示了過濾特徵的方式,並非完整代碼,相信你們會理解其中的妙處:

let obj = {  
    getGiantFile: function(fileId) {/*...*/ }
};

obj = new Proxy(obj, {  
    get(target, key, proxy) {
        return function(...args) {
            const id = args[0];
            let isEnroute = checkEnroute(id);
            let isDownloading = checkStatus(id);      
            let cached = getCached(id);

            if (isEnroute || isDownloading) {
                return false;
            }
            if (cached) {
                return cached;
            }
            return Reflect.apply(target[key], target, args);
        }
    }
});

6. 中斷代理

Proxy 支持隨時取消對 target 的代理,這一操做經常使用於徹底封閉對數據或接口的訪問。在下面的示例中,咱們使用了 Proxy.revocable 方法建立了可撤銷代理的代理對象:

let sensitiveData = { username: 'devbryce' };
const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);
function handleSuspectedHack(){  
    revokeAccess();
}

// logs 'devbryce'
console.log(sensitiveData.username);
handleSuspectedHack();
// TypeError: Revoked
console.log(sensitiveData.username);

Decorator

ES7 中實現的 Decorator,至關於設計模式中的裝飾器模式。若是簡單地區分 Proxy 和 Decorator 的使用場景,能夠歸納爲:Proxy 的核心做用是控制外界對被代理者內部的訪問,Decorator 的核心做用是加強被裝飾者的功能。只要在它們核心的使用場景上作好區別,那麼像是訪問日誌這樣的功能,雖然本文使用了 Proxy 實現,但也可使用 Decorator 實現,開發者能夠根據項目的需求、團隊的規範、本身的偏好自由選擇。

相關文章
相關標籤/搜索