什么是Proxy


首先,我們要清楚,Proxy是什么意思,這個單詞翻譯過來,就是 代理。

可以理解為,有一個很火的明星,開通了一個微博賬號,這個賬號非常活躍,回復粉絲、到處點贊之類的,但可能並不是真的由本人在維護的。

而是在背后有一個其他人 or 團隊來運營,我們就可以稱他們為代理人,因為他們發表的微博就代表了明星本人的意思。

P.S. 強行舉例子,因為本人不追星,只是猜測可能會有這樣的運營團隊

這個代入到JavaScript當中來,就可以理解為對對象或者函數的代理操作。

JavaScript中的Proxy

Proxy是ES6中提供的新的API,可以用來定義對象各種基本操作的自定義行為 (在文檔中被稱為traps,我覺得可以理解為一個針對對象各種行為的鈎子),拿它可以做很多有意思的事情,在我們需要對一些對象的行為進行控制時將變得非常有效。

創建一個Proxy的實例需要傳入兩個參數

  1. target 要被代理的對象,可以是一個object或者function
  2. handlers對該代理對象的各種操作行為處

 

  

    1. let target = {}
    2.  
      let handlers = {} // do nothing
    3.  
      let proxy = new Proxy(target, handlers)
    4.  
    5.  
      proxy.a = 123
    6.  
    7.  
      console.log(target.a) // 123
       
       
      在第二個參數為空對象的情況下,基本可以理解為是對第一個參數做的一次淺拷貝 (Proxy必須是淺拷貝,如果是深拷貝則會失去了代理的意義)
       
       
       
       

      Traps(各種行為的代理)
      就像上邊的示例代碼一樣,如果沒有定義對應的trap,則不會起任何作用,相當於直接操作了target。

      當我們寫了某個trap以后,在做對應的動作時,就會觸發我們的回調函數,由我們來控制被代理對象的行為。

      最常用的兩個trap應該就是get和set了。

      早年JavaScript有着在定義對象時針對某個屬性進行設置getter、setter:

       


      1.let obj = {
      2._age: 18,
      3.get age () {
      4.return `I'm ${this._age} years old`
      5.},
      6.set age (val) {
      7.  this._age = Number(val)
      8.}
      9.}
      10.​
      11.console.log(obj.age) // I'm 18 years old
      12.obj.age = 19
      13.console.log(obj.age) // I'm 19 years old

       

      就像這段代碼描述的一樣,我們設置了一個屬性_age,然后又設置了一個get ageset age

      然后我們可以直接調用obj.age來獲取一個返回值,也可以對其進行賦值。

      這么做有幾個缺點:

       

      1. 針對每一個要代理的屬性都要編寫對應的gettersetter
      2. 必須還要存在一個存儲真實值的key(如果我們直接在getter里邊調用this.age則會出現堆棧溢出的情況,因為無論何時調用this.age進行取值都會觸發getter)

       

      Proxy很好的解決了這兩個問題:

       


      1.let target = { age: 18, name: 'Niko Bellic' }
      2.let handlers = {
      3.get (target, property) {
      4.return `${property}: ${target[property]}`
      5.},
      6.set (target, property, value) {
      7.target[property] = value
      8.}
      9.}
      10.let proxy = new Proxy(target, handlers)
      ​11.
      12.proxy.age = 19
      13.console.log(target.age, proxy.age) // 19, age : 19
      14.console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic

       

      我們通過創建get、set兩個trap來統一管理所有的操作,可以看到,在修改proxy的同時,target的內容也被修改,而且我們對proxy的行為進行了一些特殊的處理。

      而且我們無需額外的用一個key來存儲真實的值,因為我們在trap內部操作的是target對象,而不是proxy對象。

       

      拿Proxy來做些什么

      因為在使用了Proxy后,對象的行為基本上都是可控的,所以我們能拿來做一些之前實現起來比較復雜的事情。

      在下邊列出了幾個簡單的適用場景。

       

      解決對象屬性為undefined的問題

      在一些層級比較深的對象屬性獲取中,如何處理undefined一直是一個痛苦的過程,如果我們用Proxy可以很好的兼容這種情況。

       

      1.(() => {
      2.let target = {}
      3.let handlers = {
      4.get: (target, property) => {
      5.target[property] = (property in target) ? target[property] : {}
      6.if (typeof target[property] === 'object') {
      7.return new Proxy(target[property], handlers)
      8.}
      9.return target[property]
      10.}
      11.}
      12.let proxy = new Proxy(target, handlers)
      13.console.log('z' in proxy.x.y) // false (其實這一步已經針對`target`創建了一個x.y的屬性)
      14.proxy.x.y.z = 'hello'
      15.console.log('z' in proxy.x.y) // true
      16.console.log(target.x.y.z) // hello
      17.})()

       

      我們代理了get,並在里邊進行邏輯處理,如果我們要進行get的值來自一個不存在的key,則我們會在target中創建對應個這個key,然后返回一個針對這個key的代理對象。

      這樣就能夠保證我們的取值操作一定不會拋出can not get xxx from undefined 但是這會有一個小缺點,就是如果你確實要判斷這個key是否存在只能夠通過in操作符來判斷,而不能夠直接通過get來判斷。

      普通函數與構造函數的兼容處理
      如果我們提供了一個Class對象給其他人,或者說一個ES5版本的構造函數。 如果沒有使用new關鍵字來調用的話,Class對象會直接拋出異常,而ES5中的構造函數this指向則會變為調用函數時的作用域。 我們可以使用apply這個trap來兼容這種情況:


      1.class Test {
      2.constructor (a, b) {
      3.console.log('constructor', a, b)
      4.}
      5.}
      ​6.
      7.// Test(1, 2) // throw an error
      8.let proxyClass = new Proxy(Test, {
      9.apply (target, thisArg, argumentsList) {
      10.// 如果想要禁止使用非new的方式來調用函數,直接拋出異常即可
      11.// throw new Error(`Function ${target.name} cannot be invoked without 'new'`)
      12.return new (target.bind(thisArg, ...argumentsList))()
      13.}
      14.})
      ​15.
      16.proxyClass(1, 2) // constructor 1 2

       

      我們使用了apply來代理一些行為,在函數調用時會被觸發,因為我們明確的知道,代理的是一個Class或構造函數,所以我們直接在apply中使用new關鍵字來調用被代理的函數。

      以及如果我們想要對函數進行限制,禁止使用new關鍵字來調用,可以用另一個trap:construct

       


      1.function add (a, b) {
      2.return a + b
      3.}
      ​4.
      5.let proxy = new Proxy(add, {
      6.construct (target, argumentsList, newTarget) {
      7.throw new Error(`Function ${target.name} cannot be invoked with 'new'`)
      8.}
      9.})
      ​10.
      11.proxy(1, 2) // 3
      12.new proxy(1, 2) // throw an error

       

      用Proxy來包裝fetch

      在前端發送請求,我們現在經常用到的應該就是fetch了,一個原生提供的API。 我們可以用Proxy來包裝它,使其變得更易用。

       


      1.let handlers = {
      2.get (target, property) {
      3.if (!target.init) {
      4.// 初始化對象
      5.['GET', 'POST'].forEach(method => {
      6.target[method] = (url, params = {}) => {
      7.return fetch(url, {
      8.headers: {
      9.'content-type': 'application/json'
      10.},
      11.mode: 'cors',
      12.credentials: 'same-origin',
      13.method,
      14....params
      15.}).then(response => response.json())
      16.}
      17.})
      18.}
      ​19.
      20.return target[property]
      21.}
      22.}
      23.let API = new Proxy({}, handlers)
      24.​
      25.await API.GET('XXX')
      26.await API.POST('XXX', {
      27.body: JSON.stringify({name: 1})
      28.})
      GETPOST進行了一層封裝,可以直接通過.GET這種方式來調用,並設置一些通用的參數。

       

      實現一個簡易的斷言工具

      寫過測試的各位童鞋,應該都會知道斷言這個東西 console.assert就是一個斷言工具,接受兩個參數,如果第一個為false,則會將第二個參數作為Error message拋出。 我們可以使用Proxy來做一個直接賦值就能實現斷言的工具。

       


      1.let assert = new Proxy({}, {
      2.set (target, message, value) {
      3.if (!value) console.error(message)
      4.}
      5.})
      ​6.
      7.assert['Isn\'t true'] = false // Error: Isn't true
      8.assert['Less than 18'] = 18 >= 19 // Error: Less than 18

      統計函數調用次數

      在做服務端時,我們可以用Proxy代理一些函數,來統計一段時間內調用的次數。 在后期做性能分析時可能會能夠用上:

       

       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM