首頁 > web前端 > js教程 > 主體

ES6中Proxy的使用說明

亚连
發布: 2018-06-13 10:57:35
原創
1497 人瀏覽過

這篇文章主要介紹了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 新增的全域對象,詳細資訊請參考MDN Reflect。

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,此後所有使用proxy 對target 屬性的存取都會經過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 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, {});
登入後複製

2. 私有屬性

##在JavaScript 或其他語言中,大家會約定俗成地在變數名稱之前加上底線_ 來表示這是一個私有屬性(並不是真正的私有),但我們無法保證真的沒人會去存取或修改它。在下面的程式碼中,我們聲明了一個私有的apiKey,便於api 這個物件內部的方法調用,但不希望從外部也能夠存取api._apiKey:

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

// logs &#39;123abc456def&#39;;
console.log("An apiKey we want to keep private", api._apiKey);

// get and mutate _apiKeys as desired
var apiKey = api._apiKey; 
api._apiKey = &#39;987654321&#39;;
登入後複製

很顯然,約定俗成是沒有束縛力的。使用 ES6 Proxy 我們就可以實現真實的私有變數了,以下針對不同的讀取方式示範兩個不同的私有化方法。第一種方法是使用set / get 攔截讀寫請求並返回undefined:

let api = { 
  _apiKey: &#39;123abc456def&#39;,
  getUsers: function(){ }, 
  getUser: function(userId){ }, 
  setUser: function(userId, config){ }
};

const RESTRICTED = [&#39;_apiKey&#39;];
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 = &#39;987654321&#39;;
登入後複製

第二種方法是使用has 攔截in 操作:

var api = { 
  _apiKey: &#39;123abc456def&#39;,
  getUsers: function(){ }, 
  getUser: function(userId){ }, 
  setUser: function(userId, config){ }
};

const RESTRICTED = [&#39;_apiKey&#39;];
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. 存取日誌

對於那些呼叫頻繁、運行緩慢或占用執行環境資源較多的屬性或接口,開發者會希望記錄它們的使用情況或性能表現,這個時候就可以使用Proxy 充當中間件的角色,輕易實現日誌功能:

let api = { 
  _apiKey: &#39;123abc456def&#39;,
  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 = [&#39;noDelete&#39;]; 
const NOCHANGE = [&#39;doNotChange&#39;];
const DEPRECATED = [&#39;oldMethod&#39;]; 

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 === &#39;function&#39; ?
      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();
登入後複製

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: &#39;devbryce&#39; };
const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);
function handleSuspectedHack(){ 
  revokeAccess();
}

// logs &#39;devbryce&#39;
console.log(sensitiveData.username);
handleSuspectedHack();
// TypeError: Revoked
console.log(sensitiveData.username);
登入後複製

Decorator

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

上面是我整理給大家的,希望今後對大家有幫助。

相關文章:

透過vue如何引入公共css檔案

在Vue中有關使用ajax方法有哪些?

在vue.js中如何實作資料分發slot

#

以上是ES6中Proxy的使用說明的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!