JavaScript 設(shè)計模式(三):代理模式

代理模式

代理模式:為一個對象提供一個代用品或占位符蒲每,以便控制它的訪問叨咖。

當(dāng)我們不方便直接訪問某個對象時,或不滿足需求時嘿期,可考慮使用一個替身對象來控制該對象的訪問。替身對象可對請求預(yù)先進行處理埋合,再決定是否轉(zhuǎn)交給本體對象备徐。

生活小栗子:

  1. 代購;
  2. 明星經(jīng)紀(jì)人甚颂;
  3. 和諧上網(wǎng)

經(jīng)常 shopping 的同學(xué)蜜猾,對代購應(yīng)該不陌生。自己不方便直接購買或買不到某件商品時振诬,會選擇委托給第三方蹭睡,讓代購或黃牛去做購買動作。程序世界的代理者也是如此赶么,我們不直接操作原有對象肩豁,而是委托代理者去進行。代理者的作用辫呻,就是對我們的請求預(yù)先進行處理或轉(zhuǎn)接給實際對象清钥。

模式特點

  1. 代理對象可預(yù)先處理請求,再決定是否轉(zhuǎn)交給本體放闺;
  2. 代理和本體對外顯示接口保持一致性
  3. 代理對象僅對本體做一次包裝

模式細分

  1. 虛擬代理(將開銷大的運算延遲到需要時執(zhí)行)
  2. 緩存代理(為開銷大的運算結(jié)果提供緩存)
  3. 保護代理(黑白雙簧祟昭,代理充當(dāng)黑臉,攔截非分要求)
  4. 防火墻代理(控制網(wǎng)絡(luò)資源的訪問)
  5. 遠程代理(為一個對象在不同的地址控件提供局部代表)
  6. 智能引用代理(訪問對象執(zhí)行一些附加操作)
  7. 寫時復(fù)制代理(延遲對象復(fù)制過程怖侦,對象需要真正修改時才進行)

JavaScript 中常用的代理模式為 “虛擬代理” 和 “緩存代理”篡悟。

模式實現(xiàn)

實現(xiàn)方式:創(chuàng)建一個代理對象谜叹,代理對象可預(yù)先對請求進行處理,再決定是否轉(zhuǎn)交給本體搬葬,代理和本體對外接口保持一致性(接口名相同)荷腊。

// 例子:代理接聽電話,實現(xiàn)攔截黑名單
var backPhoneList = ['189XXXXX140'];       // 黑名單列表
// 代理
var ProxyAcceptPhone = function(phone) {
    // 預(yù)處理
    console.log('電話正在接入...');
    if (backPhoneList.includes(phone)) {
        // 屏蔽
        console.log('屏蔽黑名單電話');
    } else {
        // 轉(zhuǎn)接
        AcceptPhone.call(this, phone);
    }
}
// 本體
var AcceptPhone = function(phone) {
    console.log('接聽電話:', phone);
};

// 外部調(diào)用代理
ProxyAcceptPhone('189XXXXX140'); 
ProxyAcceptPhone('189XXXXX141'); 

代理并不會改變本體對象急凰,遵循 “單一職責(zé)原則”停局,即 “自掃門前雪,各找各家”香府。不同對象承擔(dān)獨立職責(zé)董栽,不過于緊密耦合,具體執(zhí)行功能還是本體對象企孩,只是引入代理可以選擇性地預(yù)先處理請求锭碳。例如上述代碼中,我們向 “接聽電話功能” 本體添加了一個屏蔽黑名單的功能(保護代理)勿璃,預(yù)先處理電話接入請求擒抛。

虛擬代理(延遲執(zhí)行)

虛擬代理的目的,是將開銷大的運算延遲到需要時再執(zhí)行补疑。

虛擬代理在圖片預(yù)加載的應(yīng)用歧沪,代碼例子來至 《JavaScript 設(shè)計模式與開發(fā)實踐》

// 本體
var myImage = (function(){
    var imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})();

// 代理
var proxyImage = (function(){
    var img = new Image;
    img.onload = function() {
        myImage.setSrc(this.src);             // 圖片加載完設(shè)置真實圖片src
    }
    return {
        setSrc: function(src) {
            myImage.setSrc('./loading.gif');  // 預(yù)先設(shè)置圖片src為loading圖
            img.src = src;
        }
    }
})();

// 外部調(diào)用
proxyImage.setSrc('./product.png');           // 有l(wèi)oading圖的圖片預(yù)加載效果

緩存代理(暫時存儲)

緩存代理的目的,是為一些開銷大的運算結(jié)果提供暫時存儲莲组,以便下次調(diào)用時诊胞,參數(shù)與結(jié)果不變情況下,從緩存返回結(jié)果锹杈,而不是重新進行本體運算撵孤,減少本體調(diào)用次數(shù)。

應(yīng)用緩存代理的本體竭望,要求運算函數(shù)應(yīng)是一個純函數(shù)邪码,簡單理解比如一個求和函數(shù) sum, 輸入?yún)?shù) (1, 1), 得到的結(jié)果應(yīng)該永遠是 2咬清。

純函數(shù):固定的輸入闭专,有固定的輸出,不影響外部數(shù)據(jù)旧烧。

模擬場景:60道判斷題測試影钉,每三道題計分一次,根據(jù)計分篩選下一步的三道題目?

三道判斷題得分結(jié)果:

  1. (0, 0 ,0)
  2. (0, 0, 1)
  3. (0, 1, 0)
  4. (0, 1, 1)
  5. (1, 0, 0)
  6. (1, 0, 1)
  7. (1, 1, 0)
  8. (1, 1, 1)

總共七種計分結(jié)果粪滤。60/3 = 20斧拍,共進行 20 次計分,每次計分執(zhí)行 3 個循環(huán)累計杖小,共 60 個循環(huán)肆汹。接下來,借用 “緩存代理” 方式予权,來實現(xiàn)最少本體運算次數(shù)昂勉。

// 本體:對三道題答案進行計分
var countScore = function(ansList) {
    let [a, b, c] = ansList;
    return a + b + c;
}

// 代理:對計分請求預(yù)先處理
var proxyCountScore = (function() {
    var existScore = {};    // 設(shè)定存儲對象
    return function(ansList) {
        var attr = ansList.join(',');  // eg. ['0,0,0']
        if (existScore[attr] != null) {
            // 從內(nèi)存返回
            return existScore[attr];
        } else {
            // 內(nèi)存不存在,轉(zhuǎn)交本體計算并存入內(nèi)存
            return existScore[attr] = countScore(ansList);
        }
    }
})();

// 調(diào)用計分
proxyCountScore([0,1,0]);

60 道題目扫腺,每 3 道題一次計分岗照,共 20 次計分運算,但總的計分結(jié)果只有 7 種笆环,那么實際上本體 countScore() 最多只需運算 7 次攒至,即可囊括所有計算結(jié)果。

通過緩存代理的方式躁劣,對計分結(jié)果進行臨時存儲迫吐。用答案字符串組成屬性名 ['0,1,0'] 作為 key 值檢索內(nèi)存,若存在直接從內(nèi)存返回账忘,減少包含復(fù)雜運算的本體被調(diào)用的次數(shù)志膀。之后如果我們的題目增加至 90 道, 120 道鳖擒,150 道題時溉浙,本體 countScore() 運算次數(shù)仍舊保持 7 次,中間節(jié)省了復(fù)雜運算的開銷蒋荚。

ES6 的 Proxy

ES6新增的 Proxy 代理對象的操作戳稽,具體的實現(xiàn)方式是在 handler 上定義對象自定義方法集合,以便預(yù)先管控對象的操作期升。

ES6 的 Proxy語法:let proxyObj = new Proxy(target, handler);

  • target: 本體广鳍,要代理的對象
  • handler: 自定義操作方法集合
  • proxyObj: 返回的代理對象,擁有本體的方法吓妆,不過會被 handler 預(yù)處理
// ES6的Proxy
let Person = {
    name: '以樂之名'
};

const ProxyPerson = new Proxy(Person, {
    get(target, key, value) {
        if (key != 'age') {
            return target[key];
        } else {
            return '保密'
        }
    },
    set(target, key, value) {
        if (key === 'rate') {
            target[key] = value === 'A' ? '推薦' : '待提高'
        }
    }
})

console.log(ProxyPerson.name);  // '以樂之名'
console.log(ProxyPerson.age);   // '保密'
ProxyPerson.rate = 'A';         
console.log(ProxyPerson.rate);  // '推薦'
ProxyPerson.rate = 'B';         
console.log(ProxyPerson.rate);  // '待提高'

handler 除常用的 set/get赊时,總共支持 13 種方法:

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

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

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

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

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

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

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

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

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

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

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

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

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

適用場景

  • 虛擬代理:
    1. 圖片預(yù)加載(loading 圖)
    2. 合并HTTP請求(數(shù)據(jù)上報匯總)
  • 緩存代理:(前提本體是純函數(shù))
    1. 緩存異步請求數(shù)據(jù)
    2. 緩存較復(fù)雜的運算結(jié)果
  • ES6 的 Proxy:
    1. 實現(xiàn)對象私有屬性
    2. 實現(xiàn)表單驗證

“策略模式” 可應(yīng)用于表單驗證信息坛猪,“代理方式” 也可實現(xiàn)脖阵。這里引用 Github - jawil 的一個例子,思路供大家分享墅茉。

// 利用 proxy 攔截格式不符數(shù)據(jù)
function validator(target, validator, errorMsg) {
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            let errMsg = errorMsg;
            if (value == null || !value.length) {
                console.log(`${errMsg[key]} 不能為空`);
                return target[key] = false;
            }
            let va = this._validator[key];  // 這里有策略模式的應(yīng)用
            if (!!va(value)) {
                return Reflect.set(target, key, value, proxy);
            } else {
                console.log(`${errMsg[key]} 格式不正確`);
                return target[key] = false;
            }
        }
    })
}

// 負(fù)責(zé)校驗的邏輯代碼
const validators = {
    name(value) {
        return value.length >= 6;
    },
    passwd(value) {
        return value.length >= 6;
    },
    moblie(value) {
        return /^1(3|5|7|8|9)[0-9]{9}$/.test(value);
    },
    email(value) {
        return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value)
    }
}

// 調(diào)用代碼
const errorMsg = {
    name: '用戶名',
    passwd: '密碼',
    moblie: '手機號碼',
    email: '郵箱地址'
}
const vali = validator({}, validators, errorMsg)
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener('submit', function () {
    let validatorNext = function* () {
        yield vali.name = registerForm.userName.value
        yield vali.passwd = registerForm.passWord.value
        yield vali.moblie = registerForm.phone.value
        yield vali.email = registerForm.email.value
    }
    let validator = validatorNext();
    for (let field of validator) {
        validator.next();
    }
}

實現(xiàn)思路: 利用 ES6 的 proxy 自定義 handlerset() 命黔,進行表單校驗并返回結(jié)果,并且借用 “策略模式" 獨立封裝驗證邏輯就斤。使得表單對象悍募,驗證邏輯,驗證器各自獨立洋机。代碼整潔性坠宴,維護性及復(fù)用性都得到增強。

關(guān)于 “設(shè)計模式” 在表單驗證的應(yīng)用槐秧,可參考 jawil 原文:《探索兩種優(yōu)雅的表單驗證——策略設(shè)計模式和ES6的Proxy代理模式》啄踊。

優(yōu)缺點

  • 優(yōu)點:
    1. 可攔截和監(jiān)聽外部對本體對象的訪問;
    2. 復(fù)雜運算前可以進行校驗或資源管理刁标;
    3. 對象職能粒度細分颠通,函數(shù)功能復(fù)雜度降低,符合 “單一職責(zé)原則”膀懈;
    4. 依托代理顿锰,可額外添加擴展功能,而不修改本體對象启搂,符合 “開發(fā)-封閉原則”
  • 缺點:
    1. 額外代理對象的創(chuàng)建硼控,增加部分內(nèi)存開銷;
    2. 處理請求速度可能有差別胳赌,非直接訪問存在開銷牢撼,但 “虛擬代理” 及 “緩存代理” 均能提升性能

參考文章

本文首發(fā)Github,期待Star疑苫!
https://github.com/ZengLingYong/blog

作者:以樂之名
本文原創(chuàng)熏版,有不當(dāng)?shù)牡胤綒g迎指出。轉(zhuǎn)載請指明出處捍掺。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末撼短,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子挺勿,更是在濱河造成了極大的恐慌曲横,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件不瓶,死亡現(xiàn)場離奇詭異禾嫉,居然都是意外死亡灾杰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門夭织,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吭露,“玉大人吠撮,你說我怎么就攤上這事尊惰。” “怎么了泥兰?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵弄屡,是天一觀的道長。 經(jīng)常有香客問我鞋诗,道長膀捷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任削彬,我火速辦了婚禮全庸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘融痛。我一直安慰自己壶笼,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布雁刷。 她就那樣靜靜地躺著覆劈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沛励。 梳的紋絲不亂的頭發(fā)上责语,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音目派,去河邊找鬼坤候。 笑死,一個胖子當(dāng)著我的面吹牛企蹭,可吹牛的內(nèi)容都是我干的白筹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼练对,長吁一口氣:“原來是場噩夢啊……” “哼遍蟋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起螟凭,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤虚青,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后螺男,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棒厘,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡纵穿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奢人。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谓媒。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖何乎,靈堂內(nèi)的尸體忽然破棺而出句惯,到底是詐尸還是另有隱情,我是刑警寧澤支救,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布抢野,位于F島的核電站,受9級特大地震影響各墨,放射性物質(zhì)發(fā)生泄漏指孤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一贬堵、第九天 我趴在偏房一處隱蔽的房頂上張望恃轩。 院中可真熱鬧,春花似錦黎做、人聲如沸叉跛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昧互。三九已至,卻和暖如春伟桅,著一層夾襖步出監(jiān)牢的瞬間敞掘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工楣铁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留玖雁,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓盖腕,卻偏偏與公主長得像赫冬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子溃列,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

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

  • 代理模式劲厌,顧名思義,就是A要對C做一件事情听隐,讓B幫忙做(怎么聽起來怪怪的)补鼻。 下面寫幾個常見的使用代理模式的例子 ...
    Sccong閱讀 197評論 0 0
  • 工廠模式類似于現(xiàn)實生活中的工廠可以產(chǎn)生大量相似的商品,去做同樣的事情,實現(xiàn)同樣的效果;這時候需要使用工廠模式风范。簡單...
    舟漁行舟閱讀 7,718評論 2 17
  • javascript設(shè)計模式與開發(fā)實踐 設(shè)計模式 每個設(shè)計模式我們需要從三點問題入手: 定義 作用 用法與實現(xiàn) 單...
    穿牛仔褲的蚊子閱讀 4,036評論 0 13
  • 1. 設(shè)計模式概述 簡介在面向?qū)ο筌浖O(shè)計過程中針對特定問題的簡潔而優(yōu)雅的解決方案咨跌。即設(shè)計模式是在某種場合下對某個...
    nimw閱讀 562評論 0 0
  • 一函數(shù)定義 1內(nèi)置函數(shù) Python內(nèi)置了很多有用的函數(shù),我們可以直接調(diào)用硼婿。不像C#中調(diào)用函數(shù)锌半,需要先實例化類,再...
    凌雲(yún)木閱讀 355評論 0 2