日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

反射和代理的具体应用

發布時間:2024/4/13 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 反射和代理的具体应用 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文發布于 github.com/ta7sudan/no…, 如需轉載請保留原作者 @ta7sudan.

ES6 為我們提供了許多新的 API, 其中個人覺得最有用的(之一)便是代理了. 代理和反射都被歸為反射 API, 那什么是反射? 根據 wiki 上的解釋.

反射是指計算機程序在運行時(Run time)可以訪問、檢測和修改它本身狀態或行為的一種能力。

所以廣義上來說, 并非只有使用了 Proxy Reflect 相關的 API 才叫反射, 而是只要在運行時訪問, 檢測和修改自身狀態和行為的都可以認為是用到了反射. 拿比較常見的 new 無關的構造函數來說, 我們常常會這樣實現.

function Person() {var self = this instanceof Person ? this : Object.create(Person.prototype);return self; } 復制代碼

像上面這樣, 我們在運行時通過檢測 this 進而檢測是否是通過 new 調用的函數, 從而決定返回值, 這也算是反射.

很多語言都提供了反射機制, 即使是匯編, 也能夠在運行時修改自身的代碼(誰讓指令和數據是在一起呢...不過即便不在一起也是可以的). 那反射和代理到底有什么用?

有人認為反射破壞了封裝, 但是它也帶來了更多的靈活性, 使得原本無法實現或難以實現的事情變得很容易實現, 盡管有缺點, 但缺點是我們可以避免的(如果有人使用了反射來破壞封裝, 那說明他在使用的時候已經清楚這樣做的結果, 產生的后果也應當自己承擔, 而如果不是必要, 則大部分時候也不會用到反射, 不存在破壞封裝), 而帶來的好處相較缺點則是明顯劃算的.

就 API 而言, 反射和代理用起來是很簡單的, 所以這里就不提了. 下面以比較常用的 get trap 來說明代理的實際應用場景. 后文中的反射泛指反射 API, 即包含了 Proxy 和 Reflect, 并不再區分.

考慮我們有一個對象 obj, 對象具有一個 sayHello 方法, 我們可能會這么寫.

var obj = {}; obj.sayHello = function () {console.log('hello'); }; 復制代碼

在初始化的時候便定義了 sayHello 方法, 但可能有時候我們覺得這沒必要, 畢竟一個函數表達式也是有開銷的. 當然你可以說我們直接在字面量里寫好 sayHello 不就行了, 為什么一定要用函數表達式? 這里只是演示, 不用在意細節, 總之我們希望在運行時某一時刻再實例化這個 sayHello 方法, 而不是一開始就實例化它, 原因可能是應用啟動速度比較重要. 那我們可能會這么寫.

var obj = {};setTimeout(() => {obj.sayHello = function () {console.log('hello');}; }, 3000); 復制代碼

現在我們過了 3 秒才實例化了 sayHello 方法, 的確滿足了我們前面說的, 在運行時某一時刻實例化的需求, 至少我們的啟動速度提升了那么一點. 那假如現在我們希望這個某一時刻不是 3 秒, 而是我們調用 sayHello 的時候呢? 換句話說, 如果調用 sayHello 時它還沒有實例化, 則我們先實例化它, 再調用它.

用 Proxy 我們可以這樣寫.

var obj = {};var pobj = new Proxy(obj, {get(target, key) {if (key === 'sayHello') {if (!target[key]) {target[key] = function () {console.log('hello');};}return target[key];}} });pobj.sayHello(); 復制代碼

很好, 這樣我們就實現了在調用時實例化, 并且只實例化一次 sayHello 之后不會重復實例化. 但是也有人會說, 這不就是一個 getter 嗎? 這種事情用 Object.defineProperty() 也能做到. 比如.

var obj = {};Object.defineProperty(obj, 'sayHello', {get() {if (!this._sayHello) {this._sayHello = function () {console.log('hello');};}return this._sayHello;} });obj.sayHello(); 復制代碼

的確, 從這個角度來看, 使用 Proxy 和使用 Object.defineProperty() 幾乎沒什么區別. 而另一方面是, 盡管這兩種方法是沒有在初始化的時候實例化 sayHello 而是把這一過程推遲到調用 sayHello 的時刻了, 但是使用 Proxy 要創建一個代理對象, 使用 Object.defineProperty() 也要執行一次函數調用, 它們的開銷可能比初始化時候使用一個函數表達式來得更大, 這有什么意義?

get trap 不僅僅是 getter

前面的例子中我們遇到了兩個問題, 一個是 Object.defineProperty() 某種意義上也能完成 Proxy 一樣的功能, 那 Proxy 有什么意義? get trap 有什么意義? 另一個是創建一個 Proxy 對象的開銷并不一定比使用一個函數表達式來得小, 這又有什么意義?

為了回答這兩個問題, 現在我們考慮 obj 不僅僅有一個 sayHello 方法, 它有成百上千個方法, 每個方法打印了方法名. 使用 Proxy 的話, 我們可以這樣寫.

var obj = {};var pobj = new Proxy(obj, {get(target, key) {if (!target[key]) {target[key] = function () {console.log(key);};}return target[key];} });pobj.sayHello(); pobj.sayGoodBye(); // sayHello // sayGoodBye 復制代碼

依舊簡短. 那用 Object.defineProperty() 呢? 不可能實現, 而即便是確定只有 100 個方法, 并且它們名字確定, 也需要調用 100 次 Object.defineProperty(), 對于函數表達式來說, 也是一樣的. 而從開銷的角度來看呢? 這時候 Proxy 依然只創建了一個代理對象, 而即便是可以使用 Object.defineProperty() 或函數表達式, 它們也要調用成百上千次.

當我們使用 obj.xxx() 去調用一個 xxx() 方法時, obj 對象本身并不知道自己是否具有 xxx() 方法, 而反射就像是一面鏡子, 讓 obj 能夠知道自己是否具有 xxx() 方法, 并且根據情況做出對應的處理.

盡管我們可以在運行時通過 Object.defineProperty() 或函數表達式動態地為 obj 對象添加方法, 但這是因為我們知道 obj 在那個時候是否存在對應方法, 而不是 obj 本身知道自己當時是否存在對應方法. 換句話說, 我們在使用對象的方法時, 總是要先知道方法名, 哪怕能夠在運行時知道, 但是[知道]這個動作也必須發生在[方法調用]這個動作之前. 這就導致了一些現實問題難以被優雅地解決.

比如前面的 obj 對象是我們暴露的 API, 給用戶使用, 它的方法都是按需實例化的. 如果沒有 Proxy, 則用戶什么時候調用 obj 的方法我們是不知道的, 所以[知道]這一動作是不可能在[方法調用]之前, 我們也就沒辦法按需實例化. 當然用戶是能夠在[方法調用]之前[知道]什么時候會有方法調用的, 但我們不可能讓用戶自己來實例化方法.

從編譯器角度來看, Proxy 是攔截了對象所有屬性的右值查詢, 而 Object.defineProperty() 則只是攔截了特定屬性的右值查詢, 這意味著 Object.defineProperty() 必須知道屬性名這一信息, 而 Proxy 則不需要知道.

前置代理和后置代理

大部分時候我們使用的都是前置代理, 即我們把直接和代理對象進行交互(所有操作都發生在代理對象身上)的方式叫做前置代理. 那什么是后置代理? 看代碼.

var pobj = new Proxy({}, {get(target, key) {if (!target[key]) {target[key] = function () {console.log(key);};}return target[key];} });var obj = Object.create(pobj); obj.sayHello(); obj.sayGoodBye(); 復制代碼

借助原型鏈機制, 我們直接和 obj 進行交互而不是和代理對象進行交互, 只有當 obj 不存在對應方法時才會通過原型鏈去查找代理對象.

可以看出來的是, 對于原本存在于目標對象(target)上的屬性, 使用代理前置開銷更大, 因為明明已經具有對應屬性了卻還要經過一次代理對象, 而使用代理后置開銷更小. 對于那些不存在的屬性, 使用后置代理開銷更大, 因為不僅要經過原型鏈查找還要經過一次代理對象, 而使用前置代理只需要經過一次代理對象. 當然也可能引擎有特殊的優化技巧使得這種性能差異并不明顯, 所以也看個人喜歡采用哪種方式吧.

Reflect

講了這么多都是在講 Proxy, 那 Reflect 呢? 它和以前的一些方法只有一些細微差別, 所以它的意義是什么? 有什么用?

Reflect 的方法和 Proxy 的方法是成對出現的, 和以前的一些方法相比, Reflect 的方法對參數的處理不同或返回值不同, 盡管很細微的差別, 但是當和 Proxy 配合使用的時候, 使用以前的方法可能導致 Proxy 對象和普通對象的一些行為不一致, 而使用 Reflect 則不會有這樣的問題, 所以建議在 Proxy 中都使用 Reflect 的對應方法.

另一方面是 Reflect 暴露的 API 相對更加底層, 性能會好一些.

最后是有些事情只能通過 Reflect 實現, 具體參考這個例子. 但是個人感覺這個例子并不是很好, 畢竟這個場景太少見了.

讓我們先來回顧一下前面后置代理的例子.

var pobj = new Proxy({}, {get(target, key) {if (!target[key]) {target[key] = function () {console.log(key);};}return target[key];} });var obj = Object.create(pobj); obj.sayHello(); obj.sayGoodBye(); 復制代碼

在這個例子中, 調用 obj 上一開始不存在的方法最終都會通過原型鏈找到代理對象, 進而找到 target 也即空對象, 然后對空對象實例化對應的方法. 這里的原型鏈查找總是讓人感覺不太爽, 明明進入到 get trap 就肯定說明 obj 一開始不存在對應方法, 那我們理應可以在這時候給 obj 設置對應方法, 這樣下次調用的時候就不會進行原型鏈的查找了, 為什么非要給那個毫無卵用的空對象設置方法, 導致每次對 obj 進行方法調用還是要進行原型鏈查找?

于是我們想起 get trap 還有個 receiver 參數, 大多數地方都寫著 receiver 就是代理對象, 也即我們這里的 pobj, 其實不是, 準確說它是實際發生屬性查找的對象, 也即我們這里的 obj, 有點像 DOM 事件中 event.target 的意思.

于是我們馬上將原有的寫法改成這樣.

var pobj = new Proxy({}, {get(target, key, receiver) {if (!receiver[key]) {receiver[key] = function () {console.log(key);};}return receiver[key];} });var obj = Object.create(pobj); obj.sayHello(); // RangeError: Maximum call stack size exceeded 復制代碼

看上去沒什么毛病, 然后我們立馬得到一個堆棧溢出的錯誤. 仔細看看我們發現關鍵問題就出在這個 receiver[key], 它對 obj.sayHello 進行了查找, 但此時 obj.sayHello 還未實例化, 于是無限對 obj.sayHello 進行查找, 最終導致堆棧溢出.

這里出現問題的根本原因是 a[b] 這樣的取值操作妥妥地會觸發 Proxy 的 get trap 的, 因為 Proxy 是更為底層的存在, 但是仔細想想我們的需求其實不是為了取值, 而是為了知道 obj 自身是否存在 sayHello 屬性, 從這一點來說, 我們沒必要使用 a[b] 這樣的方式來判斷, 我們可以用 hasOwnProperty(). 于是繼續改造.

var pobj = new Proxy({}, {get(target, key, receiver) {if (!receiver.hasOwnProperty(key)) {receiver[key] = function () {console.log(key);};}return receiver[key];} });var obj = Object.create(pobj); obj.sayHello(); // RangeError: Maximum call stack size exceeded 復制代碼

還是堆棧溢出, 因為 hasOwnProperty() 其實是 Object.prototype.hasOwnProperty(), 意味著在原型鏈的盡頭, 而 pobj 在原型鏈上更近的位置, 于是相當于 receiver/obj 并不存在 hasOwnProperty(), 于是變成了對 obj.hasOwnProperty() 無限查找導致堆棧溢出.

那繼續吧, 我們直接用 Object.prototype.hasOwnProperty() 總行了吧.

var pobj = new Proxy({}, {get(target, key, receiver) {if (!Object.prototype.hasOwnProperty.call(receiver, key)) {receiver[key] = function () {console.log(key);};}return receiver[key];} });var obj = Object.create(pobj); obj.sayHello(); obj.sayHello(); // sayHello // sayHello 復制代碼

到這里其實問題已經解決了, 我們的后置代理只會在第一次未實例化方法時進行原型鏈查找, 之后調用 obj.sayHello() 都是直接和 obj 進行交互, 既沒有原型鏈查找也沒有代理. 那這和 Reflect 有什么關系?

其實這里用 Reflect 會更好一點, 一方面相對于長長的 Object.prototype.hasOwnProperty.call 來說會更短更直觀, 一方面性能也好一點(反正 Node 源碼中是把 call 換成了 Reflect).

var pobj = new Proxy({}, {get(target, key, receiver) {if (!Reflect.has(receiver, key)) {Reflect.set(receiver, key, function () {console.log(key);});return Reflect.get(receiver, key);} else {return Reflect.get(target, key);}} });var obj = Object.create(pobj); obj.sayHello(); obj.sayHello(); console.log(obj.hasOwnProperty('sayHello')); 復制代碼

最終我們改成了這樣子, 和前面又稍稍有一些不一樣, 有個 else 把非 obj 自身的屬性查找轉發給了 target, 因為后面有個 hasOwnProperty() 調用, 如果不轉發給 target 的話, 則導致繼承自 Object 的屬性和方法全都會產生堆棧溢出.

后續補充: 這里我犯了兩個錯誤, 為了說明這個錯誤所以前面的內容不再修改, 當作標本.

先讓我們來看看最終版本的 if (!Reflect.has(receiver, key)) 這段邏輯和之前的 if (!receiver[key]), 我們說, 最終我們希望的是檢測對應屬性是否存在, 這話嚴格來說也不算錯. 但每個人對存在的定義可能都不同, 有人認為 receiver[key] === undefined 就算不存在, 而如果有人覺得 Reflect.has(receiver, key) 為 false 算不存在, 但其實它們是很不一樣的. 這里我們準確定義應該是, receiver[key] === undefined 是做的可用性檢測, 而 Reflect.has(receiver, key) 是做的存在性檢測. 所以這里用 Reflect.has(receiver, key) 嚴格來說也不能算錯, 但是很容易被人忽視的一點就是, 在后置代理中, receiver 對象的任何存在但不可用的屬性, 都會導致無法委托到原型鏈上的代理對象. 這也算是使用后置代理的一點限制吧.

而第二個錯誤, 則是實實在在的錯誤了. 前面說過, 一旦進入到 get trap 就肯定說明 obj 一開始不存在對應方法, 既然我們已經知道不存在對應方法了, 那為什么還要用 if (!Reflect.has(receiver, key)) 做存在性檢測? 所以這步邏輯是多余的. 但是另一方面是, 很多 Object.prototype 上的方法, 其實 receiver 也是不存在的, 所以當調用這些方法的時候也是會進入到 get trap 的, 我們依舊需要把它們轉發到 target 上去. 于是我們應當寫成這樣.

var pobj = new Proxy({}, {get(target, key, receiver) {if (Reflect.has(target, key)) {return Reflect.get(target, key);}Reflect.set(receiver, key, function () {console.log(key);});return Reflect.get(receiver, key);} });var obj = Object.create(pobj); obj.sayHello(); obj.sayHello(); console.log(obj.hasOwnProperty('sayHello')); 復制代碼

其實也沒有省太多事就是了, 雖然我們去掉了一個判斷, 但是為了保證繼承自 Object 的方法正常使用, 又引入了一個新的判斷, 看上去只是把 if-else 中的邏輯調換了位置而已, 不過邏輯上講, 這樣更合理一些吧.

其他細節

對于數組使用代理的話, get trap 和 set trap 也可以攔截到數組方法, 比如 forEach push 等, 因為實際上這些方法也會對數組使用如 arr[index] 這樣的形式去獲取和設置值.

另外 Proxy 的各個 trap 中的 this 均是指向 handler 對象, 而不是代理對象, 也不是目標對象, 而 trap 中返回函數(如果可以返回一個函數的話)的 this 指向的是代理對象而不是目標對象. 即

var obj = {}, handler = {get(target, key, receiver) {console.log(this === target);console.log(this === receiver);console.log(this === handler);} };var pobj = new Proxy(obj, handler); pobj.name; // false // false // truevar obj = {}, handler = {get(target, key, receiver) {return function () {console.log(this === target);console.log(this === receiver);console.log(this === handler);};} };var pobj = new Proxy(obj, handler); pobj.test(); // false // true // false 復制代碼

這里也順便提下 Object.defineProperty() 對 this 的處理. Object.defineProperty() 的 getter/setter 中的 this 指向的是目標對象而非屬性描述符對象, 如果 getter 中返回函數, 則函數的 this 也是指向目標對象.

var obj = {name: 'aaa' };Object.defineProperty(obj, 'test', {get() {console.log(this.name);} });obj.test; // aaavar obj = {name: 'aaa' };Object.defineProperty(obj, 'test', {get() {return function () {console.log(this.name);};} });obj.test(); // aaa 復制代碼

參考資料

  • zh.wikipedia.org/wiki/反射_(計算…
  • www.zhihu.com/question/28…
  • www.zhihu.com/question/27…
  • www.zhihu.com/question/27…
  • developer.mozilla.org/en-US/docs/…
  • developer.mozilla.org/en-US/docs/…
  • github.com/getify/You-…

總結

以上是生活随笔為你收集整理的反射和代理的具体应用的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。