這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
一、介紹
定義: 用於定義基本操作的自定義行為
本質: 修改的是程式預設形為,就形同於在程式語言層面上做修改,屬於超程式設計(meta programming)
超程式設計(Metaprogramming,又譯超程式設計,是指某類計算機程式的編寫,這類計算機程式編寫或者操縱其它程式(或者自身)作為它們的資料,或者在執行時完成部分本應在編譯時完成的工作
一段程式碼來理解
#!/bin/bash # metaprogram echo '#!/bin/bash' >program for ((I=1; I<=1024; I++)) do echo "echo $I" >>program done chmod +x program
這段程式每執行一次能幫我們生成一個名為program
的檔案,檔案內容為1024行echo
,如果我們手動來寫1024行程式碼,效率顯然低效
- 超程式設計優點:與手工編寫全部程式碼相比,程式設計師可以獲得更高的工作效率,或者給與程式更大的靈活度去處理新的情形而無需重新編譯
Proxy
亦是如此,用於建立一個物件的代理,從而實現基本操作的攔截和自定義(如屬性查詢、賦值、列舉、函式呼叫等)
二、用法
Proxy
為 建構函式,用來生成 Proxy
例項
var proxy = new Proxy(target, handler)
引數
target
表示所要攔截的目標物件(任何型別的物件,包括原生陣列,函式,甚至另一個代理))
handler
通常以函式作為屬性的物件,各屬性中的函式分別定義了在執行各種操作時代理 p
的行為
handler解析
關於handler
攔截屬性,有如下:
- get(target,propKey,receiver):攔截物件屬性的讀取
- set(target,propKey,value,receiver):攔截物件屬性的設定
- has(target,propKey):攔截
propKey in proxy
的操作,返回一個布林值 - deleteProperty(target,propKey):攔截
delete proxy[propKey]
的操作,返回一個布林值 - ownKeys(target):攔截
Object.keys(proxy)
、for...in
等迴圈,返回一個陣列 - getOwnPropertyDescriptor(target, propKey):攔截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回屬性的描述物件 - defineProperty(target, propKey, propDesc):攔截
Object.defineProperty(proxy, propKey, propDesc)
,返回一個布林值 - preventExtensions(target):攔截
Object.preventExtensions(proxy)
,返回一個布林值 - getPrototypeOf(target):攔截
Object.getPrototypeOf(proxy)
,返回一個物件 - isExtensible(target):攔截
Object.isExtensible(proxy)
,返回一個布林值 - setPrototypeOf(target, proto):攔截
Object.setPrototypeOf(proxy, proto)
,返回一個布林值 - apply(target, object, args):攔截 Proxy 例項作為函式呼叫的操作
- construct(target, args):攔截 Proxy 例項作為建構函式呼叫的操作
Reflect
若需要在Proxy
內部呼叫物件的預設行為,建議使用Reflect
,其是ES6
中操作物件而提供的新 API
基本特點:
- 只要
Proxy
物件具有的代理方法,Reflect
物件全部具有,以靜態方法的形式存在 - 修改某些
Object
方法的返回結果,讓其變得更合理(定義不存在屬性行為的時候不報錯而是返回false
) - 讓
Object
操作都變成函式行為
下面我們介紹proxy
幾種用法:
get()
get
接受三個引數,依次為目標物件、屬性名和 proxy
例項本身,最後一個引數可選
var person = { name: "張三" }; var proxy = new Proxy(person, { get: function(target, propKey) { return Reflect.get(target,propKey) } }); proxy.name // "張三"
get
能夠對陣列增刪改查進行攔截,下面是試下你陣列讀取負數的索引
function createArray(...elements) { let handler = { get(target, propKey, receiver) { let index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } return Reflect.get(target, propKey, receiver); } }; let target = []; target.push(...elements); return new Proxy(target, handler); } let arr = createArray('a', 'b', 'c'); arr[-1] // c
注意:如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則會報錯
const target = Object.defineProperties({}, { foo: { value: 123, writable: false, configurable: false }, }); const handler = { get(target, propKey) { return 'abc'; } }; const proxy = new Proxy(target, handler); proxy.foo // TypeError: Invariant check failed
set()
set
方法用來攔截某個屬性的賦值操作,可以接受四個引數,依次為目標物件、屬性名、屬性值和 Proxy
例項本身
假定Person
物件有一個age
屬性,該屬性應該是一個不大於 200 的整數,那麼可以使用Proxy
保證age
的屬性值符合要求
let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value > 200) { throw new RangeError('The age seems invalid'); } } // 對於滿足條件的 age 屬性以及其他屬性,直接儲存 obj[prop] = value; } }; let person = new Proxy({}, validator); person.age = 100; person.age // 100 person.age = 'young' // 報錯 person.age = 300 // 報錯
如果目標物件自身的某個屬性,不可寫且不可配置,那麼set
方法將不起作用
const obj = {}; Object.defineProperty(obj, 'foo', { value: 'bar', writable: false, }); const handler = { set: function(obj, prop, value, receiver) { obj[prop] = 'baz'; } }; const proxy = new Proxy(obj, handler); proxy.foo = 'baz'; proxy.foo // "bar"
注意,嚴格模式下,set
代理如果沒有返回true
,就會報錯
'use strict'; const handler = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; // 無論有沒有下面這一行,都會報錯 return false; } }; const proxy = new Proxy({}, handler); proxy.foo = 'bar'; // TypeError: 'set' on proxy: trap returned falsish for property 'foo'
deleteProperty()
deleteProperty
方法用於攔截delete
操作,如果這個方法丟擲錯誤或者返回false
,當前屬性就無法被delete
命令刪除
var handler = { deleteProperty (target, key) { invariant(key, 'delete'); Reflect.deleteProperty(target,key) return true; } }; function invariant (key, action) { if (key[0] === '_') { throw new Error(`無法刪除私有屬性`); } } var target = { _prop: 'foo' }; var proxy = new Proxy(target, handler); delete proxy._prop // Error: 無法刪除私有屬性
注意,目標物件自身的不可配置(configurable)的屬性,不能被deleteProperty
方法刪除,否則報錯
取消代理
Proxy.revocable(target, handler);
三、使用場景
Proxy
其功能非常類似於設計模式中的代理模式,常用功能如下:
- 攔截和監視外部對物件的訪問
- 降低函式或類的複雜度
- 在複雜操作前對操作進行校驗或對所需資源進行管理
使用 Proxy
保障資料型別的準確性
let numericDataStore = { count: 0, amount: 1234, total: 14 }; numericDataStore = new Proxy(numericDataStore, { set(target, key, value, proxy) { if (typeof value !== 'number') { throw Error("屬性只能是number型別"); } return Reflect.set(target, key, value, proxy); } }); numericDataStore.count = "foo" // Error: 屬性只能是number型別 numericDataStore.count = 333 // 賦值成功
宣告瞭一個私有的 apiKey
,便於 api
這個物件內部的方法呼叫,但不希望從外部也能夠訪問 api._apiKey
let api = { _apiKey: '123abc456def', getUsers: function(){ }, getUser: function(userId){ }, setUser: function(userId, config){ } }; const RESTRICTED = ['_apiKey']; api = new Proxy(api, { get(target, key, proxy) { if(RESTRICTED.indexOf(key) > -1) { throw Error(`${key} 不可訪問.`); } return Reflect.get(target, key, proxy); }, set(target, key, value, proxy) { if(RESTRICTED.indexOf(key) > -1) { throw Error(`${key} 不可修改`); } return Reflect.get(target, key, value, proxy); } }); console.log(api._apiKey) api._apiKey = '987654321' // 上述都丟擲錯誤
還能透過使用Proxy
實現觀察者模式
觀察者模式(Observer mode)指的是函式自動觀察資料物件,一旦物件有變化,函式就會自動執行
observable
函式返回一個原始物件的 Proxy
代理,攔截賦值操作,觸發充當觀察者的各個函式
const queuedObservers = new Set(); const observe = fn => queuedObservers.add(fn); const observable = obj => new Proxy(obj, {set}); function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); queuedObservers.forEach(observer => observer()); return result; }
觀察者函式都放進Set
集合,當修改obj
的值,在會set
函式中攔截,自動執行Set
所有的觀察者
參考文獻
- https://es6.ruanyifeng.com/#docs/proxy
- https://vue3js.cn/es6