摘要: 神奇而有趣的Proxy。javascript
Fundebug經受權轉載,版權歸原做者全部。前端
首先,咱們要清楚,Proxy
是什麼意思,這個單詞翻譯過來,就是 代理。java
能夠理解爲,有一個很火的明星,開通了一個微博帳號,這個帳號很是活躍,回覆粉絲、處處點贊之類的,但可能並非真的由本人在維護的。json
而是在背後有一個其餘人 or 團隊來運營,咱們就能夠稱他們爲代理人,由於他們發表的微博就表明了明星本人的意思。segmentfault
P.S. 強行舉例子,由於本人不追星,只是猜想可能會有這樣的運營團隊app
這個代入到JavaScript
當中來,就能夠理解爲對對象
或者函數
的代理操做。cors
Proxy是ES6中提供的新的API,能夠用來定義對象各類基本操做的自定義行爲 (在文檔中被稱爲traps,我以爲能夠理解爲一個針對對象各類行爲的鉤子),拿它能夠作不少有意思的事情,在咱們須要對一些對象的行爲進行控制時將變得很是有效。函數
建立一個Proxy
的實例須要傳入兩個參數工具
target
要被代理的對象,能夠是一個object
或者function
handlers
對該代理對象的各類操做行爲處理let target = {}
let handlers = {} // do nothing
let proxy = new Proxy(target, handlers)
proxy.a = 123
console.log(target.a) // 123
複製代碼
在第二個參數爲空對象的狀況下,基本能夠理解爲是對第一個參數作的一次淺拷貝 (Proxy必須是淺拷貝,若是是深拷貝則會失去了代理的意義)性能
就像上邊的示例代碼同樣,若是沒有定義對應的trap
,則不會起任何做用,至關於直接操做了target
。
當咱們寫了某個trap
之後,在作對應的動做時,就會觸發咱們的回調函數,由咱們來控制被代理對象的行爲。
最經常使用的兩個trap
應該就是get
和set
了。
早年JavaScript
有着在定義對象時針對某個屬性進行設置getter
、setter
:
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 age
和set age
。
而後咱們能夠直接調用obj.age
來獲取一個返回值,也能夠對其進行賦值。
這麼作有幾個缺點:
getter
、setter
。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
複製代碼
咱們經過建立get
、set
兩個trap
來統一管理全部的操做,能夠看到,在修改proxy
的同時,target
的內容也被修改,並且咱們對proxy
的行爲進行了一些特殊的處理。
並且咱們無需額外的用一個key
來存儲真實的值,由於咱們在trap
內部操做的是target
對象,而不是proxy
對象。
由於在使用了Proxy
後,對象的行爲基本上都是可控的,因此咱們能拿來作一些以前實現起來比較複雜的事情。
在下邊列出了幾個簡單的適用場景。
在一些層級比較深的對象屬性獲取中,如何處理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
複製代碼
在前端發送請求,咱們如今常常用到的應該就是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})
})
複製代碼
對GET
、POST
進行了一層封裝,能夠直接經過.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)
}
})
複製代碼
這裏列出了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 | 獲取一個自有屬性 (不會去原型鏈查找) 的屬性描述 |