Proxy是什麼
首先,我們要清楚,Proxy
是什麼意思,這個單詞翻譯過來,就是 代理。
可以理解為,有一個很火的明星,開通了一個微博賬號,這個賬號非常活躍,回覆粉絲、到處點贊之類的,但可能並不是真的由本人在維護的。
而是在背後有一個其他人 or 團隊來運營,我們就可以稱他們為代理人,因為他們發表的微博就代表了明星本人的意思。
P.S. 強行舉例子,因為本人不追星,只是猜測可能會有這樣的運營團隊
這個代入到JavaScript
當中來,就可以理解為對物件
或者函式
的代理操作。
JavaScript中的Proxy
Proxy是ES6中提供的新的API,可以用來定義物件各種基本操作的自定義行為
(在文件中被稱為traps
,我覺得可以理解為一個針對物件各種行為的鉤子)
拿它可以做很多有意思的事情,在我們需要對一些物件的行為進行控制時將變得非常有效。
Proxy的語法
建立一個Proxy
的例項需要傳入兩個引數
target
要被代理的物件,可以是一個object
或者function
handlers
對該代理物件的各種操作行為處理
1 2 3 4 5 6 7 |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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
很好的解決了這兩個問題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
可以很好的相容這種情況。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
(() => { 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
來相容這種情況:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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
1 2 3 4 5 6 7 8 9 10 11 12 |
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
來包裝它,使其變得更易用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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
來做一個直接賦值就能實現斷言的工具。
1 2 3 4 5 6 7 8 |
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
代理一些函式,來統計一段時間內呼叫的次數。
在後期做效能分析時可能會能夠用上:
1 2 3 4 5 6 7 8 |
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 | 獲取一個自有屬性 (不會去原型鏈查詢) 的屬性描述 |