JavaScript設計模式與實踐--代理模式

1.什麼是代理模式

代理模式(Proxy Pattern)是程序設計中的一種設計模式。javascript

在現實生活中,proxy是一個被受權表明其餘人的人。好比,許多州容許代理投票,這意味着你能夠受權他人在選舉中表明你投票。java

你極可能據說過proxy服務器,它會接收來自你這的全部流量,表明你發送給另外一端,並把響應返回給你。當你不但願請求的目的地知道你請求的具體來源時,使用proxy服務器就頗有用了。全部的目標服務器看到的只是來自proxy服務器的請求。es6

再接近本文的主題一些,這種類型的代理和ES6 proxy要作的就很相似了,涉及到使用類(B)去包裝類(A)並攔截/控制對(A)的訪問。ajax

當你想進行如下操做時proxy模式一般會頗有用:編程

  • 攔截或控制對某個對象的訪問
  • 經過隱藏事務或輔助邏輯來減少方法/類的複雜性
  • 防止在未經驗證/準備的狀況下執行重度依賴資源的操做

當一個複雜對象的多份副本須存在時,代理模式能夠結合享元模式以減小內存用量。典型做法是建立一個複雜對象及多個代理者,每一個代理者會引用到本來的複雜對象。而做用在代理者的運算會轉送到本來對象。一旦全部的代理者都不存在時,複雜對象會被移除。後端

上面是維基百科中對代理模式的一個總體的定義.而在JavaScript中代理模式的具體表現形式就是ES6中的新增對象---Proxy設計模式

2 ES6中的代理模式

ES6所提供Proxy構造函數可以讓咱們輕鬆的使用代理模式:api

let proxy = new Proxy(target, handler);
複製代碼

Proxy構造函數傳入兩個參數,第一個參數target表示所要代理的對象,第二個參數handler也是一個對象用來設置對所代理的對象的行爲。若是想知道Proxy的具體使用方法,可參考阮一峯的《 ECMAScript入門 - Proxy 》緩存

Proxy構造器能夠在全局對象上訪問到。經過它,你能夠有效的攔截針對對象的各類操做,收集訪問的信息,並按你的意願返回任何值。從這個角度來講,proxy和中間件有不少類似之處。安全

let dataStore = {
  name: 'Billy Bob',
  age: 15
};

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

dataStore = new Proxy(dataStore, handler);

// 這會執行咱們的攔截邏輯,記錄請求並把值賦給`name`變量
const name = dataStore.name;
複製代碼

具體來講,proxy容許你攔截許多對象上經常使用的方法和屬性,最多見的有getsetapply(針對函數)和construct(針對使用new關鍵字調用的構造函數)。關於使用proxy能夠攔截的方法的完整列表,請參考規範。Proxy還能夠配置成隨時中止接受請求,有效的取消全部針對被代理的目標對象的訪問。這能夠經過一個revoke方法實現。

3 代理模式經常使用場景

3.1 剝離驗證邏輯

一個把Proxy用於驗證的例子,驗證一個數據源中的全部屬性都是同一類型。下面的例子中咱們要確保每次給numericDataStore數據源設置一個屬性時,它的值必須是數字。

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

// 這會拋出異常
numericDataStore.count = "foo";

// 這會設置成功
numericDataStore.count = 333;
複製代碼

這頗有意思,但有多大的可能性你會建立一個這樣的對象呢?確定不會。。。

若是你想爲一個對象上的部分或所有屬性編寫自定義的校驗規則,代碼可能會更復雜一些,但我很是喜歡Proxy能夠幫你把校驗代碼與核心代碼分離開這一點。難道只有我討厭把校驗代碼和方法或類混在一塊兒嗎?

// 定義一個接收自定義校驗規則並返回一個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;
複製代碼

經過這種方式,你就能夠無限的擴展校驗規則而不用修改類或方法。

再說一個和校驗有關的點子。假設你想檢查傳給一個方法的參數並在傳入的參數與函數簽名不符時輸出一些有用的幫助信息。你能夠經過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

// 不會輸出警告信息
obj.pickyMethodOne({}, "a little string", 123);
obj.pickyMethodOne(123, {});
複製代碼

在看一個表單驗證的例子。Proxy構造函數第二個參數中的set方法,能夠很方便的驗證向一個對象的傳值。咱們以一個傳統的登錄表單舉例,該表單對象有兩個屬性,分別是accountpassword,每一個屬性值都有一個簡單和其屬性名對應的驗證方法,驗證規則以下:

// 表單對象
const userForm = {
  account: '',
  password: '',
}

// 驗證方法
const validators = {
  account(value) {
    // account 只容許爲中文
    const re = /^[\u4e00-\u9fa5]+$/;
    return {
      valid: re.test(value),
      error: '"account" is only allowed to be Chinese'
    }
  },

  password(value) {
    // password 的長度應該大於6個字符
    return {
      valid: value.length >= 6,
      error: '"password "should more than 6 character'
    }
  }
}
複製代碼

下面咱們來使用Proxy實現一個通用的表單驗證器

const getValidateProxy = (target, validators) => {
  return new Proxy(target, {
    _validators: validators,
    set(target, prop, value) {
      if (value === '') {
        console.error(`"${prop}" is not allowed to be empty`);
        return target[prop] = false;
      }
      const validResult = this._validators[prop](value);
      if(validResult.valid) {
        return Reflect.set(target, prop, value);
      } else {
        console.error(`${validResult.error}`);
        return target[prop] = false;
      }
    }
  })
}

const userFormProxy = getValidateProxy(userForm, validators);
userFormProxy.account = '123'; // "account" is only allowed to be Chinese
userFormProxy.password = 'he'; // "password "should more than 6 character
複製代碼

咱們調用getValidateProxy方法去生成了一個代理對象userFormProxy,該對象在設置屬性的時候會根據validators的驗證規則對值進行校驗。這咱們使用的是console.error拋出錯誤信息,固然咱們也能夠加入對DOM的事件來實現頁面中的校驗提示。

3.2 真正的私有屬性

在JavaScript中常見的作法是在屬性名以前或以後放一個下劃線來標識該屬性僅供內部使用。但這並不能阻止其餘人讀取或修改它。

在下面的例子中,有一個咱們想在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,你能夠經過若干方式來實現真實,徹底的私有屬性。

首先,你可使用一個proxy來截獲針對某個屬性的請求並做出限制或是直接返回undefined

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

// Add other restricted properties to this array
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);
    }
});

// throws an error
console.log(api._apiKey);

// throws an error
api._apiKey = '987654321'; 
複製代碼

你還可使用hastrap來掩蓋這個屬性的存在。

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

// Add other restricted properties to this array
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.3 默默的記錄對象訪問

針對那些重度依賴資源,執行緩慢或是頻繁使用的方法或接口,你可能喜歡統計它們的使用或是性能。Proxy能夠很容易的悄悄在後臺作到這一點。

注意:你不能僅僅使用applytrap來攔截方法。任何使用當你要執行某個方法時,你首先須要get這個方法。所以,若是你要攔截一個方法調用,你須要先攔截對該方法的get操做,而後攔截apply操做。

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

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

// executes apply trap in the background
api.getUsers();

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

這很酷,由於你能夠記錄各類各樣的信息而不用修改應用程序的代碼或是阻塞代碼執行。而且只須要在這些代碼的基礎上稍事修改就能夠記錄特性函數的執行性能了。

3.4 給出提示信息或是阻止特定操做

假設你想阻止其餘人刪除noDelete屬性,想讓調用oldMethod方法的人知道該方法已經被廢棄,或是想阻止其餘人修改doNotChange屬性。如下是一種快捷的方法。

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

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

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 防止沒必要要的資源消耗操做--緩存代理

假設你有一個服務器接口返回一個巨大的文件。當前一個請求還在處理中,或是文件正在被下載,又或是文件已經被下載以後你不想該接口被再次請求。代理在這種狀況下能夠很好的緩衝對服務器的訪問並在可能的時候讀取緩存,而不是按照用戶的要求頻繁請求服務器。緩存代理能夠將一些開銷很大的方法的運算結果進行緩存,再次調用該函數時,若參數一致,則能夠直接返回緩存中的結果,而不用再從新進行運算。例如在採用後端分頁的表格時,每次頁碼改變時須要從新請求後端數據,咱們能夠將頁碼和對應結果進行緩存,當請求同一頁時就不用在進行ajax請求而是直接返回緩存中的數據。在這裏我會跳過大部分代碼,但下面的例子仍是足夠向你展現它的工做方式。

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);
    }
  }
}); 
複製代碼

再列舉一個比較好理解的例子

下面咱們以沒有通過任何優化的計算斐波那契數列的函數來假設爲開銷很大的方法,這種遞歸調用在計算40以上的斐波那契項時就能明顯的感到延遲感。

const getFib = (number) => {
  if (number <= 2) {
    return 1;
  } else {
    return getFib(number - 1) + getFib(number - 2);
  }
}
複製代碼

如今咱們來寫一個建立緩存代理的工廠函數:

const getCacheProxy = (fn, cache = new Map()) => {
  return new Proxy(fn, {
    apply(target, context, args) {
      const argsString = args.join(' ');
      if (cache.has(argsString)) {
        // 若是有緩存,直接返回緩存數據
        console.log(`輸出${args}的緩存結果: ${cache.get(argsString)}`);
        return cache.get(argsString);
      }
      const result = fn(...args);
      cache.set(argsString, result);
      return result;
    }
  })
}

const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); // 102334155
getFibProxy(40); // 輸出40的緩存結果: 102334155
複製代碼

當咱們第二次調用getFibProxy(40)時,getFib函數並無被調用,而是直接從cache中返回了以前被緩存好的計算結果。經過加入緩存代理的方式,getFib只須要專一於本身計算斐波那契數列的職責,緩存的功能使由Proxy對象實現的。這實現了咱們以前提到的單一職責原則

3.6. 即時撤銷對敏感數據的訪問

Proxy支持隨時撤銷對目標對象的訪問。當你想完全封鎖對某些數據或API的訪問時(好比,出於安全,認證,性能等緣由),這可能會頗有用。如下是一個使用revocable方法的簡單例子。注意當你使用它時,你不須要對Proxy方法使用new關鍵字。

let sensitiveData = {  
  username: 'devbryce'
};

const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);

function handleSuspectedHack(){  
  // Don't panic
  // Breathe
  revokeAccess();
}

// logs 'devbryce'
console.log(sensitiveData.username);

handleSuspectedHack();

// TypeError: Revoked
console.log(sensitiveData.username); 
複製代碼

好吧,以上就是全部我要講的內容。我很但願能聽到你在工做中是如何使用Proxy的。

4 總結

在面向對象的編程中,代理模式的合理使用可以很好的體現下面兩條原則:

  • 單一職責原則: 面向對象設計中鼓勵將不一樣的職責分佈到細粒度的對象中,Proxy 在原對象的基礎上進行了功能的衍生而又不影響原對象,符合鬆耦合高內聚的設計理念。

  • 開放-封閉原則:代理能夠隨時從程序中去掉,而不用對其餘部分的代碼進行修改,在實際場景中,隨着版本的迭代可能會有多種緣由再也不須要代理,那麼就能夠容易的將代理對象換成原對象的調用

對於代理模式 Proxy 的做用主要體如今三個方面:

一、 攔截和監視外部對對象的訪問

二、 下降函數或類的複雜度

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

參考:

相關文章
相關標籤/搜索