ES6系列之Proxy

原文鏈接 http://blog.poetries.top/2018/12/21/es6-proxy/

關(guān)注公眾號獲取更多資訊

一环础、proxy概述

Proxy的兼容性

image.png

proxy在目標(biāo)對象的外層搭建了一層攔截,外界對目標(biāo)對象的某些操作载慈,必須通過這層攔截

var proxy = new Proxy(target, handler);

new Proxy()表示生成一個Proxy實例缸匪,target參數(shù)表示所要攔截的目標(biāo)對象,handler參數(shù)也是一個對象准夷,用來定制攔截行為

var target = {
   name: 'poetries'
 };
 var logHandler = {
   get: function(target, key) {
     console.log(`${key} 被讀取`);
     return target[key];
   },
   set: function(target, key, value) {
     console.log(`${key} 被設(shè)置為 ${value}`);
     target[key] = value;
   }
 }
 var targetWithLog = new Proxy(target, logHandler);
 
 targetWithLog.name; // 控制臺輸出:name 被讀取
 targetWithLog.name = 'others'; // 控制臺輸出:name 被設(shè)置為 others
 
 console.log(target.name); // 控制臺輸出: others
  • targetWithLog 讀取屬性的值時钥飞,實際上執(zhí)行的是 logHandler.get :在控制臺輸出信息,并且讀取被代理對象 target 的屬性衫嵌。
  • targetWithLog 設(shè)置屬性值時读宙,實際上執(zhí)行的是 logHandler.set :在控制臺輸出信息,并且設(shè)置被代理對象 target 的屬性的值
// 由于攔截函數(shù)總是返回35楔绞,所以訪問任何屬性都得到35
var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

Proxy 實例也可以作為其他對象的原型對象

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

proxy對象是obj對象的原型结闸,obj對象本身并沒有time屬性,所以根據(jù)原型鏈酒朵,會在proxy對象上讀取該屬性桦锄,導(dǎo)致被攔截

Proxy的作用

對于代理模式 Proxy 的作用主要體現(xiàn)在三個方面

  • 攔截和監(jiān)視外部對對象的訪問
  • 降低函數(shù)或類的復(fù)雜度
  • 在復(fù)雜操作前對操作進(jìn)行校驗或?qū)λ栀Y源進(jìn)行管理

二、Proxy所能代理的范圍--handler

實際上 handler 本身就是ES6所新設(shè)計的一個對象.它的作用就是用來 自定義代理對象的各種可代理操作 耻讽。它本身一共有13中方法,每種方法都可以代理一種操作.其13種方法如下

// 在讀取代理對象的原型時觸發(fā)該操作察纯,比如在執(zhí)行 Object.getPrototypeOf(proxy) 時。
handler.getPrototypeOf()

// 在設(shè)置代理對象的原型時觸發(fā)該操作,比如在執(zhí)行 Object.setPrototypeOf(proxy, null) 時饼记。
handler.setPrototypeOf()

 
// 在判斷一個代理對象是否是可擴展時觸發(fā)該操作香伴,比如在執(zhí)行 Object.isExtensible(proxy) 時。
handler.isExtensible()

 
// 在讓一個代理對象不可擴展時觸發(fā)該操作具则,比如在執(zhí)行 Object.preventExtensions(proxy) 時即纲。
handler.preventExtensions()

// 在獲取代理對象某個屬性的屬性描述時觸發(fā)該操作,比如在執(zhí)行 Object.getOwnPropertyDescriptor(proxy, "foo") 時博肋。
handler.getOwnPropertyDescriptor()

 
// 在定義代理對象某個屬性時的屬性描述時觸發(fā)該操作低斋,比如在執(zhí)行 Object.defineProperty(proxy, "foo", {}) 時。
andler.defineProperty()

 
// 在判斷代理對象是否擁有某個屬性時觸發(fā)該操作匪凡,比如在執(zhí)行 "foo" in proxy 時膊畴。
handler.has()

// 在讀取代理對象的某個屬性時觸發(fā)該操作,比如在執(zhí)行 proxy.foo 時病游。
handler.get()

 
// 在給代理對象的某個屬性賦值時觸發(fā)該操作唇跨,比如在執(zhí)行 proxy.foo = 1 時。
handler.set()

// 在刪除代理對象的某個屬性時觸發(fā)該操作衬衬,比如在執(zhí)行 delete proxy.foo 時买猖。
handler.deleteProperty()

// 在獲取代理對象的所有屬性鍵時觸發(fā)該操作,比如在執(zhí)行 Object.getOwnPropertyNames(proxy) 時滋尉。
handler.ownKeys()

// 在調(diào)用一個目標(biāo)對象為函數(shù)的代理對象時觸發(fā)該操作玉控,比如在執(zhí)行 proxy() 時。
handler.apply()

 
// 在給一個目標(biāo)對象為構(gòu)造函數(shù)的代理對象構(gòu)造實例時觸發(fā)該操作狮惜,比如在執(zhí)行new proxy() 時高诺。
handler.construct()

三、Proxy場景

3.1 實現(xiàn)私有變量

var target = {
   name: 'poetries',
   _age: 22
}

var logHandler = {
  get: function(target,key){
    if(key.startsWith('_')){
      console.log('私有變量age不能被訪問')
      return false
    }
    return target[key];
  },
  set: function(target, key, value) {
     if(key.startsWith('_')){
      console.log('私有變量age不能被修改')
      return false
    }
     target[key] = value;
   }
} 
var targetWithLog = new Proxy(target, logHandler);
 
// 私有變量age不能被訪問
targetWithLog.name; 
 
// 私有變量age不能被修改
targetWithLog.name = 'others'; 

在下面的代碼中讽挟,我們聲明了一個私有的 apiKey懒叛,便于 api 這個對象內(nèi)部的方法調(diào)用,但不希望從外部也能夠訪問 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 我們就可以實現(xiàn)真實的私有變量了胖烛,下面針對不同的讀取方式演示兩個不同的私有化方法眼姐。第一種方法是使用 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.2 抽離校驗?zāi)K

讓我們從一個簡單的類型校驗開始做起,這個示例演示了如何使用 Proxy 保障數(shù)據(jù)類型的準(zhǔn)確性

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" 不是數(shù)值
numericDataStore.count = "foo";

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

如果要直接為對象的所有屬性開發(fā)一個校驗器可能很快就會讓代碼結(jié)構(gòu)變得臃腫众旗,使用 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' && val > 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 校驗器的內(nèi)容趟畏,而不會對相關(guān)的類或函數(shù)造成直接破壞贡歧。更復(fù)雜一點,我們還可以使用 Proxy 模擬類型檢查,檢查函數(shù)是否接收了類型和數(shù)量都正確的參數(shù)

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, {});

3.3 訪問日志

對于那些調(diào)用頻繁利朵、運行緩慢或占用執(zhí)行環(huán)境資源較多的屬性或接口律想,開發(fā)者會希望記錄它們的使用情況或性能表現(xiàn),這個時候就可以使用 Proxy 充當(dāng)中間件的角色绍弟,輕而易舉實現(xiàn)日志功能

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();

3.4 預(yù)警和攔截

假設(shè)你不想讓其他開發(fā)者刪除 noDelete 屬性技即,還想讓調(diào)用 oldMethod 的開發(fā)者了解到這個方法已經(jīng)被廢棄了,或者告訴開發(fā)者不要修改 doNotChange 屬性樟遣,那么就可以使用 Proxy 來實現(xiàn)

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();

3.5 過濾操作

某些操作會非常占用資源而叼,比如傳輸大文件,這個時候如果文件已經(jīng)在分塊發(fā)送了豹悬,就不需要在對新的請求作出相應(yīng)(非絕對)葵陵,這個時候就可以使用 Proxy 對當(dāng)請求進(jìn)行特征檢測,并根據(jù)特征過濾出哪些是不需要響應(yīng)的瞻佛,哪些是需要響應(yīng)的埃难。下面的代碼簡單演示了過濾特征的方式,并不是完整代碼涤久,相信大家會理解其中的妙處

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);
        }
    }
});

3.6 中斷代理

Proxy 支持隨時取消對 target 的代理涡尘,這一操作常用于完全封閉對數(shù)據(jù)或接口的訪問。在下面的示例中响迂,我們使用了 Proxy.revocable 方法創(chuàng)建了可撤銷代理的代理對象:

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);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末考抄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蔗彤,更是在濱河造成了極大的恐慌川梅,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件然遏,死亡現(xiàn)場離奇詭異贫途,居然都是意外死亡,警方通過查閱死者的電腦和手機待侵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門丢早,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人秧倾,你說我怎么就攤上這事怨酝。” “怎么了那先?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵农猬,是天一觀的道長。 經(jīng)常有香客問我售淡,道長斤葱,這世上最難降的妖魔是什么慷垮? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮揍堕,結(jié)果婚禮上料身,老公的妹妹穿的比我還像新娘。我一直安慰自己鹤啡,他們只是感情好惯驼,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著递瑰,像睡著了一般祟牲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抖部,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天说贝,我揣著相機與錄音,去河邊找鬼慎颗。 笑死乡恕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的俯萎。 我是一名探鬼主播傲宜,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼夫啊!你這毒婦竟也來了函卒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤撇眯,失蹤者是張志新(化名)和其女友劉穎报嵌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體熊榛,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡锚国,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了玄坦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片血筑。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖营搅,靈堂內(nèi)的尸體忽然破棺而出云挟,到底是詐尸還是另有隱情,我是刑警寧澤转质,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站帖世,受9級特大地震影響休蟹,放射性物質(zhì)發(fā)生泄漏沸枯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一赂弓、第九天 我趴在偏房一處隱蔽的房頂上張望绑榴。 院中可真熱鬧,春花似錦盈魁、人聲如沸翔怎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赤套。三九已至,卻和暖如春珊膜,著一層夾襖步出監(jiān)牢的瞬間容握,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工车柠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留剔氏,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓竹祷,卻偏偏與公主長得像谈跛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子塑陵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容