這篇文章為大家帶來了關於JavaScript中Proxy與Reflect的相關知識,希望對大家有幫助。
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 ] |
set(target, property, value, receiver) 賦值物件屬性 |
|
或 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
| 或|
或Object.deleteProperty(target, property)
|
#getOwnPropertyDescriptor(target, property) |
|
Object.getOwnPropertyDescriptor(target, property) |
ownKeys(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)
| #建構子呼叫
在上面列出的陷阱里是有拦截函数调用一类操作的,但是只限代理的对象是函数的情况下有效,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学习教程
以上是一起聊聊Javascript之Proxy與Reflect的詳細內容。更多資訊請關注PHP中文網其他相關文章!