代理模式是爲一個對象提供一個佔位符,以便控制對它的訪問。 代理模式是一種很是有意義的模式,在生活中能夠找到不少代理模式的場景。好比,明星都有經紀人做爲代理。若是想請明星來辦一場商業演出,只能聯繫他的經紀人。經紀人會把商業演出的細節和報酬都談好以後,再把合同交給明星籤。 代理模式的關鍵是當客戶不方便直接訪問一個對象或者不知足須要的時候,提供一個替身對象來控制對這個對象的訪問,客戶實際上訪問的是替身對象。替身對象對請求作出一些處理以後,再把請求轉交給本體對象。本文將詳細介紹代理模式javascript
好比,實現一個小明讓B代替本身向A送花的功能。首先,不引入代理,小明直接送花給A,代碼以下java
var Flower = function(){}; var xiaoming = { sendFlower: function( target ){ var flower = new Flower(); target.receiveFlower( flower ); } }; var A = { receiveFlower: function( flower ){ console.log( '收到花 ' + flower ); } }; xiaoming.sendFlower( A );
接下來,引入代理 B,即小明經過 B 來給 A 送花緩存
var Flower = function(){}; var xiaoming = { sendFlower: function( target){ var flower = new Flower(); target.receiveFlower( flower ); } }; var B = { receiveFlower: function( flower ){ A.receiveFlower( flower ); } }; var A = { receiveFlower: function( flower ){ console.log( '收到花 ' + flower ); } }; xiaoming.sendFlower( B );
如今改變故事的背景設定,假設當A在心情好的時候收到花,小明表白成功的概率有60%,而當A在心情差的時候收到花,小明表白的成功率無限趨近於0。小明跟A剛剛認識兩天,還沒法辨別A何時心情好。若是不合時宜地把花送給A,花被直接扔掉的可能性很大。可是A的朋友B卻很瞭解A,因此小明只管把花交給B,B會監聽A的心情變化,而後選擇A心情好的時候把花轉交給A,代碼以下:服務器
var Flower = function(){}; var xiaoming = { sendFlower: function( target){ var flower = new Flower(); target.receiveFlower( flower ); } }; var B = { receiveFlower: function( flower ){ A.listenGoodMood(function(){ // 監聽A的好心情 A.receiveFlower( flower ); }); } }; var A = { receiveFlower: function( flower ){ console.log( '收到花 ' + flower ); }, listenGoodMood: function( fn ){ setTimeout(function(){ // 假設10秒以後A的心情變好 fn(); }, 10000 ); } }; xiaoming.sendFlower( B );
雖然這只是個虛擬的例子,但能夠從中找到兩種代理模式的身影網絡
【保護代理】app
代理B能夠幫助A過濾掉一些請求,好比送花的人中年齡太大的或者沒有寶馬的,這種請求就能夠直接在代理B處被拒絕掉。這種代理叫做保護代理異步
【虛擬代理】函數
把newFlower的操做交給代理B去執行,代理B會選擇在A心情好時再執行newFlower,這是代理模式的另外一種形式,叫做虛擬代理。虛擬代理把一些開銷很大的對象,延遲到真正須要它的時候纔去建立。代碼以下:this
var B = { receiveFlower: function( flower ){ A.listenGoodMood(function(){ // 監聽A 的好心情 A.receiveFlower( flower ); //延遲建立flower對象 }); } };
在Web開發中,圖片預加載是一種經常使用的技術,若是直接給某個img標籤節點設置src屬性,因爲圖片過大或者網絡不佳,圖片的位置每每有段時間會是一片空白。常見的作法是先用一張loading圖片佔位,而後用異步的方式加載圖片,等圖片加載好了再把它填充到img節點裏,這種場景就很適合使用虛擬代理spa
下面來實現這個虛擬代理,首先建立一個普通的本體對象,這個對象負責往頁面中建立一個img標籤,而且提供一個對外的setSrc接口,外界調用這個接口,即可以給該img標籤設置
var myImage = (function(){ var imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode ); return { setSrc: function( src ){ imgNode.src = src; } } })(); myImage.setSrc( 'https://static.xiaohuochai.site/icon/icon_200.png' );
如今開始引入代理對象proxyImage,經過這個代理對象,在圖片被真正加載好以前,頁面中將出現一張佔位的loading.gif,來提示用戶圖片正在加載
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 ); } return { setSrc: function( src ){ myImage.setSrc( 'loading.gif' ); img.src = src; } } })(); proxyImage.setSrc( 'https://static.xiaohuochai.site/icon/icon_200.png' );
如今經過proxyImage間接地訪問myImage。proxyImage控制了客戶對myImage的訪問,而且在此過程當中加入一些額外的操做,好比在真正的圖片加載好以前,先把img節點的src設置爲一張本地的loading圖片
若是不使用代理,則圖片預加載的函數實現代碼以下
var MyImage = (function(){ var imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode ); var img = new Image; img.onload = function(){ imgNode.src = img.src; }; return { setSrc: function( src ){ imgNode.src = 'loading.gif'; img.src = src; } } })(); MyImage.setSrc( 'https://static.xiaohuochai.site/icon/icon_200.png' );
下面引入一個面向對象設計的原則——單一職責原則
單一職責原則指的是,就一個類(一般也包括對象和函數等)而言,應該僅有一個引發它變化的緣由。若是一個對象承擔了多項職責,就意味着這個對象將變得巨大,引發它變化的緣由可能會有多個。面向對象設計鼓勵將行爲分佈到細粒度的對象之中,若是一個對象承擔的職責過多,等於把這些職責耦合到了一塊兒,這種耦合會致使脆弱和低內聚的設計。當變化發生時,設計可能會遭到意外的破壞
職責被定義爲「引發變化的緣由」。上面代碼中的MyImage對象除了負責給img節點設置src外,還要負責預加載圖片。在處理其中一個職責時,有可能由於其強耦合性影響另一個職責的實現
另外,在面向對象的程序設計中,大多數狀況下,若違反其餘任何原則,同時將違反開放——封閉原則。若是隻是從網絡上獲取一些體積很小的圖片,或者5年後的網速快到根本再也不須要預加載,可能但願把預加載圖片的這段代碼從MyImage對象裏刪掉。這時候就不得不改動 MyImage 對象了
實際上,須要的只是給img節點設置src,預加載圖片只是一個錦上添花的功能。若是能把這個操做放在另外一個對象裏面,天然是一個很是好的方法。因而代理的做用在這裏就體現出來了,代理負責預加載圖片,預加載的操做完成以後,把請求從新交給本體MyImage
縱觀整個程序,並無改變或者增長MyImage的接口,可是經過代理對象,實際上給系統添加了新的行爲。這是符合開放——封閉原則的。給img節點設置 src 和圖片預加載這兩個功能, 被隔離在兩個對象裏,它們能夠各自變化而不影響對方。況且就算有一天再也不須要預加載, 那麼只須要改爲請求本體而不是請求代理對象便可
【代理和本體接口的一致性】
代理對象和本體都對外提供了setSrc方法,在客戶看來,代理對象和本體是一致的, 代理接手請求的過程對於用戶來講是透明的,用戶並不清楚代理和本體的區別,這樣作有兩個好處:一、用戶能夠放心地請求代理,只關心是否能獲得想要的結果;二、在任何使用本體的地方均可以替換成使用代理
若是代理對象和本體對象都爲一個函數(函數也是對象),函數必然都 能被執行,則能夠認爲它們也具備一致的「接口」
var myImage = (function(){ var imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode ); return function( src ){ imgNode.src = src; } })(); var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage( this.src ); } return function( src ){ myImage( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' ); img.src = src; } })(); proxyImage( 'https://static.xiaohuochai.site/icon/icon_200.png' );
在Web開發中,也許最大的開銷就是網絡請求。假設在作一個文件同步的功能,選中一個checkbox時,它對應的文件就會被同步到另一臺備用服務器上面
首先,在頁面中放置好這些checkbox節點
<body> <input type="checkbox" id="1"></input>1 <input type="checkbox" id="2"></input>2 <input type="checkbox" id="3"></input>3 <input type="checkbox" id="4"></input>4 <input type="checkbox" id="5"></input>5 <input type="checkbox" id="6"></input>6 <input type="checkbox" id="7"></input>7 <input type="checkbox" id="8"></input>8 <input type="checkbox" id="9"></input>9 </body>
接下來,給這些checkbox綁定點擊事件,而且在點擊的同時往另外一臺服務器同步文件
var synchronousFile = function( id ){ console.log( '開始同步文件,id 爲: ' + id ); }; var checkbox = document.getElementsByTagName( 'input' ); for ( var i = 0, c; c = checkbox[ i++ ]; ){ c.onclick = function(){ if ( this.checked === true ){ synchronousFile( this.id ); } } };
當選中3個checkbox的時候,依次往服務器發送了3次同步文件的請求。能夠預見,如此頻繁的網絡請求將會帶來至關大的開銷。解決方案是能夠經過一個代理函數proxySynchronousFile來收集一段時間以內的請求,最後一次性發送給服務器。好比等待2秒以後才把這2秒以內須要同步的文件ID打包發給服務器,若是不是對實時性要求很是高的系統,2秒的延遲不會帶來太大反作用,卻能大大減輕服務器的壓力
var synchronousFile = function( id ){ console.log( '開始同步文件,id 爲: ' + id ); }; var proxySynchronousFile = (function(){ var cache = [], // 保存一段時間內須要同步的ID timer; // 定時器 return function( id ){ cache.push( id ); if ( timer ){ // 保證不會覆蓋已經啓動的定時器 return; } timer = setTimeout(function(){ synchronousFile( cache.join( ',' ) ); // 2 秒後向本體發送須要同步的ID 集合 clearTimeout( timer ); // 清空定時器 timer = null; cache.length = 0; // 清空ID 集合 }, 2000 ); } })(); var checkbox = document.getElementsByTagName( 'input' ); for ( var i = 0, c; c = checkbox[ i++ ]; ){ c.onclick = function(){ if ( this.checked === true ){ proxySynchronousFile( this.id ); } } };
假設要加載miniConsole.js這個文件,該文件用於打印log,但該文件很大。因此,一般解決方案是用一個佔位的miniConsole代理對象來給用戶提早使用,而後將打印log的請求都包裹在一個函數裏,隨後這些函數將所有放到緩存隊列中,這些邏輯都是在miniConsole代理對象中完成實現的。等用戶按下F2喚出控制檯的時候,纔開始加載真正的miniConsole.js的代碼,加載完成以後將遍歷miniConsole代理對象中的緩存函數隊列,同時依次執行它們
未加載真正的miniConsole.js以前的代碼以下:
var cache = []; var miniConsole = { log: function(){ var args = arguments; cache.push( function(){ return miniConsole.log.apply( miniConsole, args ); }); } }; miniConsole.log(1);
當用戶按下F2時,開始加載真正的miniConsole.js,代碼以下:
var handler = function( ev ){ if ( ev.keyCode === 113 ){ var script = document.createElement( 'script' ); script.onload = function(){ for ( var i = 0, fn; fn = cache[ i++ ]; ){ fn(); } }; script.src = 'miniConsole.js'; document.getElementsByTagName( 'head' )[0].appendChild( script ); } }; document.body.addEventListener( 'keydown', handler, false ); // miniConsole.js 代碼: miniConsole = { log: function(){ // 真正代碼略 console.log( Array.prototype.join.call( arguments ) ); } };
要注意的是,要保證在F2被重複按下時,miniConsole.js只被加載一次
var miniConsole = (function(){ var cache = []; var handler = function( ev ){ if ( ev.keyCode === 113 ){ var script = document.createElement( 'script' ); script.onload = function(){ for ( var i = 0, fn; fn = cache[ i++ ]; ){ fn(); } }; script.src = 'miniConsole.js'; document.getElementsByTagName( 'head' )[0].appendChild( script ); document.body.removeEventListener( 'keydown', handler );// 只加載一次miniConsole.js } }; document.body.addEventListener( 'keydown', handler, false ); return { log: function(){ var args = arguments; cache.push( function(){ return miniConsole.log.apply( miniConsole, args ); }); } } })(); miniConsole.log( 11 ); // 開始打印log // miniConsole.js 代碼 miniConsole = { log: function(){ // 真正代碼略 console.log( Array.prototype.join.call( arguments ) ); } }
緩存代理能夠爲一些開銷大的運算結果提供暫時的存儲,在下次運算時,若是傳遞進來的參數跟以前一致,則能夠直接返回前面存儲的運算結果
下面是一個計算乘積的例子
先建立一個用於求乘積的函數:
var mult = function(){ console.log( '開始計算乘積' ); var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return a; }; mult( 2, 3 ); // 輸出:6 mult( 2, 3, 4 ); // 輸出:24
而後加入緩存代理函數
var proxyMult = (function(){ var cache = {}; return function(){ var args = Array.prototype.join.call( arguments, ',' ); if ( args in cache ){ return cache[ args ]; } return cache[ args ] = mult.apply( this, arguments ); } })(); proxyMult( 1, 2, 3, 4 ); // 輸出:24 proxyMult( 1, 2, 3, 4 ); // 輸出:24
當第二次調用proxyMult(1,2,3,4)的時候,本體mult函數並無被計算,proxyMult直接返回了以前緩存好的計算結果。經過增長緩存代理的方式,mult函數能夠繼續專一於自身的職責——計算乘積,緩存的功能是由代理對象實現的
在項目中經常遇到分頁的需求,同一頁的數據理論上只須要去後臺拉取一次,這些已經拉取到的數據在某個地方被緩存以後,下次再請求同一頁的時候,即可以直接使用以前的數據。顯然這裏也能夠引入緩存代理,實現方式跟計算乘積的例子差很少,惟一不一樣的是,請求數據是個異步的操做,沒法直接把計算結果放到代理對象的緩存中,而是要經過回調的方式
經過傳入高階函數的方式,能夠爲各類計算方法建立緩存代理。如今這些計算方法被看成參數傳入一個專門用於建立緩存代理的工廠中,這樣一來,就能夠爲乘法、加法、減法等建立緩存代理,代碼以下:
/**************** 計算乘積 *****************/ var mult = function(){ var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return a; }; /**************** 計算加和 *****************/ var plus = function(){ var a = 0; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a + arguments[i]; } return a; }; /**************** 建立緩存代理的工廠 *****************/ var createProxyFactory = function( fn ){ var cache = {}; return function(){ var args = Array.prototype.join.call( arguments, ',' ); if ( args in cache ){ return cache[ args ]; } return cache[ args ] = fn.apply( this, arguments ); } }; var proxyMult = createProxyFactory( mult ), proxyPlus = createProxyFactory( plus ); alert ( proxyMult( 1, 2, 3, 4 ) ); // 輸出:24 alert ( proxyMult( 1, 2, 3, 4 ) ); // 輸出:24 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 輸出:10 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 輸出:10
代理模式的變體種類很是多,還包括如下幾種
一、防火牆代理:控制網絡資源的訪問,保護主題不讓「壞人」接近
二、遠程代理:爲一個對象在不一樣的地址空間提供局部表明
三、保護代理:用於對象應該有不一樣訪問權限的狀況
四、智能引用代理:取代了簡單的指針,它在訪問對象時執行一些附加操做,好比計算一個對象被引用的次數
五、寫時複製代理:一般用於複製一個龐大對象的狀況。寫時複製代理延遲了複製的過程,當對象被真正修改時,纔對它進行復制操做。寫時複製代理是虛擬代理的一種變體,DLL(操做系統中的動態連接庫)是其典型運用場景
代理模式包括許多小分類,在javascript開發中最經常使用的是虛擬代理和緩存代理。雖然代理模式很是有用,但在編寫業務代碼時,每每不須要去預先猜想是否須要使用代理模式。當真正發現不方便直接訪問某個對象的時候,再編寫代理也不遲