首先,我們要清楚, Proxy是什麼意思,這個單詞翻譯過來,就是 代理。
可以理解為,有一個很火的明星,開通了一個微博帳號,這個帳號非常活躍,回復粉絲、到處點讚之類的,但可能並不是真的由本人在維護的。而是在背後有一個其他人 or 團隊來運營,我們就可以稱他們為代理人,因為他們發表的微博就代表了明星本人的意思。
P.S. 強行舉例子,因為本人不追星,只是猜測可能會有這樣的運營團隊
這個代入到 JavaScript當中來,就可以理解為對 對象或者 函數的代理操作。
JavaScript中的ProxyProxy是ES6中提供的新的API,可以用來定義對象各種基本操作的自定義行為(在文檔中被稱為 traps,我覺得可以理解為一個針對對象各種行為的鉤子)。拿它可以做很多有意思的事情,在我們需要對一些對象的行為進行控制時將變得非常有效。
Proxy的語法創建一個 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必須是淺拷貝,如果是深拷貝則會失去了代理的意義)。
Traps(各種行為的代理)就像上邊的示例代碼一樣,如果沒有定義對應的 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,然後又設置了一個 getage和 setage。然後我們可以直接調用 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來做些什麼因為在使用了 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的代理對象。這樣就能夠保證我們的取值操作一定不會拋出 cannotgetxxxfromundefined。
但是這會有一個小缺點,就是如果你確實要判斷這個 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})
})
對 GET、 POST進行了一層封裝,可以直接通過 .GET這種方式來調用,並設置一些通用的參數。
實現一個簡易的斷言工具寫過測試的各位童鞋,應該都會知道斷言這個東西。 console.assert就是一個斷言工具,接受兩個參數,如果第一個為 false,則會將第二個參數作為 Errormessage拋出。
我們可以使用 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裡邊同樣有一些例子
trapsdescriptionget獲取某個 key值set設置某個 key值has使用 in操作符判斷某個 key是否存在apply函數調用,僅在代理對象為 function時有效ownKeys獲取目標對象所有的 keyconstruct函數通過實例化調用,僅在代理對象為 function時有效isExtensible判斷對象是否可擴展, Object.isExtensible的代理deleteProperty刪除一個 propertydefineProperty定義一個新的 propertygetPrototypeOf獲取原型對象setPrototypeOf設置原型對象preventExtensions設置對象為不可擴展getOwnPropertyDescriptor獲取一個自有屬性 (不會去原型鏈查找) 的屬性描述參考資料Magic Methods in JavaScript? Meet Proxy!
How to use JavaScript Proxies for Fun and Profit
MDN-Proxy
杭州賽區即將開始,6 月 2 號,等你相聚!