ECMAScript 在 ES6 規範中加入了 Proxy 與 Reflect 兩個新特性,這兩個新特性增強了 JavaScript 中物件存取的可控性,使得 JS 模組、類的封裝能夠更加嚴密與簡單,也讓操作物件時的報錯變得更加可控。
Proxy,正如其名,代理。這個介面可以給指定的物件建立一個代理物件,對代理物件的任何操作,如:存取屬性、對屬性賦值、函數呼叫,都會被攔截,然後交由我們定義的函數來處理相應的操作,
JavaScript 的特性讓物件有很大的操作空間,同時 JavaScript 也提供了很多方法讓我們去改造物件,可以隨意新增屬性、隨意刪除屬性、隨意更改物件的原型……但是此前 Object 類提供的 API 有許多缺點:
Proxy 介面的出現很好地解決了這些問題:
Proxy 介面在 JS 環境中是一個建構函式:
ƒ Proxy ( target: Object, handlers: Object ) : Proxy
這個建構函式有兩個引數,第一個是我們要代理的物件,第二個是包含處理各種操作的函數的物件。
下面是呼叫範例:
//需要代理的目標 var target = { msg: "I wish I was a bird!" }; //包含處理各種操作的函數的物件 var handler = { //處理其中一種操作的函數,此處是存取屬性的操作 get(target, property) { //在控制檯列印存取了哪個屬性 console.log(`你存取了 ${property} 屬性`); //實現操作的功能 return target[property]; } } //構造代理物件 var proxy = new Proxy( target , handler); //存取代理物件 proxy.msg //控制檯: 你存取了 msg 屬性 //← I wish I was a bird!
在上面的例子中,先建立了一個物件,賦值給 target ,然後再以 target 為目標建立了一個代理物件,賦值給 proxy。在作為第二個引數提供給 Proxy 建構函式的物件裡有屬性名為「get」的屬性,是一個函數,「get」是 Proxy 介面一個陷阱的名稱,Proxy 會參照我們作為第二個引數提供的物件裡的屬性,找到那些屬性名與陷阱名相同的屬性,自動設定相應的陷阱並把屬性上的函數作為陷阱的處理常式。陷阱能夠攔截對代理物件的特定操作,把操作的細節轉換成引數傳遞給我們的處理常式,讓處理常式去完成這一操作,這樣我們就可以通過處理常式來控制物件的各種行為。
在上面的例子裡,構造代理物件時提供的 get 函數就是處理存取物件屬性操作的函數,代理物件攔截存取物件屬性的操作並給 get 函數傳遞目標物件和請求存取的屬性名兩個引數,並將函數的返回值作為存取的結果。
Proxy 的陷阱一共有13種:
陷阱名與對應的函數引數 | 攔截的操作 | 操作範例 |
---|---|---|
get(target, property) | 存取物件屬性 | target.property 或 target[property] |
set(target, property, value, receiver) | 賦值物件屬性 | target.property = value 或 target[property] = value |
has(target, property) | 判斷物件屬性是否存在 | property in target |
isExtensible(target) | 判斷物件可否新增屬性 | Object.isExtensible(target) |
preventExtensions(target) | 使物件無法新增新屬性 | Object.preventExtensions(target) |
defineProperty(target, property, descriptor) | 定義物件的屬性 | Object.defineProperty(target, property, descriptor) |
deleteProperty(target, property) | 刪除物件的屬性 | delete target.property 或 delete target[property] 或 Object.deleteProperty(target, property) |
getOwnPropertyDescriptor(target, property) | 獲取物件自有屬性的描述符 | Object.getOwnPropertyDescriptor(target, property) |
ownKeys(target) | 列舉物件全部自有屬性 | Object.getOwnPropertyNames(target). concat(Object.getOwnPropertySymbols(target)) |
getPrototypeOf(target) | 獲取物件的原型 | Object.getPrototypeOf(target) |
setPrototypeOf(target) | 設定物件的原型 | Object.setPrototypeOf(target) |
apply(target, thisArg, argumentsList) | 函數呼叫 | target(...arguments) 或 target.apply(target, thisArg, argumentsList) |
construct(target, argumentsList, newTarget) | 建構函式呼叫 | new target(...arguments) |
在上面列出的陷阱裡是有攔截函數呼叫一類操作的,但是隻限代理的物件是函數的情況下有效,Proxy 在真正呼叫我們提供的接管函數前是會進行型別檢查的,所以通過代理讓普通的物件擁有函數一樣的功能這種事就不要想啦。
某一些陷阱對處理常式的返回值有要求,如果不符合要求則會丟擲 TypeError 錯誤。限於篇幅問題,本文不深入闡述,需要了解可自行查詢資料。
除了直接 new Proxy 物件外,Proxy 建構函式上還有一個靜態函數 revocable,可以構造一個能被銷燬的代理物件。
Proxy.revocable( target: Object, handlers: Object ) : Object Proxy.revocable( target, handlers ) → { proxy: Proxy, revoke: ƒ () }
這個靜態函數接收和建構函式一樣的引數,不過它的返回值和建構函式稍有不同,會返回一個包含代理物件和銷燬函數的物件,銷燬函數不需要任何引數,我們可以隨時呼叫銷燬函數將代理物件和目標物件的代理關係斷開。斷開代理後,再對代理物件執行任何操作都會丟擲 TypeError 錯誤。
//建立代理物件 var temp1 = Proxy.revocable({a:1}, {}); //← {proxy: Proxy, revoke: ƒ} //存取代理物件 temp1.proxy.a //← 1 //銷燬代理物件 temp1.revoke(); //再次存取代理物件 temp1.proxy.a //未捕獲的錯誤: TypeError: Cannot perform 'get' on a proxy that has been revoked
弄清楚了具體的原理後,下面舉例一個應用場景。
假設某個需要對外暴露的物件上有你不希望被別人存取的屬性,就可以找代理物件作替身,在外部存取代理物件的屬性時,針對不想被別人存取的屬性返回空值或者報錯:
//目標物件 var target = { msg: "我是鮮嫩的美少女!", secret: "其實我是800歲的老太婆!" //不想被別人存取的屬性 }; //建立代理物件 var proxy = new Proxy( target , { get(target, property) { //如果存取 secret 就報錯 if (property == "secret") throw new Error("不允許存取屬性 secret!"); return target[property]; } }); //存取 msg 屬性 proxy.msg //← 我是鮮嫩的美少女! //存取 secret 屬性 proxy.secret //未捕獲的錯誤: 不允許存取屬性 secret!
在上面的例子中,我針對對 secret 屬性的存取進行了報錯,守護住了「美少女」的祕密,讓我們歌頌 Proxy 的偉大!
只不過,Proxy 只是在程式邏輯上進行了接管,上帝視角的控制檯依然能列印代理物件完整的內容,真是遺憾……(不不不,這挺好的!)
proxy//控制檯: Proxy {msg: '我是鮮嫩的美少女!', secret: '其實我是800歲的老太婆!'}
以下是關於 Proxy 的一些細節問題:
學過其他語言的人看到 Reflect 這個詞可能會首先聯想到「反射」這個概念,但 JavaScript 由於語言特性是不需要反射的,所以這裡的 Reflect 其實和反射無關,是 JavaScript 給 Proxy 配套的一系列函數。
Reflect 在 JS 環境裡是一個全域性物件,包含了與 Proxy 各種陷阱配套的函數。
Reflect: Object Reflect → { apply: ƒ apply(), construct: ƒ construct(), defineProperty: ƒ defineProperty(), deleteProperty: ƒ deleteProperty(), get: ƒ (), getOwnPropertyDescriptor: ƒ getOwnPropertyDescriptor(), getPrototypeOf: ƒ getPrototypeOf(), has: ƒ has(), isExtensible: ƒ isExtensible(), ownKeys: ƒ ownKeys(), preventExtensions: ƒ preventExtensions(), set: ƒ (), setPrototypeOf: ƒ setPrototypeOf(), Symbol(Symbol.toStringTag): "Reflect" }
可以看到,Reflect 上的所有函數都對應一個 Proxy 的陷阱。這些函數接受的引數,返回值的型別,都和 Proxy 上的別無二致,可以說 Reflect 就是 Proxy 攔截的那些操作的原本實現。
那 Reflect 存在的意義是什麼呢?
上文提到過,Proxy 上某一些陷阱對處理常式的返回值有要求。如果想讓代理物件能正常工作,那就不得不按照 Proxy 的要求去寫處理常式。或許會有人覺得只要用 Object 提供的方法不就好了,然而不能這麼想當然,因為某些陷阱要求的返回值和 Object 提供的方法拿到的返回值是不同的,而且有些陷阱還會有邏輯上的要求,和 Object 提供的方法的細節也有所出入。舉個簡單的例子:Proxy 的 defineProperty 陷阱要求的返回值是布林型別,成功就是 true,失敗就是 false。而 Object.defineProperty 在成功的時候會返回定義的物件,失敗則會報錯。如此應該能夠看出為陷阱編寫實現的難點,如果要求簡單那自然是輕鬆,但是要求一旦複雜起來那真是想想都頭大,大多數時候我們其實只想過濾掉一部分操作而已。Reflect 就是專門為了解決這個問題而提供的,因為 Reflect 裡的函數都和 Proxy 的陷阱配套,返回值的型別也和 Proxy 要求的相同,所以如果我們要實現原本的功能,直接呼叫 Reflect 裡對應的函數就好了。
//需要代理的物件 var target = { get me() {return "我是鮮嫩的美少女!"} //定義 me 屬性的 getter }; //建立代理物件 var proxy = new Proxy( target , { //攔截定義屬性的操作 defineProperty(target, property, descriptor) { //如果定義的屬性是 me 就返回 false 阻止 if (property == "me") return false; //使用 Reflect 提供的函數實現原本的功能 return Reflect.defineProperty(target, property, descriptor); } }); //嘗試重新定義 me 屬性 Object.defineProperty(proxy , "me", {value: "我是800歲的老太婆!"}) //未捕獲的錯誤: TypeError: 'defineProperty' on proxy: trap returned falsish for property 'me' //嘗試定義 age 屬性 Object.defineProperty(proxy , "age", {value: 17}) //← Proxy {age: 17} //使用 Reflect 提供的函數來定義屬性 Reflect.defineProperty(proxy , "me", {value: "我是800歲的老太婆!"}) //← false Reflect.defineProperty(proxy , "age", {value: 17}) //← true
在上面的例子裡,由於我很懶,所以我在接管定義屬性功能的地方「偷工減料」用了 Reflect 提供的 defineProperty 函數。用 Object.defineProperty 在代理物件上定義 me 屬性時報了錯,表示失敗,而定義 age 屬性則成功完成了。可以看到,除了被報錯的 me 屬性,對其他屬性的定義是可以成功完成的。我還使用 Reflect 提供的函數執行了同樣的操作,可以看到 Reflect 也無法越過 Proxy 的代理,同時也顯示出了 Reflect 和傳統方法返回值的區別。
雖然 Reflect 的好處很多,但是它也有一個問題:JS 全域性上的 Reflect 物件是可以被修改的,可以替換掉裡面的方法,甚至還能把 Reflect 刪掉。
//備份原本的 Reflect.get var originGet = Reflect.get; //修改 Reflect.get Reflect.get = function get(target ,property) { console.log("哈哈,你的 get 已經是我的形狀了!"); return originGet(target ,property); }; //呼叫 Reflect.get Reflect.get({a:1}, "a") //控制檯: 哈哈,你的 get 已經是我的形狀了! //← 1 //刪除 Reflect 變數 delete Reflect //← true //存取 Reflect 變數 Reflect //未捕獲的錯誤: ReferenceError: Reflect is not defined
基於上面的演示,不難想到,可以通過修改 Reflect 以欺騙的方式越過 Proxy 的代理。所以如果你對安全性有要求,建議在使用 Reflect 時,第一時間將全域性上的 Reflect 深度複製到你的閉包作用域並且只使用你的備份,或者將全域性上的 Reflect 凍結並鎖定參照。
相關推薦:
以上就是一起聊聊Javascript之Proxy與Reflect的詳細內容,更多請關注TW511.COM其它相關文章!