Proxy對象用于創(chuàng)建一個對象的代理,從而實現(xiàn)基本操作的攔截和自定義(如屬性查找八堡、賦值淤井、枚舉卸留、函數(shù)調用等)蓉冈。Proxy被用于許多庫和瀏覽器框架上,例如vue3就是使用Proxy來實現(xiàn)數(shù)據(jù)響應式的弟胀。本文帶你了解Proxy的用法與局限性狸演。
Proxy參數(shù)與說明
const target = {}
const handler = {
get(target, key, recevier) {
return target[key]
},
set(target, key, val, recevier) {
target[key] = val
return true
}
}
const proxy = new Proxy(target, handler)
參數(shù)
- target 需要包裝的對象,可以是任何變量
- handle 代理配置滑黔,通常是用函數(shù)作為屬性值的對象笆包,為了方便表達本文以
捕捉器函數(shù)
來稱呼這些屬性
??對proxy
進行操作時,如果handler
對象中存在相應的捕捉器函數(shù)
則運行這個函數(shù)略荡,如果不存在則直接對target
進行處理庵佣。
??在JavaScript中對于對象的大部分操作都存在內(nèi)部方法
,它是最底層的工作方式汛兜。例如對數(shù)據(jù)讀取時底層會調用[[Get]]
巴粪,寫入的時底層會調用[[Set]]
。我們不能直接通過方法名調用它,而Proxy
的代理配置
中的捕捉器函數(shù)
則可以攔截這些內(nèi)部方法的調用肛根。
內(nèi)部方法與捕捉器函數(shù)
下表描述了內(nèi)部方法
與捕捉器函數(shù)
的對應關系:
內(nèi)部方法 | 捕捉器函數(shù) | 函數(shù)參數(shù) | 函數(shù)返回值 | 劫持 |
---|---|---|---|---|
[[Get]] |
get |
target , property , recevier
|
any |
讀取屬性 |
[[Set]] |
set |
target , property , value 辫塌, recevier
|
boolean 表示操作是否 |
寫入屬性 |
[[HasProperty]] |
has |
target , property
|
boolean |
in 操作符 |
[[Delete]] |
deleteProperty |
target , property
|
boolean 表示操作是否 |
delete 操作符 |
[[Call]] |
apply |
target , thisArg , argumentsList
|
any |
函數(shù)調用 |
[[Construct]] |
construct |
target , argumentsList , newTarget
|
object |
new 操作符 |
[[GetPrototypeOf]] |
getPrototypeOf |
target |
object 或null
|
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
target , prototype
|
boolean 表示操作是否 |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
target |
boolean |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
target |
boolean 表示操作是否 |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
target , property , descriptor
|
boolean 表示操作是否 |
Object.defineProperty ObjectdefineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
target , property
|
object 或 undefined
|
Object.getOwnPropertyDescriptor for...in Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
target |
一個可枚舉object . |
Object.getOwnPropertyNames Object.getOwnPropertySymbols for...in Object.keys/values/entries |
捕捉器函數(shù)參數(shù)說明
-
target
是目標對象,被作為第一個參數(shù)傳遞給new Proxy
-
property
將被設置或獲取的屬性名或Symbol
-
value
要設置的新的屬性值 -
recevier
最初被調用的對象派哲。通常是proxy
本身,但是可能以其他方式被間接地調用(因此不一定是proxy
本身臼氨,后面我會說明) -
thisArg
被調用時的上下文對象 -
argumentsList
被調用時的參數(shù)數(shù)組 -
newTarget
最初被調用的構造函數(shù) -
descriptor
待定義或修改的屬性的描述符
這里我們重點講一下捕捉器函數(shù)參數(shù)的recevier
和newTarget
其他參數(shù)就不一一介紹,基本上一看就懂了芭届。
改造console.log
??在Proxy
的捕捉器函數(shù)
中使用console.log
很容易造成死循環(huán)储矩,因為如果console.log(poxy)
時會讀取Proxy
的屬性,可能會經(jīng)過捕捉器函數(shù)
褂乍,經(jīng)過捕捉器函數(shù)
再次console.log(poxy)
椰苟。為了方便調試,我這里改造了以下console.log
树叽。
// 通過當前是否是log模式來判斷是否是打印
let isLog = false
{
const logs = []
const platformLog = console.log
const resolvedPromise = Promise.resolve()
// 當前是否正在執(zhí)行l(wèi)ogs
let isFlush = false
console.log = (...args) => {
logs.push(args)
isFlush || logFlush()
}
const logFlush = () => {
isFlush = true
resolvedPromise.then(() => {
isLog = true
logs.forEach(args => {
platformLog.apply(platformLog, args)
})
logs.length = 0
isLog = false
isFlush = false
})
}
}
recevier與被代理方法上的this
??recevier
最初被調用的對象舆蝴,什么意思呢,就是誰調用的Proxy
經(jīng)過捕捉器函數(shù)
那么它就是誰题诵〗嗾蹋看下方實例說明
const animal = {
_name: '動物',
getName() {
isLog || console.log(this)
return this._name
}
}
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
isLog || console.log(recevier)
return target[key]
}
})
// 最初被調用的對象是animalProxy,
// 這里訪問時get捕捉器函數(shù)的recevier參數(shù)就是animalProxy
// 被代理的this就是animalProxy
animalProxy.getName()
const pig = {
// 通過原型性锭,繼承animalProxy
__proto__: animalProxy,
test: animalProxy
}
// pig中不存在name赠潦,通過原型查找,原型是Proxy草冈,讀取時經(jīng)過get捕捉器函數(shù)
// 最初被調用的對象時pig
// 這里訪問時get捕捉器函數(shù)的recevier參數(shù)就是pig
// 被代理的this就是pig
pig.getName()
// 最初被調用的對象是pig.test即為animalProxy
// 這里訪問時get捕捉器函數(shù)的recevier參數(shù)就是animalProxy
// 被代理的this就是animalProxy
pig.test.getName()
??上方示例清晰的說明了recevier
她奥,就是當調用proxy
對象時調用者是誰,其實與function
中this
的機制是一致的怎棱。
newTarget參數(shù)
??newTarget
最初被調用的構造函數(shù)哩俭,在es6
中添加了class對象的支持,而newTarget
也就是主要識別類中繼承關系的對象拳恋,比如看下方例子
const factoryClassProxy = type =>
new Proxy(type, {
construct(target, args, newTarget) {
console.log(newTarget)
const instance = new target(...args)
if (target.prototype !== newTarget.prototype) {
Object.setPrototypeOf(instance, newTarget.prototype)
}
return instance
}
})
const AnimalProxy = factoryClassProxy(
class {
name = '動物'
getName() {
return this.name
}
}
)
const PigProxy = factoryClassProxy(
class Animal extends AnimalProxy {
name = '豬'
}
)
const PetsPigProxy = factoryClassProxy(
class Pig extends PigProxy {
name = '寵物豬'
}
)
// construct捕捉器函數(shù)被觸發(fā)三次凡资,
// 第一次是PetsPigProxy觸發(fā) NewTarget為PetsPigProxy
// 第二次是PigProxy觸發(fā) NewTarget為PetsPigProxy
// 第三次是AnimalProxy觸發(fā) NewTarget為PetsPigProxy
const pig = new PetsPigProxy()
??通過上面的例子我們可以比較清晰的知道最初被調用的構造函數(shù)的意思了,就是當外部使用new Type()
時谬运,無論是父類還是當前類 construct捕捉器函數(shù)
的newTarget
參數(shù)都是指向這個Type
隙赁。大家注意到上方的construct捕捉器函數(shù)
內(nèi)部實現(xiàn)中添加了設置原型,這里涉及到new
關鍵字,我們先講講new
和super
的內(nèi)部工作原理
<b>當用戶使用new
關鍵字時</b>
- 創(chuàng)建一個原型指向當前
class
原型的對象 - 將當前
class
構建函數(shù)的this
指向上一步創(chuàng)建的對象,并執(zhí)行 - 當遇到
super()
函數(shù)調用,將當前this
指向父類構造函數(shù)并執(zhí)行 - 如果父類也存在
super()
函數(shù)調用,則再次執(zhí)行上一步 -
super()
執(zhí)行完成,如果沒有返回對象則默認返回this
- 將
super()
執(zhí)行的結果設置為當前構造函數(shù)的this
- 當前
class
構造函數(shù)執(zhí)行完成,如果沒有返回對象則默認返回this
??所以當我們不指定原型的情況下,上方的代碼就會丟失所有子類的原型
,原型
始終指向最頂級父類,因為super
時也會調用construct捕捉器函數(shù)
,這時new
創(chuàng)建一個原型指向當前class
原型的對象,并在返回時將子類的this
改變?yōu)閯倓倓?chuàng)建的對象,所以子類的this
原型就只有父類的了。上面所使用的方法可以正常一切操作,但是這個實例終究是父級直接構造出來的梆暖,所以在構造方法中new.target
是指向父類構造方法的伞访,如果使用console.log
打印出來會發(fā)現(xiàn)這個實例是Animal
對象, 可能有些同學會想著這樣優(yōu)化,比如:
const factoryClassProxy = (() => {
const instanceStack = []
const getInstance = () => instanceStack[instanceStack.length - 1]
const removeInstance = () => instanceStack.pop()
const setInstance = instance => {
instanceStack.push(instance)
return instance
}
return type =>
new Proxy(type, {
construct(target, args, newTarget) {
const isCurrent = target.prototype === newTarget.prototype
const currentInsetance = isCurrent
? setInstance(Object.create(target.prototype))
: getInstance()
if (currentInsetance) {
target.apply(currentInsetance, args)
removeInstance()
return currentInsetance
} else {
return new target(...args)
}
}
})
})();
??但是很遺憾class
的構造函數(shù)加了限制,在class
構造期間會通過new.target檢查當前是否是通過new
關鍵字調用,class
僅允許new
關鍵字調用, 直接通過函數(shù)式調用會報錯,所以這種方法也無效,目前我沒找到其他方法,如果各位大神有方法麻煩評論區(qū)貼一下謝謝了轰驳。有個最新的對象可以解決這個問題就是Reflect
這一塊我們后面再整體講一講厚掷。
代理具有私有屬性的對象
??類屬性在默認情況下是公共的弟灼,可以被外部類檢測或修改。在ES2020 實驗草案 中蝗肪,增加了定義私有類字段的能力,寫法是使用一個#
作為前綴蠕趁。我們將上面的示例改造成類寫法,先改造Animal
對象如下:
class Animal {
#name = '動物'
getName() {
isLog || console.log(this)
return this.#name
}
}
const animal = new Animal()
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
return target[key]
},
set(target, key, value, recevier) {
target[key] = value
}
})
// TypeError: Cannot read private member #name from an object whose class did not declare it
console.log(animalProxy.getName())
??上面代碼直接運行報錯了,為什么呢,我們通過recevier與被代理方法上的this得知在運行animalProxy.getName()
時getName
方法的this
是指向animalProxy
的,而私有成員
是不允許外部訪問的,訪問時會直接報錯,我們需要將this
改成正確的指向,如下:
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
const value = target[key]
return typeof value === 'function'
? value.bind(target)
: value
},
...
})
// 動物
console.log(animalProxy.getName())
代理具有內(nèi)部插槽的內(nèi)建對象
??有許多的內(nèi)建對象
比如Map
薛闪,Set
,Date
俺陋,Promise
都使用了內(nèi)部插槽
豁延,內(nèi)部插槽
類似于上面的對象的私有屬性,不允許外部訪問腊状,所以當代理沒做處理時诱咏,直接代理他們會發(fā)生錯誤例如:
const factoryInstanceProxy = instance =>
new Proxy(instance, {
get(target, prop) {
return target[prop]
},
set(target, prop, val) {
target[prop] = val
return true
}
})
// TypeError: Method Map.prototype.set called on incompatible receiver #<Map>
const map = factoryInstanceProxy(new Map())
map.set(0, 1)
// TypeError: this is not a Date object.
const date = factoryInstanceProxy(new Date())
date.getTime()
// Method Promise.prototype.then called on incompatible receiver #<Promise>
const resolvePromise = factoryInstanceProxy(Promise.resolve())
resolvePromise.then()
// TypeError: Method Set.prototype.add called on incompatible receiver #<Set>
const set = factoryInstanceProxy(new Set())
set.add(1)
在上方訪問時this
都是指向Proxy
的,而內(nèi)部插槽只允許內(nèi)部訪問缴挖,Proxy
中沒有這個內(nèi)部插槽屬性袋狞,所以只能失敗,要處理這個問題可以像代理具有私有屬性的對象中一樣的方式處理映屋,將function
的this
綁定,這樣訪問時就能正確的找到內(nèi)部插槽了苟鸯。
const factoryInstanceProxy = instance =>
new Proxy(instance, {
get(target, prop) {
const value = target[key]
return typeof value === 'function'
? value.bind(target)
: value
}
...
})
ownKeys捕捉器函數(shù)
??可能有些同學會想,為什么要把ownKeys捕捉器
單獨拎出來說呢棚点,這不是一看就會的嗎早处?別著急,大家往下看瘫析,里面還是有一個需要注意的知識點的砌梆。我們看這樣一個例子:
const user = {
name: 'bill',
age: 29,
sex: '男',
// _前綴識別為私有屬性,不能訪問贬循,修改
_code: '44xxxxxxxxxxxx17'
}
const isPrivateProp = prop => prop.startsWith('_')
const userProxy = new Proxy(user, {
get(target, prop) {
return !isPrivateProp(prop)
? target[prop]
: null
},
set(target, prop, val) {
if (!isPrivateProp(prop)) {
target[prop] = val
return true
} else {
return false
}
},
ownKeys(target) {
return Object.keys(target)
.filter(prop => !prop.startsWith('_'))
}
})
console.log(Object.keys(userProxy))
??不錯一切都預期運行咸包,這時候產(chǎn)品過來加了個需求,根據(jù)身份證的前兩位自動識別當前用戶所在的省份杖虾,腦袋瓜子一轉诉儒,直接在代理處識別添加不就好了,我們來改一下代碼
// 附加屬性列表
const provinceProp = 'province'
// 附加屬性列表
const attach = [ provinceProp ]
// 通過code獲取省份方法
const getProvinceByCode = (() => {
const codeMapProvince = {
'44': '廣東省'
...
}
return code => codeMapProvince[code.substr(0, 2)]
})()
const userProxy = new Proxy(user, {
get(target, prop) {
let value = null
switch(prop) {
case provinceProp:
value = getProvinceByCode(target._code)
break;
default:
value = isPrivateProp(prop) ? null : target[prop]
}
return value
},
set(target, prop, val) {
if (isPrivateProp(prop) || attach.includes(prop)) {
return false
} else {
target[prop] = val
return true
}
},
ownKeys(target) {
return Object.keys(target)
.filter(prop => !prop.startsWith('_'))
.concat(attach)
}
})
console.log(userProxy.province) // 廣東省
console.log(Object.keys(userProxy)) // ["name", "age", "sex"]
??可以看到對代理的附加屬性直接訪問是正常的亏掀,但是使用Object.keys
獲取屬性列表的時候只能列出user
對象原有的屬性忱反,問題出在哪里了呢?
??這是因為Object.keys
會對每個屬性調用內(nèi)部方法[[GetOwnProperty]]
獲取它的屬性描述符
滤愕,返回自身帶有enumerable(可枚舉)
的非Symbol
的key
温算。enumerable
是從對象的屬性的描述符
中獲取的,在上面的例子中province
沒有屬性的描述符
也就沒有enumerable
屬性了间影,所以province
會被忽略
??要解決這個問題就需要為province
添加屬性描述符注竿,而通過我們上面內(nèi)部方法與捕捉器函數(shù)表知道[[GetOwnProperty]]
獲取時會通過getOwnPropertyDescriptor
捕捉器函數(shù)獲取,我們加個這個捕捉器函數(shù)就可以解決了。
const userProxy = new Proxy(user, {
...
getOwnPropertyDescriptor(target, prop) {
return attach.includes(prop)
? { configurable: true, enumerable: true }
: Object.getOwnPropertyDescriptor(target, prop)
}
})
// ["name", "age", "sex", "province"]
console.log(Object.keys(userProxy))
??注意configurable
必須為true
,因為如果是不可配置的隘冲,Proxy
會阻止你為該屬性的描述符代理镣丑。
Reflect
??在上文newTarget參數(shù)中我們使用了不完美的construct捕捉器
處理函數(shù),在創(chuàng)建子類時會多次new
父類對象愈犹,而且最終傳出的也是頂級父類的對象,在console.log
時可以看出闻丑。其實Proxy
有一個最佳搭檔漩怎,可以完美處理,那就是Reflect嗦嗡。
??Reflect
是一個內(nèi)置的對象
勋锤,它提供攔截 JavaScript 操作的方法。這些方法與Proxy捕捉器
的方法相同侥祭。所有Proxy捕捉器
都有對應的Reflect
方法叁执,而且Reflect
不是一個函數(shù)對象,因此它是不可構造的矮冬,我們可以像使用Math
使用他們比如Reflect.get(...)
徒恋,除了與Proxy捕捉器
一一對應外,Reflect
方法與Object
方法也有大部分重合欢伏,大家可以通過這里入挣,比較 Reflect 和 Object 方法。
下表描述了Reflect
與捕捉器函數(shù)
的對應關系硝拧,而對應的Reflect
參數(shù)與捕捉器函數(shù)
大部分径筏,參考內(nèi)部方法與捕捉器函數(shù)
捕捉器函數(shù) | Reflect對應方法 | 方法參數(shù) | 方法返回值 |
---|---|---|---|
get |
Reflect.get() |
target , property , recevier
|
屬性的值 |
set |
Reflect.set() |
target , property , value , recevier
|
Boolean 值表明是否成功設置屬性障陶。 |
has |
Reflect.has() |
target , property
|
Boolean 類型的對象指示是否存在此屬性滋恬。 |
deleteProperty |
Reflect.deleteProperty() |
target , property
|
Boolean 值表明該屬性是否被成功刪除 |
apply |
Reflect.apply() |
target , thisArg , argumentsList
|
調用完帶著指定參數(shù)和 this 值的給定的函數(shù)后返回的結果。 |
construct |
Reflect.construct() |
target , argumentsList , newTarget
|
target (如果newTarget 存在抱究,則為newTarget )為原型恢氯,調用target 函數(shù)為構造函數(shù),argumentList 為其初始化參數(shù)的對象實例。 |
getPrototypeOf |
Reflect.getPrototypeOf() | target |
給定對象的原型鼓寺。如果給定對象沒有繼承的屬性勋拟,則返回 null 。 |
setPrototypeOf |
Reflect.setPrototypeOf() |
target , prototype
|
Boolean 值表明是否原型已經(jīng)成功設置妈候。 |
isExtensible |
Reflect.isExtensible() | target |
Boolean 值表明該對象是否可擴展 |
preventExtensions |
Reflect.preventExtensions() | target |
Boolean 值表明目標對象是否成功被設置為不可擴展 |
getOwnPropertyDescriptor |
Reflect.getOwnPropertyDescriptor() |
target , property
|
如果屬性存在于給定的目標對象中敢靡,則返回屬性描述符;否則苦银,返回 undefined 啸胧。 |
ownKeys |
Reflect.ownKeys() | target |
由目標對象的自身屬性鍵組成的 Array 赶站。 |
Reflect的recevier參數(shù)
??當使用Reflect.get
或者Reflect.set
方法時會有可選參數(shù)recevier
傳入,這個參數(shù)時使用getter
或者setter
時可以改變this
指向使用的纺念,如果不使用Reflect
時我們是沒辦法改變getter
或者setter
的this
指向的因為他們不是一個方法贝椿,參考下方示例:
const user = {
_name: '進餐小能手',
get name() {
return this._name
},
set name(newName) {
this._name = newName
return true
}
}
const target = {
_name: 'bill'
}
const name = Reflect.get(user, 'name', target)
// bill
console.log(name)
Reflect.set(user, 'name', 'lzb', target)
// { _name: 'lzb' }
console.log(target)
// { _name: '進餐小能手' }
console.log(user)
Reflect的newTarget參數(shù)
??當使用Reflect.construct
時會有一個可選參數(shù)newTarget
參數(shù)可以傳入,Reflect.construct
是一個能夠new Class
的方法實現(xiàn)陷谱,比如new User('bill')
與Reflect.construct(User, ['bill'])
是一致的烙博,而newTarget
可以改變創(chuàng)建出來的對象的原型,在es5
中能夠用Object.create
實現(xiàn)叭首,但是有略微的區(qū)別习勤,在構造方法中new.target
可以查看到當前構造方法踪栋,如果使用es5
實現(xiàn)的話這個對象是undefined
因為不是通過new
創(chuàng)建的焙格,使用Reflect.construct
則沒有這個問題 參考下方兩種實現(xiàn)方式
function OneClass() {
console.log(new.target)
this.name = 'one';
}
function OtherClass() {
console.log(new.target)
this.name = 'other';
}
// 創(chuàng)建一個對象:
var obj1 = Reflect.construct(OneClass, args, OtherClass);
// 打印 function OtherClass
// 與上述方法等效:
var obj2 = Object.create(OtherClass.prototype);
OneClass.apply(obj2, args);
// 打印 undefined
console.log(obj1.name); // 'one'
console.log(obj2.name); // 'one'
console.log(obj1 instanceof OneClass); // false
console.log(obj2 instanceof OneClass); // false
console.log(obj1 instanceof OtherClass); // true
console.log(obj2 instanceof OtherClass); // true
construct捕捉器
??在newTarget參數(shù)中我們實現(xiàn)了不完美的construct捕捉器
,而通過閱讀Reflect夷都,我們知道了一個能夠完美契合我們想要的能夠實現(xiàn)的方案眷唉,那就是Reflect.construct
不僅能夠識別new.target
,也能夠處理多是創(chuàng)建對象問題囤官,我們改造一下實現(xiàn)冬阳,示例如下
const factoryClassProxy = type =>
new Proxy(type, {
construct(target, args, newTarget) {
return Reflect.construct(...arguments)
}
})
const AnimalProxy = factoryClassProxy(
class {
name = '動物'
getName() {
return this.name
}
}
)
const PigProxy = factoryClassProxy(
class Animal extends AnimalProxy {
name = '豬'
}
)
const PetsPigProxy = factoryClassProxy(
class Pig extends PigProxy {
name = '寵物豬'
}
)
代理setter、getter函數(shù)
??我們通過閱讀recevier與被代理方法上的this知道了recevier
的指向,接下來請思考這樣一段代碼
const animal = {
_name: '動物',
getName() {
return this._name
},
get name() {
return this._name
}
}
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
return target[key]
}
})
const pig = {
__proto__: animalProxy,
_name: '豬'
}
console.log(pig.name)
console.log(animalProxy.name)
console.log(pig.getName())
console.log(animalProxy.getName())
??如果你運行上方代碼會發(fā)現(xiàn)打印順序依次是動物党饮,動物肝陪,豬,動物
刑顺,使用getName
通過方法訪問時是沒問題的氯窍,因為代理拿到了getName
的實現(xiàn),然后通過當前對象訪問蹲堂,所以this
是當前誰調用就是誰,但是通過getter
調用時狼讨,在通過target[key]
時就已經(jīng)調用了方法實現(xiàn),所以this
始終是指向當前代理的對象target
柒竞,想要修正這里就得通過代理內(nèi)的捕捉器
入手政供,修正this
的對象,而recevier
就是指向當前調用者的朽基,但是getter
不像成員方法可以直接通過bind布隔、call、apply
能夠修正this
稼虎,這時候我們就要借助Reflect.get
方法了执泰。setter
的原理也是一樣的這里就不作多講了,參考下方
const animal = {
_name: '動物',
getName() {
return this._name
},
get name() {
return this._name
}
}
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
return Reflect.get(...arguments)
}
})
const pig = {
__proto__: animalProxy,
_name: '豬'
}
console.log(pig.name)
console.log(animalProxy.name)
console.log(pig.getName())
console.log(animalProxy.getName())
Proxy與Reflect的結合
??因為Reflect
與Proxy
的捕捉器
都有對應的方法渡蜻,所以大部分情況下我們都能直接使用Reflect
的API
來對Proxy
的操作相結合术吝。我們能專注Proxy
要執(zhí)行的業(yè)務比如下方代碼
new Proxy(animal, {
get(target, key, recevier) {
// 具體業(yè)務
...
return Reflect.get(...arguments)
},
set(target, property, value, recevier) {
// 具體業(yè)務
...
return Reflect.set(...arguments)
},
has(target, property) {
// 具體業(yè)務
...
return Reflect.has(...arguments)
}
...
})
Proxy.revocable撤銷代理
??假如有這么一個業(yè)務计济,我們在做一個商城系統(tǒng),產(chǎn)品要求跟蹤用戶的商品內(nèi)操作的具體蹤跡排苍,比如展開了商品詳情沦寂,點擊播放了商品的視頻等等,為了與具體業(yè)務脫耦淘衙,使用Proxy
是一個不錯的選擇于是我們寫了下面這段代碼
// track-commodity.js
// 具體的跟蹤代碼
const track = {
// 播放了視頻
introduceVideo() {
...
},
// 獲取了商品詳情
details() {
...
}
}
export const processingCommodity = (commodity) =>
new Proxy(commodity, {
get(target, key) {
if (track[key]) {
track[key]()
}
return Reflect.get(...arguments)
}
})
// main.js
// 具體業(yè)務中使用
commodity = processingCommodity(commodity)
??我們編寫了上方传藏,不錯很完美,但是后期一堆客戶反應不希望自己的行蹤被跟蹤彤守,產(chǎn)品又要求我們改方案毯侦,用戶可以在設置中要求不跟蹤,不能直接重啟刷新頁面具垫,也不能讓緩存中的商品對象重新加載這時候侈离,如果讓新的商品不被代理很簡單只要加個判斷就行了,但是舊數(shù)據(jù)也不能重新加載筝蚕,那就只能撤銷代理了卦碾,接下來我們介紹一下新的API
??Proxy.revocable(target, handler)方法可以用來創(chuàng)建一個可撤銷的代理對象。該方法的參數(shù)與new Proxy(target, handler)
一樣起宽,第一個參數(shù)傳入要代理的對象洲胖,第二個參數(shù)傳入捕捉器
。該方法返回一個對象坯沪,這個對象的proxy
返回target
的代理對象绿映,revoke
返回撤銷代理的方法,具體使用如下
const { proxy, revoke } = Proxy.revocable(target, handler)
??接下來我們改進一下我們的跟蹤代碼腐晾,如下
// track-commodity.js
...
// 為什么使用 WeakMap 而不是 Map叉弦,因為它不會阻止垃圾回收。
// 如果商品代理除了WeakMap之外沒有地方引用赴魁,則會從內(nèi)存中清除
const revokes = new WeakMap()
export const processingCommodity = (commodity) => {
const { proxy, revoke } = Proxy.revocable(commodity, {
get(target, key) {
if (track[key]) {
track[key]()
}
return Reflect.get(...arguments)
}
})
revokes.set(proxy, revoke)
return proxy
}
export const unProcessingCommodity = (commodity) => {
const revoke = revokes.get(commodity)
if (revoke) {
revoke()
} else {
return commodity
}
}
// main.js
// 查看是否設置了可跟蹤
const changeCommodity = () =>
commodity = setting.isTrack
? processingCommodity(commodity)
: unProcessingCommodity(commodity)
// 初始化
changeCommodity()
// 監(jiān)聽設置改變
bus.on('changeTrackSetting', changeCommodity)
??還有一個問題卸奉,我們看到當revoke()
撤銷代理后我們并沒有返回代理前的commodity
對象,這該怎么辦呢颖御,怎么從代理處拿取代理前的對象呢榄棵,我認為比較好的有兩種方案,我們往下看潘拱。
通過代理獲取被代理對象
??通過代理處拿取代理前的對疹鳄,我認為有兩種比較好的方案我分別介紹一下。
??1:Proxy.revocable撤銷代理中實例看到芦岂,我們既然添加了proxy
與revoke
的WeakMap
對象瘪弓,為什么不多添加一份proxy
與target
的對象呢,說說干就干
...
const commoditys = new WeakMap()
const revokes = new WeakMap()
const processingCommodity = (commodity) => {
const { proxy, revoke } = Proxy.revocable(commodity, {
get(target, key) {
if (track[key]) {
track[key]()
}
return Reflect.get(...arguments)
}
})
commoditys.set(proxy, commodity)
revokes.set(proxy, revoke)
return proxy
}
const unProcessingCommodity = (commodity) => {
const revoke = revokes.get(commodity)
if (revoke) {
revoke()
return commoditys.get(commodity)
} else {
return commodity
}
}
??2:與第一種方案不同禽最,第二種方案是直接在代理的get捕捉器
中加入邏輯處理腺怯,既然我們能夠攔截get
袱饭,那我們就能夠在里面添加一些我們track-commodity.js
的內(nèi)置邏輯,就是當get
某個key
時我們就返回代理的原始對象呛占,當然這個key
不能和業(yè)務中使用到的commodity
的key
沖突虑乖,而且要確保只有內(nèi)部使用,所以我們需要使用到Symbol晾虑,只要不導出用戶就拿不到這個key
就都解決了疹味,參考下方代碼
...
const toRaw = Symbol('getCommodity')
const revokes = new WeakMap()
const processingCommodity = (commodity) => {
const { proxy, revoke } = Proxy.revocable(commodity, {
get(target, key) {
if (key === toRaw) {
return target
}
if (track[key]) {
track[key]()
}
return Reflect.get(...arguments)
}
})
revokes.set(proxy, revoke)
return proxy
}
const unProcessingCommodity = (commodity) => {
const revoke = revokes.get(commodity)
if (revoke) {
// 注意要在撤銷代理前使用
const commodity = commodity[toRaw]
revoke()
return commodity
} else {
return commodity
}
}
Proxy的局限性
??代理提供了一種獨特的方法,可以在調整現(xiàn)有對象的行為帜篇,但是它并不完美糙捺,有一定的局限性。
代理私有屬性
??我們在代理具有私有屬性的對象時介紹了如何避開this
是當前代理無法訪問私有屬性的問題笙隙,但是這里也有一定的問題洪灯,因為一個對象里肯定不止只有訪問私有屬性的方法,如果有訪問自身非私有屬性時逃沿,這里的處理方式有一定的問題婴渡,比如下方代碼
class Animal {
#name = '動物'
feature = '它們一般以有機物為食幻锁,能感覺凯亮,可運動,能夠自主運動哄尔〖傧活動或能夠活動之物'
getName() {
return this.#name
}
getFeature() {
return this.feature
}
}
const animal = new Animal()
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
const value = Reflect.get(...arguments)
return typeof value === 'function'
? value.bind(target)
: value
}
})
const pig = {
__proto__: animalProxy,
feature: '豬是一種脊椎動物、哺乳動物岭接、家畜富拗,也是古雜食類哺乳動物,主要分為家豬和野豬'
}
// 動物
console.log(pig.getName())
// 它們一般以有機物為食鸣戴,能感覺啃沪,可運動,能夠自主運動窄锅〈辞В活動或能夠活動之物
console.log(pig.getFeature())
??因為只要是function
都會執(zhí)行bind
綁定當前被代理的對象animal
,所以當pig
通過原型繼承了animalProxy
之后this
訪問的都是animal
入偷,還有追驴,這意味著我們要熟悉被代理對象內(nèi)的api
,通過識別是否是私有屬性訪問才綁定this
疏之,需要了解被代理對象的api
殿雪。還有一個問題是私有屬性只允許自身訪問,在沒有代理的幫助下上方的pig.getName()
會出錯TypeError
锋爪,而通過bind
之后就可以正常訪問丙曙,這一塊要看具體業(yè)務爸业,不過還是建議跟沒代理時保持一致,這里處理比較簡單亏镰,在知道使用私有屬性api
之后沃呢,只要識別當前訪問對象是否是原對象的代理即可。具體處理代碼下方所示
const targets = new WeakMap()
const privateMethods = ['getName']
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
const isPrivate = privateMethods.includes(key)
if (isPrivate && targets.get(recevier) !== target) {
throw `${key}方法僅允許自身調用`
}
const value = Reflect.get(...arguments)
if (isPrivate && typeof value === 'function') {
return value.bind(target)
} else {
return value
}
}
})
targets.set(animalProxy, animal)
const pig = {
__proto__: animalProxy,
feature: '豬是一種脊椎動物拆挥、哺乳動物薄霜、家畜,也是古雜食類哺乳動物纸兔,主要分為家豬和野豬'
}
// 動物
console.log(animalProxy.getName())
// TypeError
// console.log(pig.getName())
// 豬是一種脊椎動物惰瓜、哺乳動物、家畜汉矿,也是古雜食類哺乳動物崎坊,主要分為家豬和野豬
console.log(pig.getFeature())
target !== Proxy
??代理跟原對象肯定是不同的對象,所以當我們使用原對象進行管理后代理卻無法進行正確管理洲拇,比如下方代理做了一個所有用戶實例的集中管理:
const users = new Set()
class User {
constructor() {
users.add(this)
}
}
const user = new User()
// true
console.log(users.has(user))
const userProxy = new Proxy(user, {})
// false
users.has(userProxy)
??所以在開發(fā)中這類問題需要特別注意奈揍,在開發(fā)時假如對一個對象做代理時,對代理的所有管理也需要再進行一層代理赋续,原對象對原對象男翰,代理對代理,比如上方這個實例可以通過下方代碼改進
const users = new Set()
class User {
constructor() {
users.add(this)
}
}
// 獲取原對象
const getRaw = (target) => target[toRaw] ? target[toRaw] : target
const toRaw = Symbol('toRaw')
const usersProxy = new Proxy(users, {
get(target, prop) {
// 注意Set size是屬性纽乱,而不是方法蛾绎,這個屬性用到了內(nèi)部插槽,
// 所以不能夠使用Reflect.get(...arguments)獲取
let value = prop === 'size'
? target[prop]
: Reflect.get(...arguments)
value = typeof value === 'function'
? value.bind(target)
: value
// 這里只做兩個api示例鸦列,當添加或者判斷一定是通過原對象判斷添加租冠,
// 因為原對象的管理只能放原對象
if (prop === 'has' || prop === 'add') {
return (target, ...args) =>
value(getRaw(target), ...args)
} else {
return value
}
}
})
const factoryUserProxy = (user) => {
const userProxy = new Proxy(user, {
get(target, prop, recevier) {
if (prop === toRaw) {
return target
} else {
return Reflect.get(...arguments)
}
}
})
return userProxy
}
const user = new User()
const userProxy = factoryUserProxy(user)
// true
console.log(users.has(user))
// true
console.log(usersProxy.has(user))
// true
console.log(usersProxy.has(userProxy))
// true
console.log(users.size)
// true
console.log(usersProxy.size)
// 因為會轉化為原對象添加,而原對象已有 所以添加不進去
usersProxy.add(userProxy)
// 1
console.log(users.size)
// 1
console.log(usersProxy.size)
??Proxy
就介紹到這里了薯嗤,本文介紹了Proxy
大部分要注意的問題以及用法顽爹。