概述
Proxy 用于修改某些操作的默認(rèn)行為,等同于在語(yǔ)言層面做出修改,所以屬于一種“元編程”(meta programming),即對(duì)編程語(yǔ)言進(jìn)行編程。
Proxy 可以理解成,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問(wèn),都必須先通過(guò)這層攔截,因此提供了一種機(jī)制,可以對(duì)外界的訪問(wèn)進(jìn)行過(guò)濾和改寫。Proxy 這個(gè)詞的原意是代理,用在這里表示由它來(lái)“代理”某些操作,可以譯為“代理器”。

上面代碼對(duì)一個(gè)空對(duì)象架設(shè)了一層攔截,重定義了屬性的讀取(get)和設(shè)置(set)行為。這里暫時(shí)先不解釋具體的語(yǔ)法,只看運(yùn)行結(jié)果。對(duì)設(shè)置了攔截行為的對(duì)象obj,去讀寫它的屬性,就會(huì)得到下面的結(jié)果。

上面代碼說(shuō)明,Proxy 實(shí)際上重載(overload)了點(diǎn)運(yùn)算符,即用自己的定義覆蓋了語(yǔ)言的原始定義。
ES6 原生提供 Proxy 構(gòu)造函數(shù),用來(lái)生成 Proxy 實(shí)例。
var proxy = new Proxy(target, handler);
Proxy 對(duì)象的所有用法,都是上面這種形式,不同的只是handler參數(shù)的寫法。其中,new Proxy()表示生成一個(gè)Proxy實(shí)例,target參數(shù)表示所要攔截的目標(biāo)對(duì)象,handler參數(shù)也是一個(gè)對(duì)象,用來(lái)定制攔截行為

上面代碼中,作為構(gòu)造函數(shù),Proxy接受兩個(gè)參數(shù)。第一個(gè)參數(shù)是所要代理的目標(biāo)對(duì)象(上例是一個(gè)空對(duì)象),即如果沒(méi)有Proxy的介入,操作原來(lái)要訪問(wèn)的就是這個(gè)對(duì)象;第二個(gè)參數(shù)是一個(gè)配置對(duì)象,對(duì)于每一個(gè)被代理的操作,需要提供一個(gè)對(duì)應(yīng)的處理函數(shù),該函數(shù)將攔截對(duì)應(yīng)的操作。比如,上面代碼中,配置對(duì)象有一個(gè)get方法,用來(lái)攔截對(duì)目標(biāo)對(duì)象屬性的訪問(wèn)請(qǐng)求。get方法的兩個(gè)參數(shù)分別是目標(biāo)對(duì)象和所要訪問(wèn)的屬性。可以看到,由于攔截函數(shù)總是返回35,所以訪問(wèn)任何屬性都得到35。
注意,要使得Proxy起作用,必須針對(duì)Proxy實(shí)例(上例是proxy對(duì)象)進(jìn)行操作,而不是針對(duì)目標(biāo)對(duì)象(上例是空對(duì)象)進(jìn)行操作。
如果handler沒(méi)有設(shè)置任何攔截,那就等同于直接通向原對(duì)象。

上面代碼中,handler是一個(gè)空對(duì)象,沒(méi)有任何攔截效果,訪問(wèn)proxy就等同于訪問(wèn)target。
Proxy 實(shí)例也可以作為其他對(duì)象的原型對(duì)象。

上面代碼中,proxy對(duì)象是obj對(duì)象的原型,obj對(duì)象本身并沒(méi)有time屬性,所以根據(jù)原型鏈,會(huì)在proxy對(duì)象上讀取該屬性,導(dǎo)致被攔截。同一個(gè)攔截器函數(shù),可以設(shè)置攔截多個(gè)操作。

對(duì)于可以設(shè)置、但沒(méi)有設(shè)置攔截的操作,則直接落在目標(biāo)對(duì)象上,按照原先的方式產(chǎn)生結(jié)果。
下面是 Proxy 支持的攔截操作一覽,一共 13 種。

Proxy 實(shí)例的方法
下面是上面這些攔截方法的詳細(xì)介紹。
get()
get方法用于攔截某個(gè)屬性的讀取操作,可以接受三個(gè)參數(shù),依次為目標(biāo)對(duì)象、屬性名和 proxy 實(shí)例本身(嚴(yán)格地說(shuō),是操作行為所針對(duì)的對(duì)象),其中最后一個(gè)參數(shù)可選。
get方法的用法,上文已經(jīng)有一個(gè)例子,下面是另一個(gè)攔截讀取操作的例子。

上面代碼表示,如果訪問(wèn)目標(biāo)對(duì)象不存在的屬性,會(huì)拋出一個(gè)錯(cuò)誤。如果沒(méi)有這個(gè)攔截函數(shù),訪問(wèn)不存在的屬性,只會(huì)返回undefined。
get方法可以繼承。

上面代碼中,攔截操作定義在Prototype對(duì)象上面,所以如果讀取obj對(duì)象繼承的屬性時(shí),攔截會(huì)生效。
下面的例子使用get攔截,實(shí)現(xiàn)數(shù)組讀取負(fù)數(shù)的索引。

上面代碼中,數(shù)組的位置參數(shù)是-1,就會(huì)輸出數(shù)組的倒數(shù)第一個(gè)成員。
利用 Proxy,可以將讀取屬性的操作(get),轉(zhuǎn)變?yōu)閳?zhí)行某個(gè)函數(shù),從而實(shí)現(xiàn)屬性的鏈?zhǔn)讲僮鳌?/p>
上面代碼設(shè)置 Proxy 以后,達(dá)到了將函數(shù)名鏈?zhǔn)绞褂玫男Ч?/p>
下面的例子則是利用get攔截,實(shí)現(xiàn)一個(gè)生成各種 DOM 節(jié)點(diǎn)的通用函數(shù)dom。

下面是一個(gè)get方法的第三個(gè)參數(shù)的例子,它總是指向原始的讀操作所在的那個(gè)對(duì)象,一般情況下就是 Proxy 實(shí)例。

上面代碼中,proxy對(duì)象的getReceiver屬性是由proxy對(duì)象提供的,所以receiver指向proxy對(duì)象。


上面代碼中,d對(duì)象本身沒(méi)有a屬性,所以讀取d.a的時(shí)候,會(huì)去d的原型proxy對(duì)象找。這時(shí),receiver就指向d,代表原始的讀操作所在的那個(gè)對(duì)象。
如果一個(gè)屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則通過(guò) Proxy 對(duì)象訪問(wèn)該屬性會(huì)報(bào)錯(cuò)。
改一下就可以讀到屬性了
set()
set方法用來(lái)攔截某個(gè)屬性的賦值操作,可以接受四個(gè)參數(shù),依次為目標(biāo)對(duì)象、屬性名、屬性值和 Proxy 實(shí)例本身,其中最后一個(gè)參數(shù)可選。
假定Person對(duì)象有一個(gè)age屬性,該屬性應(yīng)該是一個(gè)不大于 200 的整數(shù),那么可以使用Proxy保證age的屬性值符合要求。

上面代碼中,由于設(shè)置了存值函數(shù)set,任何不符合要求的age屬性賦值,都會(huì)拋出一個(gè)錯(cuò)誤,這是數(shù)據(jù)驗(yàn)證的一種實(shí)現(xiàn)方法。利用set方法,還可以數(shù)據(jù)綁定,即每當(dāng)對(duì)象發(fā)生變化時(shí),會(huì)自動(dòng)更新 DOM。
有時(shí),我們會(huì)在對(duì)象上面設(shè)置內(nèi)部屬性,屬性名的第一個(gè)字符使用下劃線開頭,表示這些屬性不應(yīng)該被外部使用。結(jié)合get和set方法,就可以做到防止這些內(nèi)部屬性被外部讀寫。

上面代碼中,只要讀寫的屬性名的第一個(gè)字符是下劃線,一律拋錯(cuò),從而達(dá)到禁止讀寫內(nèi)部屬性的目的。
下面是set方法第四個(gè)參數(shù)的例子

上面代碼中,set方法的第四個(gè)參數(shù)receiver,指的是原始的操作行為所在的那個(gè)對(duì)象,一般情況下是proxy實(shí)例本身,請(qǐng)看下面的例子。

上面代碼中,設(shè)置myObj.foo屬性的值時(shí),myObj并沒(méi)有foo屬性,因此引擎會(huì)到myObj的原型鏈去找foo屬性。myObj的原型對(duì)象proxy是一個(gè) Proxy 實(shí)例,設(shè)置它的foo屬性會(huì)觸發(fā)set方法。這時(shí),第四個(gè)參數(shù)receiver就指向原始賦值行為所在的對(duì)象myObj。
注意,如果目標(biāo)對(duì)象自身的某個(gè)屬性,不可寫且不可配置,那么set方法將不起作用。

上面代碼中,obj.foo屬性不可寫,Proxy 對(duì)這個(gè)屬性的set代理將不會(huì)生效。
注意,嚴(yán)格模式下,set代理如果沒(méi)有返回true,就會(huì)報(bào)錯(cuò)。

上面代碼中,嚴(yán)格模式下,set代理返回false或者undefined,都會(huì)報(bào)錯(cuò)。
Apply()
apply方法攔截函數(shù)的調(diào)用、call和apply操作。
apply方法可以接受三個(gè)參數(shù),分別是目標(biāo)對(duì)象、目標(biāo)對(duì)象的上下文對(duì)象(this)和目標(biāo)對(duì)象的參數(shù)數(shù)組。
下面是一個(gè)例子。
上面代碼中,變量p是 Proxy 的實(shí)例,當(dāng)它作為函數(shù)調(diào)用時(shí)(p()),就會(huì)被apply方法攔截,返回一個(gè)字符串。
下面是另外一個(gè)例子。

上面代碼中,每當(dāng)執(zhí)行proxy函數(shù)(直接調(diào)用或call和apply調(diào)用),就會(huì)被apply方法攔截。
另外,直接調(diào)用Reflect.apply方法,也會(huì)被攔截。

has()
has方法用來(lái)攔截HasProperty操作,即判斷對(duì)象是否具有某個(gè)屬性時(shí),這個(gè)方法會(huì)生效。典型的操作就是in運(yùn)算符。
has方法可以接受兩個(gè)參數(shù),分別是目標(biāo)對(duì)象、需查詢的屬性名。
下面的例子使用has方法隱藏某些屬性,不被in運(yùn)算符發(fā)現(xiàn)。

上面代碼中,如果原對(duì)象的屬性名的第一個(gè)字符是下劃線,proxy.has就會(huì)返回false,從而不會(huì)被in運(yùn)算符發(fā)現(xiàn)。
如果原對(duì)象不可配置或者禁止擴(kuò)展,這時(shí)has攔截會(huì)報(bào)錯(cuò)。

上面代碼中,obj對(duì)象禁止擴(kuò)展,結(jié)果使用has攔截就會(huì)報(bào)錯(cuò)。也就是說(shuō),如果某個(gè)屬性不可配置(或者目標(biāo)對(duì)象不可擴(kuò)展),則has方法就不得“隱藏”(即返回false)目標(biāo)對(duì)象的該屬性。
值得注意的是,has方法攔截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判斷一個(gè)屬性是對(duì)象自身的屬性,還是繼承的屬性。
另外,雖然for...in循環(huán)也用到了in運(yùn)算符,但是has攔截對(duì)for...in循環(huán)不生效。

上面代碼中,has攔截只對(duì)in運(yùn)算符生效,對(duì)for...in循環(huán)不生效,導(dǎo)致不符合要求的屬性沒(méi)有被for...in循環(huán)所排除。
construct()
construct方法用于攔截new命令,下面是攔截對(duì)象的寫法。

construct方法可以接受兩個(gè)參數(shù)。
- target:目標(biāo)對(duì)象
- args:構(gòu)造函數(shù)的參數(shù)對(duì)象
- newTarget:創(chuàng)造實(shí)例對(duì)象時(shí),new命令作用的構(gòu)造函數(shù)(下面例子的p)

construct方法返回的必須是一個(gè)對(duì)象,否則會(huì)報(bào)錯(cuò)。

deleteProperty()
deleteProperty方法用于攔截delete操作,如果這個(gè)方法拋出錯(cuò)誤或者返回false,當(dāng)前屬性就無(wú)法被delete命令刪除。

上面代碼中,deleteProperty方法攔截了delete操作符,刪除第一個(gè)字符為下劃線的屬性會(huì)報(bào)錯(cuò)。
注意,目標(biāo)對(duì)象自身的不可配置(configurable)的屬性,不能被deleteProperty方法刪除,否則報(bào)錯(cuò)。
defineProperty()
defineProperty方法攔截了Object.defineProperty操作。

上面代碼中,defineProperty方法返回false,導(dǎo)致添加新屬性總是無(wú)效。
注意,如果目標(biāo)對(duì)象不可擴(kuò)展(non-extensible),則defineProperty不能增加目標(biāo)對(duì)象上不存在的屬性,否則會(huì)報(bào)錯(cuò)。另外,如果目標(biāo)對(duì)象的某個(gè)屬性不可寫(writable)或不可配置(configurable),則defineProperty方法不得改變這兩個(gè)設(shè)置。
getOwnPropertyDescriptor()
getOwnPropertyDescriptor方法攔截Object.getOwnPropertyDescriptor(),返回一個(gè)屬性描述對(duì)象或者undefined。

上面代碼中,handler.getOwnPropertyDescriptor方法對(duì)于第一個(gè)字符為下劃線的屬性名會(huì)返回undefined。
getPrototypeOf()
getPrototypeOf方法主要用來(lái)攔截獲取對(duì)象原型。具體來(lái)說(shuō),攔截下面這些操作。
- Object.prototype.__proto__
- Object.prototype.isPrototypeOf()
- Object.getPrototypeOf()
- Reflect.getPrototypeOf()
- instanceof
下面是一個(gè)例子。

上面代碼中,getPrototypeOf方法攔截Object.getPrototypeOf(),返回proto對(duì)象。
注意,getPrototypeOf方法的返回值必須是對(duì)象或者null,否則報(bào)錯(cuò)。另外,如果目標(biāo)對(duì)象不可擴(kuò)展(non-extensible), getPrototypeOf方法必須返回目標(biāo)對(duì)象的原型對(duì)象。
isExtensible()
isExtensible方法攔截Object.isExtensible操作。

ownKeys()
ownKeys方法用來(lái)攔截對(duì)象自身屬性的讀取操作。具體來(lái)說(shuō),攔截以下操作。
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- for...in循環(huán)
下面是攔截Object.keys()的例子。

上面代碼攔截了對(duì)于target對(duì)象的Object.keys()操作,只返回a、b、c三個(gè)屬性之中的a屬性。
下面的例子是攔截第一個(gè)字符為下劃線的屬性名。

注意,使用Object.keys方法時(shí),有三類屬性會(huì)被ownKeys方法自動(dòng)過(guò)濾,不會(huì)返回。
- 目標(biāo)對(duì)象上不存在的屬性
- 屬性名為 Symbol 值
- 不可遍歷(enumerable)的屬性

上面代碼中,ownKeys方法之中,顯式返回不存在的屬性(d)、Symbol 值(Symbol.for('secret'))、不可遍歷的屬性(key),結(jié)果都被自動(dòng)過(guò)濾掉。
ownKeys方法還可以攔截Object.getOwnPropertyNames()。

for...in循環(huán)也受到ownKeys方法的攔截。

上面代碼中,ownkeys指定只返回a和b屬性,由于obj沒(méi)有這兩個(gè)屬性,因此for...in循環(huán)不會(huì)有任何輸出。
ownKeys方法返回的數(shù)組成員,只能是字符串或 Symbol 值。如果有其他類型的值,或者返回的根本不是數(shù)組,就會(huì)報(bào)錯(cuò)。

實(shí)例:Web 服務(wù)的客戶端
Proxy 對(duì)象可以攔截目標(biāo)對(duì)象的任意屬性,這使得它很合適用來(lái)寫 Web 服務(wù)的客戶端。

同理,Proxy 也可以用來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)的 ORM 層