Proxy對(duì)象用于創(chuàng)建一個(gè)對(duì)象的代理,從而實(shí)現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。Proxy被用于許多庫(kù)和瀏覽器框架上,例如vue3就是使用Proxy來實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式的。本文帶你了解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 需要包裝的對(duì)象,可以是任何變量
- handle 代理配置,通常是用函數(shù)作為屬性值的對(duì)象,為了方便表達(dá)本文以
捕捉器函數(shù)
來稱呼這些屬性
??對(duì)proxy
進(jìn)行操作時(shí),如果handler
對(duì)象中存在相應(yīng)的捕捉器函數(shù)
則運(yùn)行這個(gè)函數(shù),如果不存在則直接對(duì)target
進(jìn)行處理。
??在JavaScript中對(duì)于對(duì)象的大部分操作都存在內(nèi)部方法
,它是最底層的工作方式。例如對(duì)數(shù)據(jù)讀取時(shí)底層會(huì)調(diào)用[[Get]]
,寫入的時(shí)底層會(huì)調(diào)用[[Set]]
。我們不能直接通過方法名調(diào)用它,而Proxy
的代理配置
中的捕捉器函數(shù)
則可以攔截這些內(nèi)部方法的調(diào)用。
內(nèi)部方法與捕捉器函數(shù)
下表描述了內(nèi)部方法
與捕捉器函數(shù)
的對(duì)應(yīng)關(guān)系:
內(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ù)調(diào)用 |
[[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 |
一個(gè)可枚舉object . |
Object.getOwnPropertyNames Object.getOwnPropertySymbols for...in Object.keys/values/entries |
捕捉器函數(shù)參數(shù)說明
-
target
是目標(biāo)對(duì)象,被作為第一個(gè)參數(shù)傳遞給new Proxy
-
property
將被設(shè)置或獲取的屬性名或Symbol
-
value
要設(shè)置的新的屬性值 -
recevier
最初被調(diào)用的對(duì)象。通常是proxy
本身,但是可能以其他方式被間接地調(diào)用(因此不一定是proxy
本身,后面我會(huì)說明) -
thisArg
被調(diào)用時(shí)的上下文對(duì)象 -
argumentsList
被調(diào)用時(shí)的參數(shù)數(shù)組 -
newTarget
最初被調(diào)用的構(gòu)造函數(shù) -
descriptor
待定義或修改的屬性的描述符
這里我們重點(diǎn)講一下捕捉器函數(shù)參數(shù)的recevier
和newTarget
其他參數(shù)就不一一介紹,基本上一看就懂了。
改造console.log
??在Proxy
的捕捉器函數(shù)
中使用console.log
很容易造成死循環(huán),因?yàn)槿绻?code>console.log(poxy)時(shí)會(huì)讀取Proxy
的屬性,可能會(huì)經(jīng)過捕捉器函數(shù)
,經(jīng)過捕捉器函數(shù)
再次console.log(poxy)
。為了方便調(diào)試,我這里改造了以下console.log
。
// 通過當(dāng)前是否是log模式來判斷是否是打印
let isLog = false
{
const logs = []
const platformLog = console.log
const resolvedPromise = Promise.resolve()
// 當(dāng)前是否正在執(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
最初被調(diào)用的對(duì)象,什么意思呢,就是誰調(diào)用的Proxy
經(jīng)過捕捉器函數(shù)
那么它就是誰。看下方實(shí)例說明
const animal = {
_name: '動(dòng)物',
getName() {
isLog || console.log(this)
return this._name
}
}
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
isLog || console.log(recevier)
return target[key]
}
})
// 最初被調(diào)用的對(duì)象是animalProxy,
// 這里訪問時(shí)get捕捉器函數(shù)的recevier參數(shù)就是animalProxy
// 被代理的this就是animalProxy
animalProxy.getName()
const pig = {
// 通過原型,繼承animalProxy
__proto__: animalProxy,
test: animalProxy
}
// pig中不存在name,通過原型查找,原型是Proxy,讀取時(shí)經(jīng)過get捕捉器函數(shù)
// 最初被調(diào)用的對(duì)象時(shí)pig
// 這里訪問時(shí)get捕捉器函數(shù)的recevier參數(shù)就是pig
// 被代理的this就是pig
pig.getName()
// 最初被調(diào)用的對(duì)象是pig.test即為animalProxy
// 這里訪問時(shí)get捕捉器函數(shù)的recevier參數(shù)就是animalProxy
// 被代理的this就是animalProxy
pig.test.getName()
??上方示例清晰的說明了recevier
,就是當(dāng)調(diào)用proxy
對(duì)象時(shí)調(diào)用者是誰,其實(shí)與function
中this
的機(jī)制是一致的。
newTarget參數(shù)
??newTarget
最初被調(diào)用的構(gòu)造函數(shù),在es6
中添加了class對(duì)象的支持,而newTarget
也就是主要識(shí)別類中繼承關(guān)系的對(duì)象,比如看下方例子
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 = '動(dòng)物'
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()
??通過上面的例子我們可以比較清晰的知道最初被調(diào)用的構(gòu)造函數(shù)的意思了,就是當(dāng)外部使用new Type()
時(shí),無論是父類還是當(dāng)前類 construct捕捉器函數(shù)
的newTarget
參數(shù)都是指向這個(gè)Type
。大家注意到上方的construct捕捉器函數(shù)
內(nèi)部實(shí)現(xiàn)中添加了設(shè)置原型,這里涉及到new
關(guān)鍵字,我們先講講new
和super
的內(nèi)部工作原理
<b>當(dāng)用戶使用new
關(guān)鍵字時(shí)</b>
- 創(chuàng)建一個(gè)原型指向當(dāng)前
class
原型的對(duì)象 - 將當(dāng)前
class
構(gòu)建函數(shù)的this
指向上一步創(chuàng)建的對(duì)象,并執(zhí)行 - 當(dāng)遇到
super()
函數(shù)調(diào)用,將當(dāng)前this
指向父類構(gòu)造函數(shù)并執(zhí)行 - 如果父類也存在
super()
函數(shù)調(diào)用,則再次執(zhí)行上一步 -
super()
執(zhí)行完成,如果沒有返回對(duì)象則默認(rèn)返回this
- 將
super()
執(zhí)行的結(jié)果設(shè)置為當(dāng)前構(gòu)造函數(shù)的this
- 當(dāng)前
class
構(gòu)造函數(shù)執(zhí)行完成,如果沒有返回對(duì)象則默認(rèn)返回this
??所以當(dāng)我們不指定原型的情況下,上方的代碼就會(huì)丟失所有子類的原型
,原型
始終指向最頂級(jí)父類,因?yàn)?code>super時(shí)也會(huì)調(diào)用construct捕捉器函數(shù)
,這時(shí)new
創(chuàng)建一個(gè)原型指向當(dāng)前class
原型的對(duì)象,并在返回時(shí)將子類的this
改變?yōu)閯倓倓?chuàng)建的對(duì)象,所以子類的this
原型就只有父類的了。上面所使用的方法可以正常一切操作,但是這個(gè)實(shí)例終究是父級(jí)直接構(gòu)造出來的,所以在構(gòu)造方法中new.target
是指向父類構(gòu)造方法的,如果使用console.log
打印出來會(huì)發(fā)現(xiàn)這個(gè)實(shí)例是Animal
對(duì)象, 可能有些同學(xué)會(huì)想著這樣優(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
的構(gòu)造函數(shù)加了限制,在class
構(gòu)造期間會(huì)通過new.target檢查當(dāng)前是否是通過new
關(guān)鍵字調(diào)用,class
僅允許new
關(guān)鍵字調(diào)用, 直接通過函數(shù)式調(diào)用會(huì)報(bào)錯(cuò),所以這種方法也無效,目前我沒找到其他方法,如果各位大神有方法麻煩評(píng)論區(qū)貼一下謝謝了。有個(gè)最新的對(duì)象可以解決這個(gè)問題就是Reflect
這一塊我們后面再整體講一講。
代理具有私有屬性的對(duì)象
??類屬性在默認(rèn)情況下是公共的,可以被外部類檢測(cè)或修改。在ES2020 實(shí)驗(yàn)草案 中,增加了定義私有類字段的能力,寫法是使用一個(gè)#
作為前綴。我們將上面的示例改造成類寫法,先改造Animal
對(duì)象如下:
class Animal {
#name = '動(dòng)物'
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())
??上面代碼直接運(yùn)行報(bào)錯(cuò)了,為什么呢,我們通過recevier與被代理方法上的this得知在運(yùn)行animalProxy.getName()
時(shí)getName
方法的this
是指向animalProxy
的,而私有成員
是不允許外部訪問的,訪問時(shí)會(huì)直接報(bào)錯(cuò),我們需要將this
改成正確的指向,如下:
const animalProxy = new Proxy(animal, {
get(target, key, recevier) {
const value = target[key]
return typeof value === 'function'
? value.bind(target)
: value
},
...
})
// 動(dòng)物
console.log(animalProxy.getName())
代理具有內(nèi)部插槽的內(nèi)建對(duì)象
??有許多的內(nèi)建對(duì)象
比如Map
,Set
,Date
,Promise
都使用了內(nèi)部插槽
,內(nèi)部插槽
類似于上面的對(duì)象的私有屬性,不允許外部訪問,所以當(dāng)代理沒做處理時(shí),直接代理他們會(huì)發(fā)生錯(cuò)誤例如:
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)
在上方訪問時(shí)this
都是指向Proxy
的,而內(nèi)部插槽只允許內(nèi)部訪問,Proxy
中沒有這個(gè)內(nèi)部插槽屬性,所以只能失敗,要處理這個(gè)問題可以像代理具有私有屬性的對(duì)象中一樣的方式處理,將function
的this
綁定,這樣訪問時(shí)就能正確的找到內(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ù)
??可能有些同學(xué)會(huì)想,為什么要把ownKeys捕捉器
單獨(dú)拎出來說呢,這不是一看就會(huì)的嗎?別著急,大家往下看,里面還是有一個(gè)需要注意的知識(shí)點(diǎn)的。我們看這樣一個(gè)例子:
const user = {
name: 'bill',
age: 29,
sex: '男',
// _前綴識(shí)別為私有屬性,不能訪問,修改
_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))
??不錯(cuò)一切都預(yù)期運(yùn)行,這時(shí)候產(chǎn)品過來加了個(gè)需求,根據(jù)身份證的前兩位自動(dòng)識(shí)別當(dāng)前用戶所在的省份,腦袋瓜子一轉(zhuǎn),直接在代理處識(shí)別添加不就好了,我們來改一下代碼
// 附加屬性列表
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"]
??可以看到對(duì)代理的附加屬性直接訪問是正常的,但是使用Object.keys
獲取屬性列表的時(shí)候只能列出user
對(duì)象原有的屬性,問題出在哪里了呢?
??這是因?yàn)?code>Object.keys會(huì)對(duì)每個(gè)屬性調(diào)用內(nèi)部方法[[GetOwnProperty]]
獲取它的屬性描述符
,返回自身帶有enumerable(可枚舉)
的非Symbol
的key
。enumerable
是從對(duì)象的屬性的描述符
中獲取的,在上面的例子中province
沒有屬性的描述符
也就沒有enumerable
屬性了,所以province
會(huì)被忽略
??要解決這個(gè)問題就需要為province
添加屬性描述符,而通過我們上面內(nèi)部方法與捕捉器函數(shù)表知道[[GetOwnProperty]]
獲取時(shí)會(huì)通過getOwnPropertyDescriptor
捕捉器函數(shù)獲取,我們加個(gè)這個(gè)捕捉器函數(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
,因?yàn)槿绻遣豢膳渲玫模?code>Proxy會(huì)阻止你為該屬性的描述符代理。
Reflect
??在上文newTarget參數(shù)中我們使用了不完美的construct捕捉器
處理函數(shù),在創(chuàng)建子類時(shí)會(huì)多次new
父類對(duì)象,而且最終傳出的也是頂級(jí)父類的對(duì)象,在console.log
時(shí)可以看出。其實(shí)Proxy
有一個(gè)最佳搭檔,可以完美處理,那就是Reflect。
??Reflect
是一個(gè)內(nèi)置的對(duì)象
,它提供攔截 JavaScript 操作的方法。這些方法與Proxy捕捉器
的方法相同。所有Proxy捕捉器
都有對(duì)應(yīng)的Reflect
方法,而且Reflect
不是一個(gè)函數(shù)對(duì)象,因此它是不可構(gòu)造的,我們可以像使用Math
使用他們比如Reflect.get(...)
,除了與Proxy捕捉器
一一對(duì)應(yīng)外,Reflect
方法與Object
方法也有大部分重合,大家可以通過這里,比較 Reflect 和 Object 方法。
下表描述了Reflect
與捕捉器函數(shù)
的對(duì)應(yīng)關(guān)系,而對(duì)應(yīng)的Reflect
參數(shù)與捕捉器函數(shù)
大部分,參考內(nèi)部方法與捕捉器函數(shù)
捕捉器函數(shù) | Reflect對(duì)應(yīng)方法 | 方法參數(shù) | 方法返回值 |
---|---|---|---|
get |
Reflect.get() |
target , property , recevier
|
屬性的值 |
set |
Reflect.set() |
target , property , value , recevier
|
Boolean 值表明是否成功設(shè)置屬性。 |
has |
Reflect.has() |
target , property
|
Boolean 類型的對(duì)象指示是否存在此屬性。 |
deleteProperty |
Reflect.deleteProperty() |
target , property
|
Boolean 值表明該屬性是否被成功刪除 |
apply |
Reflect.apply() |
target , thisArg , argumentsList
|
調(diào)用完帶著指定參數(shù)和 this 值的給定的函數(shù)后返回的結(jié)果。 |
construct |
Reflect.construct() |
target , argumentsList , newTarget
|
target (如果newTarget 存在,則為newTarget )為原型,調(diào)用target 函數(shù)為構(gòu)造函數(shù),argumentList 為其初始化參數(shù)的對(duì)象實(shí)例。 |
getPrototypeOf |
Reflect.getPrototypeOf() | target |
給定對(duì)象的原型。如果給定對(duì)象沒有繼承的屬性,則返回 null 。 |
setPrototypeOf |
Reflect.setPrototypeOf() |
target , prototype
|
Boolean 值表明是否原型已經(jīng)成功設(shè)置。 |
isExtensible |
Reflect.isExtensible() | target |
Boolean 值表明該對(duì)象是否可擴(kuò)展 |
preventExtensions |
Reflect.preventExtensions() | target |
Boolean 值表明目標(biāo)對(duì)象是否成功被設(shè)置為不可擴(kuò)展 |
getOwnPropertyDescriptor |
Reflect.getOwnPropertyDescriptor() |
target , property
|
如果屬性存在于給定的目標(biāo)對(duì)象中,則返回屬性描述符;否則,返回 undefined 。 |
ownKeys |
Reflect.ownKeys() | target |
由目標(biāo)對(duì)象的自身屬性鍵組成的 Array 。 |
Reflect的recevier參數(shù)
??當(dāng)使用Reflect.get
或者Reflect.set
方法時(shí)會(huì)有可選參數(shù)recevier
傳入,這個(gè)參數(shù)時(shí)使用getter
或者setter
時(shí)可以改變this
指向使用的,如果不使用Reflect
時(shí)我們是沒辦法改變getter
或者setter
的this
指向的因?yàn)樗麄儾皇且粋€(gè)方法,參考下方示例:
const user = {
_name: '進(jìn)餐小能手',
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: '進(jìn)餐小能手' }
console.log(user)
Reflect的newTarget參數(shù)
??當(dāng)使用Reflect.construct
時(shí)會(huì)有一個(gè)可選參數(shù)newTarget
參數(shù)可以傳入,Reflect.construct
是一個(gè)能夠new Class
的方法實(shí)現(xiàn),比如new User('bill')
與Reflect.construct(User, ['bill'])
是一致的,而newTarget
可以改變創(chuàng)建出來的對(duì)象的原型,在es5
中能夠用Object.create
實(shí)現(xiàn),但是有略微的區(qū)別,在構(gòu)造方法中new.target
可以查看到當(dāng)前構(gòu)造方法,如果使用es5
實(shí)現(xiàn)的話這個(gè)對(duì)象是undefined
因?yàn)椴皇峭ㄟ^new
創(chuàng)建的,使用Reflect.construct
則沒有這個(gè)問題 參考下方兩種實(shí)現(xiàn)方式
function OneClass() {
console.log(new.target)
this.name = 'one';
}
function OtherClass() {
console.log(new.target)
this.name = 'other';
}
// 創(chuàng)建一個(gè)對(duì)象:
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ù)中我們實(shí)現(xiàn)了不完美的construct捕捉器
,而通過閱讀Reflect,我們知道了一個(gè)能夠完美契合我們想要的能夠?qū)崿F(xiàn)的方案,那就是Reflect.construct
不僅能夠識(shí)別new.target
,也能夠處理多是創(chuàng)建對(duì)象問題,我們改造一下實(shí)現(xiàn),示例如下
const factoryClassProxy = type =>
new Proxy(type, {
construct(target, args, newTarget) {
return Reflect.construct(...arguments)
}
})
const AnimalProxy = factoryClassProxy(
class {
name = '動(dòng)物'
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
的指向,接下來請(qǐng)思考這樣一段代碼
const animal = {
_name: '動(dòng)物',
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())
??如果你運(yùn)行上方代碼會(huì)發(fā)現(xiàn)打印順序依次是動(dòng)物,動(dòng)物,豬,動(dòng)物
,使用getName
通過方法訪問時(shí)是沒問題的,因?yàn)榇砟玫搅?code>getName的實(shí)現(xiàn),然后通過當(dāng)前對(duì)象訪問,所以this
是當(dāng)前誰調(diào)用就是誰,但是通過getter
調(diào)用時(shí),在通過target[key]
時(shí)就已經(jīng)調(diào)用了方法實(shí)現(xiàn),所以this
始終是指向當(dāng)前代理的對(duì)象target
,想要修正這里就得通過代理內(nèi)的捕捉器
入手,修正this
的對(duì)象,而recevier
就是指向當(dāng)前調(diào)用者的,但是getter
不像成員方法可以直接通過bind、call、apply
能夠修正this
,這時(shí)候我們就要借助Reflect.get
方法了。setter
的原理也是一樣的這里就不作多講了,參考下方
const animal = {
_name: '動(dòng)物',
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的結(jié)合
??因?yàn)?code>Reflect與Proxy
的捕捉器
都有對(duì)應(yīng)的方法,所以大部分情況下我們都能直接使用Reflect
的API
來對(duì)Proxy
的操作相結(jié)合。我們能專注Proxy
要執(zhí)行的業(yè)務(wù)比如下方代碼
new Proxy(animal, {
get(target, key, recevier) {
// 具體業(yè)務(wù)
...
return Reflect.get(...arguments)
},
set(target, property, value, recevier) {
// 具體業(yè)務(wù)
...
return Reflect.set(...arguments)
},
has(target, property) {
// 具體業(yè)務(wù)
...
return Reflect.has(...arguments)
}
...
})
Proxy.revocable撤銷代理
??假如有這么一個(gè)業(yè)務(wù),我們?cè)谧鲆粋€(gè)商城系統(tǒng),產(chǎn)品要求跟蹤用戶的商品內(nèi)操作的具體蹤跡,比如展開了商品詳情,點(diǎn)擊播放了商品的視頻等等,為了與具體業(yè)務(wù)脫耦,使用Proxy
是一個(gè)不錯(cuò)的選擇于是我們寫了下面這段代碼
// 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è)務(wù)中使用
commodity = processingCommodity(commodity)
??我們編寫了上方,不錯(cuò)很完美,但是后期一堆客戶反應(yīng)不希望自己的行蹤被跟蹤,產(chǎn)品又要求我們改方案,用戶可以在設(shè)置中要求不跟蹤,不能直接重啟刷新頁(yè)面,也不能讓緩存中的商品對(duì)象重新加載這時(shí)候,如果讓新的商品不被代理很簡(jiǎn)單只要加個(gè)判斷就行了,但是舊數(shù)據(jù)也不能重新加載,那就只能撤銷代理了,接下來我們介紹一下新的API
??Proxy.revocable(target, handler)方法可以用來創(chuàng)建一個(gè)可撤銷的代理對(duì)象。該方法的參數(shù)與new Proxy(target, handler)
一樣,第一個(gè)參數(shù)傳入要代理的對(duì)象,第二個(gè)參數(shù)傳入捕捉器
。該方法返回一個(gè)對(duì)象,這個(gè)對(duì)象的proxy
返回target
的代理對(duì)象,revoke
返回撤銷代理的方法,具體使用如下
const { proxy, revoke } = Proxy.revocable(target, handler)
??接下來我們改進(jìn)一下我們的跟蹤代碼,如下
// track-commodity.js
...
// 為什么使用 WeakMap 而不是 Map,因?yàn)樗粫?huì)阻止垃圾回收。
// 如果商品代理除了WeakMap之外沒有地方引用,則會(huì)從內(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
// 查看是否設(shè)置了可跟蹤
const changeCommodity = () =>
commodity = setting.isTrack
? processingCommodity(commodity)
: unProcessingCommodity(commodity)
// 初始化
changeCommodity()
// 監(jiān)聽設(shè)置改變
bus.on('changeTrackSetting', changeCommodity)
??還有一個(gè)問題,我們看到當(dāng)revoke()
撤銷代理后我們并沒有返回代理前的commodity
對(duì)象,這該怎么辦呢,怎么從代理處拿取代理前的對(duì)象呢,我認(rèn)為比較好的有兩種方案,我們往下看。
通過代理獲取被代理對(duì)象
??通過代理處拿取代理前的對(duì),我認(rèn)為有兩種比較好的方案我分別介紹一下。
??1:Proxy.revocable撤銷代理中實(shí)例看到,我們既然添加了proxy
與revoke
的WeakMap
對(duì)象,為什么不多添加一份proxy
與target
的對(duì)象呢,說說干就干
...
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)置邏輯,就是當(dāng)get
某個(gè)key
時(shí)我們就返回代理的原始對(duì)象,當(dāng)然這個(gè)key
不能和業(yè)務(wù)中使用到的commodity
的key
沖突,而且要確保只有內(nèi)部使用,所以我們需要使用到Symbol,只要不導(dǎo)出用戶就拿不到這個(gè)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的局限性
??代理提供了一種獨(dú)特的方法,可以在調(diào)整現(xiàn)有對(duì)象的行為,但是它并不完美,有一定的局限性。
代理私有屬性
??我們?cè)?a href="#%E4%BB%A3%E7%90%86%E5%85%B7%E6%9C%89%E7%A7%81%E6%9C%89%E5%B1%9E%E6%80%A7%E7%9A%84%E5%AF%B9%E8%B1%A1" target="_blank">代理具有私有屬性的對(duì)象時(shí)介紹了如何避開this
是當(dāng)前代理無法訪問私有屬性的問題,但是這里也有一定的問題,因?yàn)橐粋€(gè)對(duì)象里肯定不止只有訪問私有屬性的方法,如果有訪問自身非私有屬性時(shí),這里的處理方式有一定的問題,比如下方代碼
class Animal {
#name = '動(dòng)物'
feature = '它們一般以有機(jī)物為食,能感覺,可運(yùn)動(dòng),能夠自主運(yùn)動(dòng)。活動(dòng)或能夠活動(dòng)之物'
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: '豬是一種脊椎動(dòng)物、哺乳動(dòng)物、家畜,也是古雜食類哺乳動(dòng)物,主要分為家豬和野豬'
}
// 動(dòng)物
console.log(pig.getName())
// 它們一般以有機(jī)物為食,能感覺,可運(yùn)動(dòng),能夠自主運(yùn)動(dòng)。活動(dòng)或能夠活動(dòng)之物
console.log(pig.getFeature())
??因?yàn)橹灰?code>function都會(huì)執(zhí)行bind
綁定當(dāng)前被代理的對(duì)象animal
,所以當(dāng)pig
通過原型繼承了animalProxy
之后this
訪問的都是animal
,還有,這意味著我們要熟悉被代理對(duì)象內(nèi)的api
,通過識(shí)別是否是私有屬性訪問才綁定this
,需要了解被代理對(duì)象的api
。還有一個(gè)問題是私有屬性只允許自身訪問,在沒有代理的幫助下上方的pig.getName()
會(huì)出錯(cuò)TypeError
,而通過bind
之后就可以正常訪問,這一塊要看具體業(yè)務(wù),不過還是建議跟沒代理時(shí)保持一致,這里處理比較簡(jiǎn)單,在知道使用私有屬性api
之后,只要識(shí)別當(dāng)前訪問對(duì)象是否是原對(duì)象的代理即可。具體處理代碼下方所示
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}方法僅允許自身調(diào)用`
}
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: '豬是一種脊椎動(dòng)物、哺乳動(dòng)物、家畜,也是古雜食類哺乳動(dòng)物,主要分為家豬和野豬'
}
// 動(dòng)物
console.log(animalProxy.getName())
// TypeError
// console.log(pig.getName())
// 豬是一種脊椎動(dòng)物、哺乳動(dòng)物、家畜,也是古雜食類哺乳動(dòng)物,主要分為家豬和野豬
console.log(pig.getFeature())
target !== Proxy
??代理跟原對(duì)象肯定是不同的對(duì)象,所以當(dāng)我們使用原對(duì)象進(jìn)行管理后代理卻無法進(jìn)行正確管理,比如下方代理做了一個(gè)所有用戶實(shí)例的集中管理:
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ā)時(shí)假如對(duì)一個(gè)對(duì)象做代理時(shí),對(duì)代理的所有管理也需要再進(jìn)行一層代理,原對(duì)象對(duì)原對(duì)象,代理對(duì)代理,比如上方這個(gè)實(shí)例可以通過下方代碼改進(jìn)
const users = new Set()
class User {
constructor() {
users.add(this)
}
}
// 獲取原對(duì)象
const getRaw = (target) => target[toRaw] ? target[toRaw] : target
const toRaw = Symbol('toRaw')
const usersProxy = new Proxy(users, {
get(target, prop) {
// 注意Set size是屬性,而不是方法,這個(gè)屬性用到了內(nèi)部插槽,
// 所以不能夠使用Reflect.get(...arguments)獲取
let value = prop === 'size'
? target[prop]
: Reflect.get(...arguments)
value = typeof value === 'function'
? value.bind(target)
: value
// 這里只做兩個(gè)api示例,當(dāng)添加或者判斷一定是通過原對(duì)象判斷添加,
// 因?yàn)樵瓕?duì)象的管理只能放原對(duì)象
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)
// 因?yàn)闀?huì)轉(zhuǎn)化為原對(duì)象添加,而原對(duì)象已有 所以添加不進(jìn)去
usersProxy.add(userProxy)
// 1
console.log(users.size)
// 1
console.log(usersProxy.size)
??Proxy
就介紹到這里了,本文介紹了Proxy
大部分要注意的問題以及用法。