COCI2010/2011 Contest#7 E
什麼是proxy
proxy翻譯過來就是代理的意思,那麼在javascript中,proxy(代理)是什麼意思呢?proxy時ES6提供的新的API,可以用來定義物件的各種基本操作。proxy是一種可以攔截並改變底層javascript引擎操作的包裝器,在新語言中通過它暴露內部運作的物件。
陣列的問題
在ES6出現以前,開發者不能通過自己定義的物件模仿javascript陣列物件的行為方式。當給陣列的特定元素賦值時,影響到該陣列的length屬性,也可以通過length屬性修改陣列元素。例如:
let colors = ['red', 'green', 'blue'] console.log(colors.length) //3 colors[3] = "black" console.log(colors.length) //4 console.log(colors[3]) //'black' colors.length = 2 console.log(colors.length) //2 console.log(colors[3]) //undefined console.log(colors[2]) //undefined console.log(colors[1]) //'green'
colors陣列一開始有3個元素,將colors[3]賦值為'black'的時候,陣列的length自動增加到4,將length設定為2的時候會移除陣列的後兩個元素而只保留前兩個。在es5之前開發者無法實現這些行為,但是現在通過代理就可以了
注意:數值屬性和length屬性具有這種非標準行為,因而在es6中陣列被認為是奇異物件
代理和反射
呼叫new proxy()可建立代替其他目標(target)物件的代理,它虛擬花了目標,所以二者看起來功能一致
代理可以攔截javascript引擎內部目標的底層兌現該操作,這些底層操作被攔截後會觸發響應特定操作的陷阱函式
反射API以Reflect物件的形式出現,物件中方法的預設特性與相同的底層操作一致,而代理可以複寫這些操作,每個代理陷阱對應一個命名和引數都相同的Reflect方法
| 代理陷阱 | 複寫特性 | 預設特性 |
| get | 讀取一個屬性值 | Reflect.get() |
| set | 寫入一個屬性 | Reflect.set() |
| has | in操作符 | Reflect.has() |
| deleteProperty | delete操作符 | Reflect.deleteProperty() |
| getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
| setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
| isExtensible | Object.IsExtensible() | Reflect.IsExtensible() |
| preventExtensions| Object.preventExtensions() | Reflect.preventExtensions() |
| getOwnPropertyDescriptor | Object.getOwnPertyDescriptor() | Reflect.getOwnPertyDescriptor() |
| defineProperty | Object.defineProperty() | Reflect.defineProperty() |
| OwnKeys | Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbol() | Reflect.OwnKeys() |
| apply | 呼叫一個函式 | Reflect.apply() |
| construct | 用new呼叫一個函式 | Reflect.construct |
每個陷阱覆寫javascript物件的一些特性,可以用他們攔截並修改這些特性。如果仍需要使用內建特性,則可以使用相應的反射API方法。建立代理會讓代理和反射API的關係變得清楚,所以我們最好深入進去看一些示例
建立一個簡單的代理
用Proxy建構函式建立代理需要傳入兩個引數:目標(target)和處理程式(handler)。處理程式時定義一個或多個陷阱的物件,在代理中,除了專門為操作定義的陷阱外,其餘操作均使用預設特性。不適用任何陷阱的處理程式等價於簡單的轉發代理,就像這樣
let target = {} let proxy = new Proxy(target,{}); proxy.name = 'proxy' console.log(proxy,proxy.name) console.log(target,target.name) console.log(proxy == target) target.name = 'target' console.log(proxy,proxy.name) console.log(target,target.name)
得到的結果如圖所示
這個示例中的代理將所有操作直接轉發到目標,將"proxy"賦值給proxy.name屬性時,會在目標上建立name,代理只是簡單地將操作轉發給目標,它不會儲存這個屬性。由於proxy.name和target.name引用的都是target.name,因此二者的值相同,從而target.name設定新值後,proxy.name也一同變化
使用set陷阱驗證屬性
假設你想建立一個屬性值是數字的物件,物件中每新增一個屬性都要加以驗證,如果不是數字必須丟擲錯誤。為了實現這個任務,可以定義一個set陷阱來覆寫設定值的預設特性。set陷阱接受4個引數
1.trapTarget 用於接收屬性(代理的目標)的物件
2. key被寫入的屬性鍵(字串或Symbol型別)
3. value被寫入屬性的值
4. receiver 操作發生的物件(通常是代理)
Reflect.set()是set陷阱對應的反射方法和預設特性,它和set代理陷阱一樣接收相同的4個引數,以方便在陷阱中使用。如果屬性已設定陷阱應該返回true,如果未設定則返回false。(Reflect.set()方法基於操作是否成功來返回恰當的值)
可以使用set陷阱並檢查傳入的值來驗證屬性值,例如
let target = {
name: 'target'
}
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
//忽略不希望受到影響的已有屬性
if (!trapTarget.hasOwnProperty(key)) {
console.log(value, isNaN(value))
if (isNaN(value)) {
throw new TypeError("屬性必須是數字")
}
}
//新增屬性
return Reflect.set(trapTarget, key, value, receiver)
}
})
// 新增一個新屬性
proxy.count = 1
console.log(proxy.count)
console.log(target.count)
//由於目標已有name屬性因而可以為他賦值
proxy.name = 'proxy'
console.log(proxy.name)
console.log(target.name)
//給不存在的屬性賦值會丟擲錯誤
proxy.anotherName = 'proxy'
結果如下圖所示
這段程式碼定義了一個代理來驗證新增到target的新屬性,當執行prox.count = 1時,set陷阱被呼叫,此時trapTarget的值等於target,key為"count",value的值等於1,reciever(本例中未使用)等於proxy。由於target上沒有count屬性,因此代理繼續將value值傳入isNaN(),如果結果時NaN,則證明傳入的屬性值不是數字,同時也丟擲一個錯誤。這段程式碼中,count被設定為1,所以代理呼叫Reflect.set()方法並傳入陷阱接收的4個引數來新增新屬性
proxy.name可以成功的賦值為一個字串,這是因為target已經擁有一個name屬性了,但通過呼叫trapTarget.hasOwnProperty()方法驗證檢查後被排除了,所以目標已有的非數字屬性仍然可以被操作。然後,將proxy.anotherName賦值為一個字串時會丟擲錯誤。目標沒有anotherName屬性,所以它的值時需要被驗證,而由於"proxy"不是一個數字值,因此丟擲錯誤。
set代理陷阱可以攔截寫入屬性的操作,get代理陷阱可以攔截讀取屬性的操作
用get陷阱驗證物件結構(Object Shape)
javascript有一個時常令人感到困惑的特殊行為,即讀取不存在的屬性時不會丟擲錯誤,而是用undefined代替被讀取屬性的值,就像在這個示例中
let target = {}
console.log(target.name)//undefined
在大多數語言中,如果target沒有name屬性,嘗試讀取target.name會丟擲一個錯誤,但是javascript卻用undefined來代替target.name屬性的值。如果你曾接觸過大型程式碼庫,應該知道這個特性會導致重大問題,特別是當錯誤輸入屬性名稱的時候,而代理可以通過檢查物件的結構來幫助你迴避這個問題。
物件結構是指物件中所有可用的屬性和方法的集合,javascript引擎通過物件結構來優化程式碼,通常會建立類來表示物件,如果你可以安全地假定一個物件將始終具有相同的屬性和方法,那麼當程式試圖訪問不存在的屬性時會丟擲錯誤,這對我們很有幫助。代理讓物件結構變得簡單
因為只有當讀取屬性時,才回檢測屬性,所以無論物件中是否存在某個屬性,都可以通過get陷阱來檢測,它接收3個引數
- trapTarget 被讀取屬性的源物件(代理的目標)
- key 要讀取的屬性鍵(字串或者symbol)
- receiver 操作發生的物件(通常是代理)
由於get陷阱不寫入值,所以它復刻了set陷阱中除value外的其他3個引數,Reflect.get()也接受同樣3個引數並返回屬性的預設值。
如果屬性在目標上不存在,則使用get陷阱和Reflect.get()時會丟擲錯誤,就像這樣:
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("屬性" + key + "不存在")
}
return Reflect.get(trapTarget, key, receiver)
}
})
//新添一個屬性,程式仍然正常執行
proxy.name = 'proxy'
console.log(proxy.name) // proxy
//如果屬性不存在,則丟擲錯誤
console.log(proxy.nme) //丟擲錯誤
此示例中的get陷阱可以攔截屬性的讀取操作,並通過in操作符來判斷receiver上是否具有被讀取的屬性,這裡之所以用in操作符檢查receiver而不檢查trapTarget,是為了放置receiver代理含有has陷阱。在這種情況下檢查trapTarget可能會忽略掉has陷阱,從而得到錯誤的結果。屬性如果不存在會丟擲一個錯誤,否則就是用預設行為。
這段程式碼展示瞭如何在沒有錯誤的情況下給proxy新增新屬性name,並寫入值和讀取值。最後一行包含一個輸入錯誤:proxy.nme有可能是proxy.name,由於nme時一個不存在的屬性,因為丟擲錯誤