JS?中Proxy代理和?Reflect反射方法示例詳解
正文
總所周知,Vue2 => Vue3 時(shí),數(shù)據(jù)響應(yīng)式方法從Object.defineProperty()
方法變成了Proxy()
,所以今天與大家 Proxy(代理)和 Reflect(反射)的知識(shí)。
講解 Proxy 和 Reflect 前,我們需要先了解屬性描述符的作用,所以我們線(xiàn)簡(jiǎn)單解釋一下屬性描述符的知識(shí)。
1.屬性描述符
屬性描述符(Property Descriptor) 本質(zhì)上是一個(gè) JavaScript 普通對(duì)象,用于描述一個(gè)屬性的相關(guān)信息,共有下面這幾種屬性。
- value:屬性值
- configurable:該屬性的描述符是否可以修改
- enumerable:該屬性是否可以被枚舉
- writable:該屬性是否可以被重新賦值
- 存取器屬性:屬性描述符中如果配置了 get 和 set 中的任何一個(gè),則該屬性不再是一個(gè)普通屬性,而變成了存取器屬性。
- get()讀值函數(shù):如果一個(gè)屬性是存取器屬性,則讀取該屬性時(shí),會(huì)運(yùn)行 get 方法,并將 get 方法得到的返回值作為屬性值
- set(newVal)存值函數(shù):如果給該屬性賦值,則會(huì)運(yùn)行 set 方法,newVal 參數(shù)為賦值的值。
存取器屬性最大的意義,在于可以控制屬性的讀取和賦值,在函數(shù)里可以進(jìn)行各種操作。
Vue2 數(shù)據(jù)響應(yīng)式就是使用了這一點(diǎn),在 getter 和 setter 函數(shù)中進(jìn)行了數(shù)據(jù)綁定與派發(fā)更新。
注意點(diǎn):value 和 writable 屬性不能與 get 和 set 屬性二者不可共存,二者只能選其一。
查看某個(gè)對(duì)象的屬性描述符,使用以下這兩種方法:
Object.getOwnPropertyDescriptor(對(duì)象, 屬性名); //得到一個(gè)對(duì)象的某個(gè)屬性的屬性描述符 Object.getOwnPropertyDescriptors(對(duì)象); //得到某個(gè)對(duì)象的所有屬性描述符
為某個(gè)對(duì)象添加屬性時(shí) 或 修改屬性時(shí),配置其屬性描述符,使用以下這兩種方法:
Object.defineProperty(對(duì)象, 屬性名, 描述符); //設(shè)置一個(gè)對(duì)象的某個(gè)屬性 Object.defineProperties(對(duì)象, 多個(gè)屬性的描述符); //設(shè)置一個(gè)對(duì)象的多個(gè)屬性
2.Reflect
Reflect 是什么? Reflect 是一個(gè)內(nèi)置的 JS 對(duì)象,它提供了一系列方法,可以讓開(kāi)發(fā)者通過(guò)調(diào)用這些方法,訪(fǎng)問(wèn)一些 JS 底層功能。
由于它類(lèi)似于其他語(yǔ)言的反射,因此取名為 Reflect。
它可以做什么? 使用 Reflect 可以實(shí)現(xiàn)諸如:屬性的賦值與取值、調(diào)用普通函數(shù)、調(diào)用構(gòu)造函數(shù)、判斷屬性是否存在與對(duì)象中 等等功能。
這些功能不是已經(jīng)存在了嗎?為什么還需要用 Reflect 實(shí)現(xiàn)一次? 有一個(gè)重要的理念,在 ES5 就被提出:減少魔法、讓代碼更加純粹(語(yǔ)言的方法使用 API 實(shí)現(xiàn),而不使用特殊語(yǔ)法實(shí)現(xiàn)),這種理念很大程度上是受到函數(shù)式編程的影響。 ES6 進(jìn)一步貫徹了這種理念,它認(rèn)為,對(duì)屬性?xún)?nèi)存的控制、原型鏈的修改、函數(shù)的調(diào)用等等,這些都屬于底層實(shí)現(xiàn),屬于一種魔法,因此,需要將它們提取出來(lái),形成一個(gè)正常的 API,并高度聚合到某個(gè)對(duì)象中,于是就造就了 Reflect 對(duì)象。 因此,你可以看到 Reflect 對(duì)象中有很多的 API 都可以使用過(guò)去的某種語(yǔ)法或其他 API 實(shí)現(xiàn)。
Reflect 里面提供了哪些 API 呢?
Reflect API | 用處 | 等同于 |
---|---|---|
Reflect.get(target, propertyKey) | 讀取對(duì)象 target 的屬性 propertyKey | 對(duì)象的屬性值讀取操作 |
Reflect.set(target, propertyKey, value) | 設(shè)置對(duì)象 target 的屬性 propertyKey 的值為 value | 對(duì)象的屬性賦值操作 |
Reflect.has(target, propertyKey) | 判斷一個(gè)對(duì)象是否擁有一個(gè)屬性 | in 操作符 |
Reflect.defineProperty(target, propertyKey, attributes) | 類(lèi)似于 Object.defineProperty,不同的是如果配置出現(xiàn)問(wèn)題,返回 false 而不是報(bào)錯(cuò) | Object.defineProperty |
Reflect.deleteProperty(target, propertyKey) | 刪除一個(gè)對(duì)象的屬性 | delete 操作符 |
Reflect.apply(target, thisArgument, argumentsList) | 調(diào)用一個(gè)指定的函數(shù),并綁定 this 和參數(shù)列表 | 函數(shù)調(diào)用操作 |
Reflect.construct(target, argumentsList) | 用構(gòu)造函數(shù)的方式創(chuàng)建一個(gè)對(duì)象 | new 操作符 |
3.Proxy
ECMAScript 6 新增的代理和反射為開(kāi)發(fā)者提供了攔截并向基本操作嵌入額外行為的能力。
具體地說(shuō),可以給目標(biāo)對(duì)象(target)定義一個(gè)關(guān)聯(lián)的代理對(duì)象,而這個(gè)代理對(duì)象可當(dāng)作一個(gè)抽象的目標(biāo)對(duì)象來(lái)使用。
因此在對(duì)目標(biāo)對(duì)象的各種操作影響到目標(biāo)對(duì)象之前,我們可以在代理對(duì)象中對(duì)這些操作加以控制,并且最終也可能會(huì)改變操作返回的結(jié)果。
所以我的理解是:代理(Proxy)能使我們開(kāi)發(fā)者擁有一種間接修改底層方法的能力,從而控制用戶(hù)的操作。
3.1 創(chuàng)建空代理
最簡(jiǎn)單的代理是空代理,即除了作為一個(gè)抽象的目標(biāo)對(duì)象,什么也不做。 默認(rèn)情況下,在代理對(duì)象上執(zhí)行的所有操作都會(huì)無(wú)障礙地傳播到目標(biāo)對(duì)象。因此,在任何可以使用目標(biāo)對(duì)象的地方,都可以通過(guò)同樣的方式來(lái)使用與之關(guān)聯(lián)的代理對(duì)象。
代理是使用 Proxy 構(gòu)造函數(shù)創(chuàng)建的,這個(gè)構(gòu)造函數(shù)接收兩個(gè)參數(shù):目標(biāo)對(duì)象和處理程序?qū)ο?。?少其中任何一個(gè)參數(shù)都會(huì)拋出 TypeError。返回一個(gè)代理對(duì)象 如:new Proxy(target, handler);
要?jiǎng)?chuàng)建空代理,可以傳一個(gè)簡(jiǎn)單的對(duì)象字面量作為處理程序?qū)ο?,從而讓所有操作暢通無(wú)阻地抵達(dá)目標(biāo)對(duì)象。
const target = { id: 'target' }; //target:目標(biāo)對(duì)象 const handler = {}; //handler:是一個(gè)普通對(duì)象,其中可以重寫(xiě)底層實(shí)現(xiàn) //創(chuàng)建空對(duì)象 const proxy = new Proxy(target, handler); // id 屬性會(huì)訪(fǎng)問(wèn)同一個(gè)值 console.log(target.id); // target console.log(proxy.id); // target // 給目標(biāo)屬性賦值會(huì)反映在兩個(gè)對(duì)象上 因?yàn)閮蓚€(gè)對(duì)象訪(fǎng)問(wèn)的是同一個(gè)值 target.id = 'foo'; console.log(target.id); // foo console.log(proxy.id); // foo // 給代理屬性賦值會(huì)反映在兩個(gè)對(duì)象上 因?yàn)檫@個(gè)賦值會(huì)轉(zhuǎn)移到目標(biāo)對(duì)象 proxy.id = 'bar'; console.log(target.id); // bar console.log(proxy.id); // bar // hasOwnProperty()方法在兩個(gè)地方 也都會(huì)應(yīng)用到目標(biāo)對(duì)象 console.log(target.hasOwnProperty('id')); // true console.log(proxy.hasOwnProperty('id')); // true // Proxy.prototype 是 undefined 因此不能使用 instanceof 操作符 console.log(target instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype 'undefined' in instanceof check // 嚴(yán)格相等可以用來(lái)區(qū)分代理和目標(biāo) console.log(target === proxy); // false
3.2 定義捕獲器
使用代理的主要目的是可以定義捕獲器(trap)。捕獲器就是在處理程序?qū)ο笾卸x的“基本操作的攔截器”。
每個(gè)處理程序?qū)ο罂梢园銈€(gè)或多個(gè)捕獲器,每個(gè)捕獲器都對(duì)應(yīng)一種基本操作,可以直接或間接在代理對(duì)象上調(diào)用。
每次在代理對(duì)象上調(diào)用這些基本操作時(shí),代理可以在這些操作傳播到目標(biāo)對(duì)象之前先調(diào)用捕獲器函數(shù),從而攔截并修改相應(yīng)的行為。
所有捕獲器都可以訪(fǎng)問(wèn)相應(yīng)的參數(shù),基于這些參數(shù)可以重建被捕獲方法的原始行為。比如,get()捕獲器會(huì)接收到目標(biāo)對(duì)象、要查詢(xún)的屬性和代理對(duì)象三個(gè)參數(shù)。
const target = { foo: "bar", }; const handler = { // 捕獲器在處理程序?qū)ο笾幸苑椒麨殒I get(trapTarget, property, receiver) { //trapTarget - 目標(biāo)對(duì)象 //property - 要查詢(xún)的屬性 //receiver - 代理對(duì)象 return "handler override"; }, }; const proxy = new Proxy(target, handler); console.log(target.foo); // bar console.log(proxy.foo); // handler override
3.3 捕獲器不變式
使用捕獲器幾乎可以改變所有基本方法的行為,但也不是沒(méi)有限制。
根據(jù) ECMAScript 規(guī)范,每個(gè)捕獲的方法都知道目標(biāo)對(duì)象上下文、捕獲函數(shù)簽名,而捕獲處理程序的行為必須遵循“捕獲器不變式”(trap invariant)。捕獲器不變式因方法不同而異,但通常都會(huì)防止捕獲器定義出現(xiàn)過(guò)于反常的行為。
比如,如果目標(biāo)對(duì)象有一個(gè)不可配置且不可寫(xiě)的數(shù)據(jù)屬性,那么在捕獲器返回一個(gè)與該屬性不同的值時(shí),會(huì)拋出 TypeError:
const target = {}; Object.defineProperty(target, "foo", { configurable: false, writable: false, value: "bar", }); const handler = { get() { return "qux"; }, }; const proxy = new Proxy(target, handler); console.log(proxy.foo); // TypeError
3.4 可撤銷(xiāo)代理
有時(shí)候可能需要中斷代理對(duì)象與目標(biāo)對(duì)象之間的聯(lián)系。
Proxy 也暴露了 revocable()方法,這個(gè)方法支持撤銷(xiāo)代理對(duì)象與目標(biāo)對(duì)象的關(guān)聯(lián)。
后續(xù)可直接調(diào)用撤銷(xiāo)函數(shù) revoke() 來(lái)撤銷(xiāo)代理。
撤銷(xiāo)代理之后再調(diào)用代理會(huì)拋出 TypeError,撤銷(xiāo)函數(shù)和代理對(duì)象是在實(shí)例化時(shí)同時(shí)生成的:
const target = { foo: "bar", }; const handler = { get() { return "intercepted"; }, }; const { proxy, revoke } = Proxy.revocable(target, handler); console.log(proxy.foo); // intercepted console.log(target.foo); // bar revoke(); console.log(proxy.foo); // TypeError
4.代理捕獲器與反射方法
4.1 get()
get()捕獲器會(huì)在獲取屬性值的操作中被調(diào)用。對(duì)應(yīng)的反射 API 方法為 Reflect.get()
const myTarget = {}; const proxy = new Proxy(myTarget, { get(target, property, receiver) { console.log("get()"); return Reflect.get(...arguments); }, }); proxy.foo; // 觸發(fā)get()捕獲器
返回值 返回值無(wú)限制。
攔截的操作
- proxy.property
- proxy[property]
- Object.create(proxy)[property]
- Reflect.get(proxy, property, receiver)
捕獲器處理程序參數(shù)
- target:目標(biāo)對(duì)象。
- property:引用的目標(biāo)對(duì)象上的字符串鍵屬性。① - receiver:代理對(duì)象或繼承代理對(duì)象的對(duì)象。
捕獲器不變式 如果 target.property 不可寫(xiě)且不可配置,則處理程序返回的值必須與 target.property 匹配。 如果 target.property 不可配置且[[Get]]特性為 undefined,處理程序的返回值也必須是 undefined。
4.2 set()
set()捕獲器會(huì)在設(shè)置屬性值的操作中被調(diào)用。對(duì)應(yīng)的反射 API 方法為 Reflect.set()。
const myTarget = {}; const proxy = new Proxy(myTarget, { set(target, property, value, receiver) { console.log("set()"); return Reflect.set(...arguments); }, }); proxy.foo = "bar"; // 觸發(fā)set()捕獲器
返回值 返回 true 表示成功;返回 false 表示失敗,嚴(yán)格模式下會(huì)拋出 TypeError。
攔截的操作
- proxy.property = value
- proxy[property] = value
- Object.create(proxy)[property] = value
- Reflect.set(proxy, property, value, receiver)
捕獲器處理程序參數(shù)
- target:目標(biāo)對(duì)象。
- property:引用的目標(biāo)對(duì)象上的字符串鍵屬性。
- value:要賦給屬性的值。
- receiver:接收最初賦值的對(duì)象。
捕獲器不變式 如果 target.property 不可寫(xiě)且不可配置,則不能修改目標(biāo)屬性的值。 如果 target.property 不可配置且[[Set]]特性為 undefined,則不能修改目標(biāo)屬性的值。 在嚴(yán)格模式下,處理程序中返回 false 會(huì)拋出 TypeError。
4.3 has()
has()捕獲器會(huì)在 in 操作符中被調(diào)用。對(duì)應(yīng)的反射 API 方法為 Reflect.has()。
const myTarget = {}; const proxy = new Proxy(myTarget, { has(target, property) { console.log("has()"); return Reflect.has(...arguments); }, }); "foo" in proxy; //觸發(fā) has()捕獲器
返回值 has()必須返回布爾值,表示屬性是否存在。返回非布爾值會(huì)被轉(zhuǎn)型為布爾值。
攔截的操作
- property in proxy
- property in Object.create(proxy)
- with(proxy) {(property);}
- Reflect.has(proxy, property)
捕獲器處理程序參數(shù)
- target:目標(biāo)對(duì)象。
- property:引用的目標(biāo)對(duì)象上的字符串鍵屬性。
捕獲器不變式 如果 target.property 存在且不可配置,則處理程序必須返回 true。 如果 target.property 存在且目標(biāo)對(duì)象不可擴(kuò)展,則處理程序必須返回 true。
4.4 deleteProperty()
deleteProperty()捕獲器會(huì)在 delete 操作符中被調(diào)用。對(duì)應(yīng)的反射 API 方法為 Reflect.deleteProperty()。
const myTarget = {}; const proxy = new Proxy(myTarget, { deleteProperty(target, property) { console.log("deleteProperty()"); return Reflect.deleteProperty(...arguments); }, }); delete proxy.foo; // 觸發(fā)deleteProperty()捕獲器
- 返回值 deleteProperty()必須返回布爾值,表示刪除屬性是否成功。返回非布爾值會(huì)被轉(zhuǎn)型為布爾值。
- 攔截的操作
- delete proxy.property
- delete proxy[property]
- Reflect.deleteProperty(proxy, property)
- 捕獲器處理程序參數(shù)
- target:目標(biāo)對(duì)象。
- property:引用的目標(biāo)對(duì)象上的字符串鍵屬性。
- 捕獲器不變式 如果自有的 target.property 存在且不可配置,則處理程序不能刪除這個(gè)屬性。
4.5 apply()
apply()捕獲器會(huì)在調(diào)用函數(shù)時(shí)中被調(diào)用。對(duì)應(yīng)的反射 API 方法為 Reflect.apply()。
const myTarget = () => {}; const proxy = new Proxy(myTarget, { apply(target, thisArg, ...argumentsList) { console.log("apply()"); return Reflect.apply(...arguments); }, }); proxy(); // 觸發(fā)apply()捕獲器
- 返回值 返回值無(wú)限制。
- 攔截的操作
- proxy(...argumentsList)
- Function.prototype.apply(thisArg, argumentsList)
- Function.prototype.call(thisArg, ...argumentsList)
- Reflect.apply(target, thisArgument, argumentsList)
- 捕獲器處理程序參數(shù)
- target:目標(biāo)對(duì)象。
- thisArg:調(diào)用函數(shù)時(shí)的 this 參數(shù)。
- argumentsList:調(diào)用函數(shù)時(shí)的參數(shù)列表
- 捕獲器不變式 target 必須是一個(gè)函數(shù)對(duì)象。
4.6 construct()
construct()捕獲器會(huì)在 new 操作符中被調(diào)用。對(duì)應(yīng)的反射 API 方法為 Reflect.construct()。
const myTarget = function () {}; const proxy = new Proxy(myTarget, { construct(target, argumentsList, newTarget) { console.log("construct()"); return Reflect.construct(...arguments); }, }); new proxy(); // 觸發(fā)construct()捕獲器
- 返回值 construct()必須返回一個(gè)對(duì)象。
- 攔截的操作
- new proxy(...argumentsList)
- Reflect.construct(target, argumentsList, newTarget)
- 捕獲器處理程序參數(shù)
- target:目標(biāo)構(gòu)造函數(shù)
- argumentsList:傳給目標(biāo)構(gòu)造函數(shù)的參數(shù)列表。
- newTarget:最初被調(diào)用的構(gòu)造函數(shù)。
- 捕獲器不變式 target 必須可以用作構(gòu)造函數(shù)。
還有另外七種捕獲器:
- defineProperty()捕獲器會(huì)在 Object.defineProperty()中被調(diào)用。
- getOwnPropertyDescriptor()捕獲器會(huì)在 Object.getOwnPropertyDescriptor()中被調(diào) 用。
- ownKeys()捕獲器會(huì)在 Object.keys()及類(lèi)似方法中被調(diào)用。
- getPrototypeOf()捕獲器會(huì)在 Object.getPrototypeOf()中被調(diào)用。
- setPrototypeOf()捕獲器會(huì)在 Object.setPrototypeOf()中被調(diào)用。
- isExtensible()捕獲器會(huì)在 Object.isExtensible()中被調(diào)用。
- preventExtensions()捕獲器會(huì)在 Object.preventExtensions()中被調(diào)用。
這七種捕獲器詳細(xì)介紹可參考MDN - Proxy。
參考博客
《Javascript 高級(jí)程序設(shè)計(jì)(第 4 版)》(JS 紅寶書(shū))
以上就是JS 中Proxy代理和 Reflect反射方法示例詳解的詳細(xì)內(nèi)容,更多關(guān)于JS Proxy代理Reflect反射的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript 對(duì)象比較實(shí)現(xiàn)代碼
js對(duì)象比較實(shí)現(xiàn)代碼。2009-04-04Javascript讀取json文件方法實(shí)例總結(jié)
json文件是一種輕量級(jí)的數(shù)據(jù)交互格式,下面這篇文章主要給大家介紹了關(guān)于Javascript讀取json文件方法的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11javascript 函數(shù)聲明與函數(shù)表達(dá)式的區(qū)別介紹
javascript中的函數(shù)聲明與函數(shù)表達(dá)式使用比較頻繁,可能很多的朋友都不知道他們之間的區(qū)別,在此為大家詳細(xì)介紹下,希望對(duì)大家有所幫助2013-10-10Code:loadScript( )加載js的功能函數(shù)
Code:loadScript( )加載js的功能函數(shù)...2007-02-02