Proxy和Reflect的要注意的问题与局限性

1,182 阅读21分钟

Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy被用于许多库和浏览器框架上,例如vue3就是使用Proxy来实现数据响应式的。本文带你了解Proxy的用法与局限性。

Proxy参数与说明

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)

参数

  • target 需要包装的对象,可以是任何变量
  • handle 代理配置,通常是用函数作为属性值的对象,为了方便表达本文以捕捉器函数来称呼这些属性

  对proxy进行操作时,如果handler对象中存在相应的捕捉器函数则运行这个函数,如果不存在则直接对target进行处理。
  在JavaScript中对于对象的大部分操作都存在内部方法,它是最底层的工作方式。例如对数据读取时底层会调用[[Get]],写入的时底层会调用[[Set]]。我们不能直接通过方法名调用它,而Proxy代理配置中的捕捉器函数则可以拦截这些内部方法的调用。

内部方法与捕捉器函数

下表描述了内部方法捕捉器函数的对应关系:

内部方法捕捉器函数函数参数函数返回值劫持
[[Get]]gettarget, property, recevierany读取属性
[[Set]]settarget, property, valuerecevierboolean表示操作是否写入属性
[[HasProperty]]hastarget, propertybooleanin 操作符
[[Delete]]deletePropertytarget, propertyboolean表示操作是否delete 操作符
[[Call]]applytarget, thisArg, argumentsListany函数调用
[[Construct]]constructtarget, argumentsList, newTargetobjectnew 操作符
[[GetPrototypeOf]]getPrototypeOftargetobjectnullObject.getPrototypeOf
[[SetPrototypeOf]]setPrototypeOftarget, prototypeboolean表示操作是否Object.setPrototypeOf
[[IsExtensible]]isExtensibletargetbooleanObject.isExtensible
[[PreventExtensions]]preventExtensionstargetboolean表示操作是否Object.preventExtensions
[[DefineOwnProperty]]definePropertytarget, property, descriptorboolean表示操作是否Object.defineProperty
ObjectdefineProperties
[[GetOwnProperty]]getOwnPropertyDescriptortarget, propertyobjectundefinedObject.getOwnPropertyDescriptor
for...in
Object.keys/values/entries
[[OwnPropertyKeys]]ownKeystarget一个可枚举object.Object.getOwnPropertyNames
Object.getOwnPropertySymbols
for...in
Object.keys/values/entries

捕捉器函数参数说明

  • target 是目标对象,被作为第一个参数传递给 new Proxy
  • property 将被设置或获取的属性名或 Symbol
  • value 要设置的新的属性值
  • recevier 最初被调用的对象。通常是proxy本身,但是可能以其他方式被间接地调用(因此不一定是proxy本身,后面我会说明)
  • thisArg 被调用时的上下文对象
  • argumentsList 被调用时的参数数组
  • newTarget 最初被调用的构造函数
  • descriptor 待定义或修改的属性的描述符

这里我们重点讲一下捕捉器函数参数的receviernewTarget其他参数就不一一介绍,基本上一看就懂了。

改造console.log

  在Proxy捕捉器函数中使用console.log很容易造成死循环,因为如果console.log(poxy)时会读取Proxy的属性,可能会经过捕捉器函数,经过捕捉器函数再次console.log(poxy)。为了方便调试,我这里改造了以下console.log

// 通过当前是否是log模式来判断是否是打印
let isLog = false
{
  const logs = []
  const platformLog = console.log
  const resolvedPromise = Promise.resolve()
  // 当前是否正在执行logs
  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经过捕捉器函数那么它就是谁。看下方实例说明

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捕捉器函数的recevier参数就是animalProxy
// 被代理的this就是animalProxy
animalProxy.getName()

const pig = {
  // 通过原型,继承animalProxy
  __proto__: animalProxy,
  test: animalProxy
}

// pig中不存在name,通过原型查找,原型是Proxy,读取时经过get捕捉器函数
// 最初被调用的对象时pig
// 这里访问时get捕捉器函数的recevier参数就是pig
// 被代理的this就是pig
pig.getName()

// 最初被调用的对象是pig.test即为animalProxy
// 这里访问时get捕捉器函数的recevier参数就是animalProxy
// 被代理的this就是animalProxy
pig.test.getName()

  上方示例清晰的说明了recevier,就是当调用proxy对象时调用者是谁,其实与functionthis的机制是一致的。

newTarget参数

  newTarget 最初被调用的构造函数,在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捕捉器函数被触发三次,
// 第一次是PetsPigProxy触发       NewTarget为PetsPigProxy
// 第二次是PigProxy触发           NewTarget为PetsPigProxy
// 第三次是AnimalProxy触发        NewTarget为PetsPigProxy
const pig = new PetsPigProxy()

  通过上面的例子我们可以比较清晰的知道最初被调用的构造函数的意思了,就是当外部使用new Type()时,无论是父类还是当前类 construct捕捉器函数newTarget参数都是指向这个Type。大家注意到上方的construct捕捉器函数内部实现中添加了设置原型,这里涉及到new关键字,我们先讲讲newsuper的内部工作原理 当用户使用new关键字时

  • 创建一个原型指向当前class原型的对象
  • 将当前class构建函数的this指向上一步创建的对象,并执行
  • 当遇到super()函数调用,将当前this指向父类构造函数并执行
  • 如果父类也存在super()函数调用,则再次执行上一步
  • super()执行完成,如果没有返回对象则默认返回this
  • super()执行的结果设置为当前构造函数的this
  • 当前class构造函数执行完成,如果没有返回对象则默认返回this

  所以当我们不指定原型的情况下,上方的代码就会丢失所有子类的原型,原型始终指向最顶级父类,因为super时也会调用construct捕捉器函数,这时new创建一个原型指向当前class原型的对象,并在返回时将子类的this改变为刚刚创建的对象,所以子类的this原型就只有父类的了。上面所使用的方法可以正常一切操作,但是这个实例终究是父级直接构造出来的,所以在构造方法中new.target是指向父类构造方法的,如果使用console.log打印出来会发现这个实例是Animal对象, 可能有些同学会想着这样优化,比如:

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的构造函数加了限制,在class构造期间会通过new.target检查当前是否是通过new关键字调用,class仅允许new关键字调用, 直接通过函数式调用会报错,所以这种方法也无效,目前我没找到其他方法,如果各位大神有方法麻烦评论区贴一下谢谢了。有个最新的对象可以解决这个问题就是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())

代理具有内部插槽的内建对象

  有许多的内建对象比如MapSetDatePromise都使用了内部插槽内部插槽类似于上面的对象的私有属性,不允许外部访问,所以当代理没做处理时,直接代理他们会发生错误例如:


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的,而内部插槽只允许内部访问,Proxy中没有这个内部插槽属性,所以只能失败,要处理这个问题可以像代理具有私有属性的对象中一样的方式处理,将functionthis绑定,这样访问时就能正确的找到内部插槽了。

const factoryInstanceProxy = instance => 
  new Proxy(instance, {
    get(target, prop) {
      const value = target[key]
      return typeof value === 'function'
        ? value.bind(target)
        : value
    }
    ...
  })

ownKeys捕捉器函数

  可能有些同学会想,为什么要把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))

  不错一切都预期运行,这时候产品过来加了个需求,根据身份证的前两位自动识别当前用户所在的省份,脑袋瓜子一转,直接在代理处识别添加不就好了,我们来改一下代码

// 附加属性列表
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会对每个属性调用内部方法[[GetOwnProperty]]获取它的属性描述符,返回自身带有enumerable(可枚举)的非Symbolkeyenumerable是从对象的属性的描述符中获取的,在上面的例子中province没有属性的描述符也就没有enumerable属性了,所以province会被忽略
  要解决这个问题就需要为province添加属性描述符,而通过我们上面内部方法与捕捉器函数表知道[[GetOwnProperty]]获取时会通过getOwnPropertyDescriptor捕捉器函数获取,我们加个这个捕捉器函数就可以解决了。


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参数中我们使用了不完美的construct捕捉器处理函数,在创建子类时会多次new父类对象,而且最终传出的也是顶级父类的对象,在console.log时可以看出。其实Proxy有一个最佳搭档,可以完美处理,那就是Reflect
  Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与Proxy捕捉器的方法相同。所有Proxy捕捉器都有对应的Reflect方法,而且Reflect不是一个函数对象,因此它是不可构造的,我们可以像使用Math使用他们比如Reflect.get(...),除了与Proxy捕捉器一一对应外,Reflect方法与Object方法也有大部分重合,大家可以通过这里,比较 Reflect 和 Object 方法

下表描述了Reflect捕捉器函数的对应关系,而对应的Reflect参数与捕捉器函数大部分,参考内部方法与捕捉器函数

捕捉器函数Reflect对应方法方法参数方法返回值
getReflect.get()target, property, recevier属性的值
setReflect.set()target, property, valuerecevierBoolean 值表明是否成功设置属性。
hasReflect.has()target, propertyBoolean 类型的对象指示是否存在此属性。
deletePropertyReflect.deleteProperty()target, propertyBoolean 值表明该属性是否被成功删除
applyReflect.apply()target, thisArg, argumentsList调用完带着指定参数和 this 值的给定的函数后返回的结果。
constructReflect.construct()target, argumentsList, newTargettarget(如果newTarget存在,则为newTarget)为原型,调用target函数为构造函数,argumentList为其初始化参数的对象实例。
getPrototypeOfReflect.getPrototypeOf()target给定对象的原型。如果给定对象没有继承的属性,则返回 null
setPrototypeOfReflect.setPrototypeOf()target, prototypeBoolean 值表明是否原型已经成功设置。
isExtensibleReflect.isExtensible()targetBoolean 值表明该对象是否可扩展
preventExtensionsReflect.preventExtensions()targetBoolean 值表明目标对象是否成功被设置为不可扩展
getOwnPropertyDescriptorReflect.getOwnPropertyDescriptor()target, property如果属性存在于给定的目标对象中,则返回属性描述符;否则,返回 undefined
ownKeysReflect.ownKeys()target由目标对象的自身属性键组成的 Array

Reflect的recevier参数

  当使用Reflect.get或者Reflect.set方法时会有可选参数recevier传入,这个参数时使用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参数

  当使用Reflect.construct时会有一个可选参数newTarget参数可以传入,Reflect.construct是一个能够new Class的方法实现,比如new User('bill')Reflect.construct(User, ['bill'])是一致的,而newTarget可以改变创建出来的对象的原型,在es5中能够用Object.create实现,但是有略微的区别,在构造方法中new.target可以查看到当前构造方法,如果使用es5实现的话这个对象是undefined因为不是通过new创建的,使用Reflect.construct则没有这个问题 参考下方两种实现方式

function OneClass() {
  console.log(new.target)
  this.name = 'one';
}

function OtherClass() {
  console.log(new.target)
  this.name = 'other';
}

// 创建一个对象:
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参数中我们实现了不完美的construct捕捉器,而通过阅读Reflect,我们知道了一个能够完美契合我们想要的能够实现的方案,那就是Reflect.construct不仅能够识别new.target,也能够处理多是创建对象问题,我们改造一下实现,示例如下


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函数

  我们通过阅读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())

  如果你运行上方代码会发现打印顺序依次是动物,动物,猪,动物,使用getName通过方法访问时是没问题的,因为代理拿到了getName的实现,然后通过当前对象访问,所以this是当前谁调用就是谁,但是通过getter调用时,在通过target[key]时就已经调用了方法实现,所以this始终是指向当前代理的对象target,想要修正这里就得通过代理内的捕捉器入手,修正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要执行的业务比如下方代码

new Proxy(animal, {
  get(target, key, recevier) {
    // 具体业务
    ...
    return Reflect.get(...arguments)
  },
  set(target, property, value, recevier) {
    // 具体业务
    ...
    return Reflect.set(...arguments)
  },
  has(target, property) {
    // 具体业务
    ...
    return Reflect.has(...arguments)
  }
  ...
})

Proxy.revocable撤销代理

  假如有这么一个业务,我们在做一个商城系统,产品要求跟踪用户的商品内操作的具体踪迹,比如展开了商品详情,点击播放了商品的视频等等,为了与具体业务脱耦,使用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
// 具体业务中使用
commodity = processingCommodity(commodity)

  我们编写了上方,不错很完美,但是后期一堆客户反应不希望自己的行踪被跟踪,产品又要求我们改方案,用户可以在设置中要求不跟踪,不能直接重启刷新页面,也不能让缓存中的商品对象重新加载这时候,如果让新的商品不被代理很简单只要加个判断就行了,但是旧数据也不能重新加载,那就只能撤销代理了,接下来我们介绍一下新的API
  Proxy.revocable(target, handler)方法可以用来创建一个可撤销的代理对象。该方法的参数与new Proxy(target, handler)一样,第一个参数传入要代理的对象,第二个参数传入捕捉器。该方法返回一个对象,这个对象的proxy返回target的代理对象,revoke返回撤销代理的方法,具体使用如下

const { proxy, revoke } = Proxy.revocable(target, handler)

  接下来我们改进一下我们的跟踪代码,如下

// track-commodity.js
...

// 为什么使用 WeakMap 而不是 Map,因为它不会阻止垃圾回收。
// 如果商品代理除了WeakMap之外没有地方引用,则会从内存中清除
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()
// 监听设置改变
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的内置逻辑,就是当get某个key时我们就返回代理的原始对象,当然这个key不能和业务中使用到的commoditykey冲突,而且要确保只有内部使用,所以我们需要使用到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的局限性

  代理提供了一种独特的方法,可以在调整现有对象的行为,但是它并不完美,有一定的局限性。

代理私有属性

  我们在代理具有私有属性的对象时介绍了如何避开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都会执行bind绑定当前被代理的对象animal,所以当pig通过原型继承了animalProxy之后this访问的都是animal,还有,这意味着我们要熟悉被代理对象内的api,通过识别是否是私有属性访问才绑定this,需要了解被代理对象的api。还有一个问题是私有属性只允许自身访问,在没有代理的帮助下上方的pig.getName()会出错TypeError,而通过bind之后就可以正常访问,这一块要看具体业务,不过还是建议跟没代理时保持一致,这里处理比较简单,在知道使用私有属性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)

  所以在开发中这类问题需要特别注意,在开发时假如对一个对象做代理时,对代理的所有管理也需要再进行一层代理,原对象对原对象,代理对代理,比如上方这个实例可以通过下方代码改进


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是属性,而不是方法,这个属性用到了内部插槽,
    // 所以不能够使用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大部分要注意的问题以及用法。