拿Proxy能夠作哪些有意思的事兒

Proxy是什麼

首先,咱們要清楚,Proxy是什麼意思,這個單詞翻譯過來,就是 代理
能夠理解爲,有一個很火的明星,開通了一個微博帳號,這個帳號很是活躍,回覆粉絲、處處點贊之類的,但可能並非真的由本人在維護的。
而是在背後有一個其餘人 or 團隊來運營,咱們就能夠稱他們爲代理人,由於他們發表的微博就表明了明星本人的意思。
P.S. 強行舉例子,由於本人不追星,只是猜想可能會有這樣的運營團隊javascript

這個代入到JavaScript當中來,就能夠理解爲對對象或者函數的代理操做。前端

JavaScript中的Proxy

Proxy是ES6中提供的新的API,能夠用來定義對象各類基本操做的自定義行爲
(在文檔中被稱爲traps,我以爲能夠理解爲一個針對對象各類行爲的鉤子)
拿它能夠作不少有意思的事情,在咱們須要對一些對象的行爲進行控制時將變得很是有效。java

Proxy的語法

建立一個Proxy的實例須要傳入兩個參數json

  1. target 要被代理的對象,能夠是一個object或者function
  2. handlers對該代理對象的各類操做行爲處理
let target = {}
let handlers = {} // do nothing
let proxy = new Proxy(target, handlers)

proxy.a = 123

console.log(target.a) // 123

在第二個參數爲空對象的狀況下,基本能夠理解爲是對第一個參數作的一次淺拷貝
(Proxy必須是淺拷貝,若是是深拷貝則會失去了代理的意義)app

Traps(各類行爲的代理)

就像上邊的示例代碼同樣,若是沒有定義對應的trap,則不會起任何做用,至關於直接操做了target
當咱們寫了某個trap之後,在作對應的動做時,就會觸發咱們的回調函數,由咱們來控制被代理對象的行爲。 cors

最經常使用的兩個trap應該就是getset了。
早年JavaScript有着在定義對象時針對某個屬性進行設置gettersetter函數

let obj = {
  _age: 18,
  get age ()  {
    return `I'm ${this._age} years old`
  },
  set age (val) {
    this._age = Number(val)
  }
}

console.log(obj.age) // I'm 18 years old
obj.age = 19
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很好的解決了這兩個問題:性能

let target = { age: 18, name: 'Niko Bellic' }
let handlers = {
  get (target, property) {
    return `${property}: ${target[property]}`
  },
  set (target, property, value) {
    target[property] = value
  }
}
let proxy = new Proxy(target, handlers)

proxy.age = 19
console.log(target.age, proxy.age)   // 19,          age : 19
console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic

咱們經過建立getset兩個trap來統一管理全部的操做,能夠看到,在修改proxy的同時,target的內容也被修改,並且咱們對proxy的行爲進行了一些特殊的處理。
並且咱們無需額外的用一個key來存儲真實的值,由於咱們在trap內部操做的是target對象,而不是proxy對象。測試

拿Proxy來作些什麼

由於在使用了Proxy後,對象的行爲基本上都是可控的,因此咱們能拿來作一些以前實現起來比較複雜的事情。
在下邊列出了幾個簡單的適用場景。

解決對象屬性爲undefined的問題

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

(() => {
  let target = {}
  let handlers = {
    get: (target, property) => {
      target[property] = (property in target) ? target[property] : {}
      if (typeof target[property] === 'object') {
        return new Proxy(target[property], handlers)
      }
      return target[property]
    }
  }
  let proxy = new Proxy(target, handlers)
  console.log('z' in proxy.x.y) // false (其實這一步已經針對`target`建立了一個x.y的屬性)
  proxy.x.y.z = 'hello'
  console.log('z' in proxy.x.y) // true
  console.log(target.x.y.z)     // hello
})()

咱們代理了get,並在裏邊進行邏輯處理,若是咱們要進行get的值來自一個不存在的key,則咱們會在target中建立對應個這個key,而後返回一個針對這個key的代理對象。
這樣就可以保證咱們的取值操做必定不會拋出can not get xxx from undefined
可是這會有一個小缺點,就是若是你確實要判斷這個key是否存在只可以經過in操做符來判斷,而不可以直接經過get來判斷。

普通函數與構造函數的兼容處理

若是咱們提供了一個Class對象給其餘人,或者說一個ES5版本的構造函數。
若是沒有使用new關鍵字來調用的話,Class對象會直接拋出異常,而ES5中的構造函數this指向則會變爲調用函數時的做用域。
咱們可使用apply這個trap來兼容這種狀況:

class Test {
  constructor (a, b) {
    console.log('constructor', a, b)
  }
}

// Test(1, 2) // throw an error
let proxyClass = new Proxy(Test, {
  apply (target, thisArg, argumentsList) {
    // 若是想要禁止使用非new的方式來調用函數,直接拋出異常便可
    // throw new Error(`Function ${target.name} cannot be invoked without 'new'`)
    return new (target.bind(thisArg, ...argumentsList))()
  }
})

proxyClass(1, 2) // constructor 1 2

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

以及若是咱們想要對函數進行限制,禁止使用new關鍵字來調用,能夠用另外一個trap:construct

function add (a, b) {
  return a + b
}

let proxy = new Proxy(add, {
  construct (target, argumentsList, newTarget) {
    throw new Error(`Function ${target.name} cannot be invoked with 'new'`)
  }
})

proxy(1, 2)     // 3
new proxy(1, 2) // throw an error

用Proxy來包裝fetch

在前端發送請求,咱們如今常常用到的應該就是fetch了,一個原生提供的API。
咱們能夠用Proxy來包裝它,使其變得更易用。

let handlers = {
  get (target, property) {
    if (!target.init) {
      // 初始化對象
      ['GET', 'POST'].forEach(method => {
        target[method] = (url, params = {}) => {
          return fetch(url, {
            headers: {
              'content-type': 'application/json'
            },
            mode: 'cors',
            credentials: 'same-origin',
            method,
            ...params
          }).then(response => response.json())
        }
      })
    }

    return target[property]
  }
}
let API = new Proxy({}, handlers)

await API.GET('XXX')
await API.POST('XXX', {
  body: JSON.stringify({name: 1})
})

GETPOST進行了一層封裝,能夠直接經過.GET這種方式來調用,並設置一些通用的參數。

實現一個簡易的斷言工具

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

let assert = new Proxy({}, {
  set (target, message, value) {
    if (!value) console.error(message)
  }
})

assert['Isn\'t true'] = false      // Error: Isn't true
assert['Less than 18'] = 18 >= 19  // Error: Less than 18

統計函數調用次數

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

function orginFunction () {}
let proxyFunction = new Proxy(orginFunction, {
  apply (target, thisArg. argumentsList) {
    log(XXX)

    return target.apply(thisArg, argumentsList)
  }
})

所有的traps

這裏列出了handlers全部能夠定義的行爲 (traps)

具體的能夠查看 MDN-Proxy
裏邊一樣有一些例子
traps description
get 獲取某個key
set 設置某個key
has 使用in操做符判斷某個key是否存在
apply 函數調用,僅在代理對象爲function時有效
ownKeys 獲取目標對象全部的key
construct 函數經過實例化調用,僅在代理對象爲function時有效
isExtensible 判斷對象是否可擴展,Object.isExtensible的代理
deleteProperty 刪除一個property
defineProperty 定義一個新的property
getPrototypeOf 獲取原型對象
setPrototypeOf 設置原型對象
preventExtensions 設置對象爲不可擴展
getOwnPropertyDescriptor 獲取一個自有屬性 (不會去原型鏈查找) 的屬性描述

參考資料

  1. Magic Methods in JavaScript? Meet Proxy!
  2. How to use JavaScript Proxies for Fun and Profit
  3. MDN-Proxy
相關文章
相關標籤/搜索