您好,登錄后才能下訂單哦!
這篇文章主要介紹了Javascript的Proxy與Reflect怎么調用的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Javascript的Proxy與Reflect怎么調用文章都會有所收獲,下面我們一起來看看吧。
ECMAScript 在 ES6 規范中加入了 Proxy 與 Reflect 兩個新特性,這兩個新特性增強了 JavaScript 中對象訪問的可控性,使得 JS 模塊、類的封裝能夠更加嚴密與簡單,也讓操作對象時的報錯變得更加可控。
Proxy,正如其名,代理。這個接口可以給指定的對象創建一個代理對象,對代理對象的任何操作,如:訪問屬性、對屬性賦值、函數調用,都會被攔截,然后交由我們定義的函數來處理相應的操作,
JavaScript 的特性讓對象有很大的操作空間,同時 JavaScript 也提供了很多方法讓我們去改造對象,可以隨意添加屬性、隨意刪除屬性、隨意更改對象的原型……但是此前 Object 類提供的 API 有許多缺點:
如果要用 Object.defineProperty 定義某個名稱集合內的全部屬性,只能通過枚舉的方式為全部屬性設置 getter 和 setter,而且由于只能每個屬性創造一個函數,集合太大會造成性能問題。
Object.defineProperty 定義后的屬性,如果仍想擁有正常的存取功能,只能將數據存放在對象的另一個屬性名上或者需要另一個對象來存放數據,對于只想監聽屬性的場合尤為不便。
Object.defineProperty 無法修改類中不可重新定義的屬性,例如數組的 length 屬性。
對于那些尚不存在且名稱不好預測的屬性,Object.defineProperty 愛莫能助。
無法修改或阻止某些行為,如:枚舉屬性名、修改對象原型。
Proxy 接口的出現很好地解決了這些問題:
Proxy 接口將對對象的所有操作歸類到數個類別中,通過 Proxy 提供的陷阱攔截特定的操作,再在我們定義的處理函數中進行邏輯判斷就可以實現復雜的功能,并且還能控制比以前更多的行為。
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 的一些細節問題:
Proxy 在處理屬性名的時候會把除 Symbol 類型外的所有屬性名都轉化成字符串,所以處理函數在判斷屬性名時需要尤其注意。
對代理對象的任何操作都會被攔截,一旦代理對象被創建就沒有辦法再修改它本身。
Proxy 的代理是非常底層的,在沒有主動暴露原始目標對象的情況下,沒有任何辦法越過代理對象訪問目標對象(在控制臺搞騷操作除外)。
Proxy 代理的目標只能是對象,不能是 JavaScript 中的原始類型。
學過其他語言的人看到 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怎么調用”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Javascript的Proxy與Reflect怎么調用”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。