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

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, valuerecevier 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 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 一個(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ù)的receviernewTarget其他參數(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í)與functionthis的機(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)鍵字,我們先講講newsuper的內(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ì)象比如MapSetDatePromise都使用了內(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ì)象中一樣的方式處理,將functionthis綁定,這樣訪問時(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(可枚舉)的非Symbolkeyenumerable是從對(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, valuerecevier 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或者setterthis指向的因?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)的方法,所以大部分情況下我們都能直接使用ReflectAPI來對(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í)例看到,我們既然添加了proxyrevokeWeakMap對(duì)象,為什么不多添加一份proxytarget的對(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ù)中使用到的commoditykey沖突,而且要確保只有內(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大部分要注意的問題以及用法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 在學(xué)習(xí)proxy和reflect之前我們先了解一下javascript中的Object。 Object構(gòu)造函數(shù)的屬...
    淺浪丶閱讀 397評(píng)論 0 0
  • Study Notes[https://wuner.gitee.io/wuner-notes/fed-e-task...
    Wuner閱讀 247評(píng)論 0 0
  • Proxy 也就是代理,可以幫助我們完成很多事情,例如對(duì)數(shù)據(jù)的處理,對(duì)構(gòu)造函數(shù)的處理,對(duì)數(shù)據(jù)的驗(yàn)證,說白了,就是在...
    小李不小閱讀 1,138評(píng)論 0 5
  • Map和WeekMap的區(qū)別 弱引用為垃圾回收會(huì)忽略該引用值的引用。也就是,如果某個(gè)引用值被賦值給多個(gè)變量,當(dāng)其他...
    一土二月鳥閱讀 398評(píng)論 0 0
  • 概念 Proxy 可以理解成,在目標(biāo)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問,都必須先通過這層攔截,因此提供了...
    隱號(hào)騎士閱讀 116評(píng)論 0 1