Proxy和Reflect的要注意的問題與局限性

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 objectnull 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 objectundefined 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ù)的receviernewTarget其他參數(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對象時調用者是誰,其實與functionthis的機制是一致的怎棱。

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關鍵字,我們先講講newsuper的內(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薛闪,SetDate俺陋,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)部插槽屬性袋狞,所以只能失敗,要處理這個問題可以像代理具有私有屬性的對象中一樣的方式處理映屋,將functionthis綁定,這樣訪問時就能正確的找到內(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(可枚舉)的非Symbolkey温算。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, valuerecevier 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或者setterthis指向的因為他們不是一個方法贝椿,參考下方示例:

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的結合

??因為ReflectProxy捕捉器都有對應的方法渡蜻,所以大部分情況下我們都能直接使用ReflectAPI來對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撤銷代理中實例看到芦岂,我們既然添加了proxyrevokeWeakMap對象瘪弓,為什么不多添加一份proxytarget的對象呢,說說干就干

...
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è)務中使用到的commoditykey沖突虑乖,而且要確保只有內(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大部分要注意的問題以及用法顽爹。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市骆姐,隨后出現(xiàn)的幾起案子镜粤,更是在濱河造成了極大的恐慌,老刑警劉巖诲锹,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件繁仁,死亡現(xiàn)場離奇詭異,居然都是意外死亡归园,警方通過查閱死者的電腦和手機黄虱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來庸诱,“玉大人捻浦,你說我怎么就攤上這事晤揣。” “怎么了朱灿?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵昧识,是天一觀的道長。 經(jīng)常有香客問我盗扒,道長跪楞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任侣灶,我火速辦了婚禮甸祭,結果婚禮上,老公的妹妹穿的比我還像新娘褥影。我一直安慰自己池户,他們只是感情好,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布凡怎。 她就那樣靜靜地躺著校焦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪统倒。 梳的紋絲不亂的頭發(fā)上寨典,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機與錄音檐薯,去河邊找鬼凝赛。 笑死注暗,一個胖子當著我的面吹牛坛缕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捆昏,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼赚楚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了骗卜?” 一聲冷哼從身側響起宠页,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎寇仓,沒想到半個月后举户,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡遍烦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年俭嘁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片服猪。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡供填,死狀恐怖拐云,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情近她,我是刑警寧澤叉瘩,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站粘捎,受9級特大地震影響薇缅,放射性物質發(fā)生泄漏。R本人自食惡果不足惜攒磨,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一捅暴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧咧纠,春花似錦蓬痒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至演痒,卻和暖如春亲轨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鸟顺。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工惦蚊, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人讯嫂。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓蹦锋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親欧芽。 傳聞我的和親對象是個殘疾皇子莉掂,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 在學習proxy和reflect之前我們先了解一下javascript中的Object。 Object構造函數(shù)的屬...
    淺浪丶閱讀 391評論 0 0
  • Study Notes[https://wuner.gitee.io/wuner-notes/fed-e-task...
    Wuner閱讀 234評論 0 0
  • Proxy 也就是代理千扔,可以幫助我們完成很多事情憎妙,例如對數(shù)據(jù)的處理,對構造函數(shù)的處理曲楚,對數(shù)據(jù)的驗證厘唾,說白了,就是在...
    小李不小閱讀 1,074評論 0 5
  • Map和WeekMap的區(qū)別 弱引用為垃圾回收會忽略該引用值的引用讯柔。也就是抡蛙,如果某個引用值被賦值給多個變量,當其他...
    一土二月鳥閱讀 386評論 0 0
  • 概念 Proxy 可以理解成魂迄,在目標對象之前架設一層“攔截”粗截,外界對該對象的訪問,都必須先通過這層攔截捣炬,因此提供了...
    隱號騎士閱讀 115評論 0 1