詳細探究ES6之Proxy代理,探究es6proxy

來源:互聯網
上載者:User

詳細探究ES6之Proxy代理,探究es6proxy

前言

在ES6中,Proxy構造器是一種可訪問的全域對象,使用它你可以在對象與各種操作對象的行為之間收集有關請求操作的各種資訊,並返回任何你想做的。ES6中的箭頭函數、數組解構、rest 參數等特性一經實現就廣為流傳,但類似 Proxy 這樣的特性卻很少見到有開發人員在使用,一方面在於瀏覽器的相容性,另一方面也在於要想發揮這些特性的優勢需要開發人員深入地理解其使用情境。就我個人而言是非常喜歡 ES6 的 Proxy,因為它讓我們以簡潔易懂的方式控制了外部對對象的訪問。在下文中,首先我會介紹 Proxy 的使用方式,然後列舉具體執行個體解釋 Proxy 的使用情境。

Proxy,見名知意,其功能非常類似於設計模式中的代理模式,該模式常用於三個方面:

1.和監視外部對對象的訪問

2.函數或類的複雜度

3.操作前對操作進行校正或對所需資源進行管理

在支援 Proxy 的瀏覽器環境中,Proxy 是一個全域對象,可以直接使用。Proxy(target, handler) 是一個建構函式,target 是被代理的對象,handlder 是聲明了各類代理操作的對象,最終返回一個代理對象。外界每次通過代理對象訪問 target 對象的屬性時,就會經過 handler 對象,從這個流程來看,代理對象很類似 middleware(中介軟體)。那麼 Proxy 可以攔截什麼操作呢?最常見的就是 get(讀取)、set(修改)對象屬性等操作,完整的可攔截巨集指令清單請點擊這裡。此外,Proxy 對象還提供了一個 revoke 方法,可以隨時登出所有的代理操作。在我們正式介紹 Proxy 之前,建議你對 Reflect 有一定的瞭解,它也是一個 ES6 新增的全域對象。

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 保障資料類型的準確性:

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 3obj.pickyMethodTwo("wopdopadoo", {}); // > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1// No warnings loggedobj.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 desiredvar 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 _apiKeyconsole.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, respectivelydataStore.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: Revokedconsole.log(sensitiveData.username);

Decorator

ES7 中實現的 Decorator,相當於設計模式中的裝飾器模式。如果簡單地區分 Proxy Decorator 的使用情境,可以概括為:Proxy 的核心作用是控制外界對被代理者內部的訪問,Decorator 的核心作用是增強被裝飾者的功能。只要在它們核心的使用情境上做好區別,那麼像是訪問日誌這樣的功能,雖然本文使用了 Proxy 實現,但也可以使用 Decorator 實現,開發人員可以根據項目的需求、團隊的規範、自己的偏好自由選擇。

總結

ES6 的 Proxy還是非常實用的,看似簡單的特性,卻有極大的用處。希望給大家學習ES6有所協助。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.