ES6 Proxy和Reflect

 

Proxy

一、概述

Proxy意思是代理器,ES6新增特性。Proxy对象用于创建一个对象的代理,这个代理会进行拦截操作,在对目标对象的访问和修改前都需要通过这层拦截,在拦截中进行需要的操作。

 

二、语法

let proxy = new Proxy(target, handler)
  • target:要进行代理的目标对象,可以是任何东西,包括函数。
  • handler:代理配置,或者说拦截操作,是带有钩子的对象。其中钩子包括获取或设置target属性的get和set

一个示例:

let target = {}
let proxy = new Proxy(target, {})

proxy.test = 5
console.log(target.test) // 5
console.log(proxy.test) // 5

 上述示例创建了一个代理,但是没有任何钩子。此时所有对proxy的操作都直接转发给target,例如设置对象属性和读取属性值。此时,没有设置拦截操作的proxy就是对target的一个透明包装。proxy对象本身没有自己的属性。想要让proxy有意义,就要进行拦截。

 

Proxy对目标对象支持的拦截操作共13种,包括以下:

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

 

其中最常见的拦截是get和set钩子,即在读写时进行。

 

1、get

get(target, property, receiver)用于拦截读取操作。

  • target:目标对象,该对象作为第一个参数传递给new Proxy
  • property:目标属性名
  • receiver:如果目标属性是一个getter访问器属性,则receiver就是本次读取属性所在的this对象。通常它就是proxy对象本身。

一个示例:

let person = {
  name: 'Leon'
}

let proxy = new Proxy(person, {
  get: (target, propKey) => {
  	console.log(`访问对象的${propKey}属性`)
    if(propKey in target) return target[propKey]
    else throw new ReferenceError('该属性不存在')
  }
})

console.log(proxy.name)
console.log(proxy.age)

 上述示例对person对象的访问操作进行了拦截,每次访问的时候都会打印出访问信息,最重要的是访问属性存在时会返回访问属性,不存在则抛出异常。

let arr = [1, 2, 3, 4, 5, 6]
let proxy = new Proxy(arr, {
  get: (target, propKey, receiver) => {
  	let ind = Number(propKey)
    if(ind < 0) propKey = String(target.length + ind)
    return Reflect.get(target, propKey, receiver)
  }
})
console.log(proxy[-2])

上述代码让一个数组可以通过负数索引访问元素。

get方法可以继承。

 

 2、set

 set(target, property, value, receiver)用于拦截写入操作。

相比get钩子,多出来一个value参数,即目标属性要设置的值。如果写入成功,set应该返回true,否则返回false

let validator = {
  set: (obj, prop, value) => {
  	if(prop === 'age') {
      if(!Number.isInteger(value)) throw new TypeError('Age must be a integer')
      if(value > 200) throw new RangeError('Nobody can live more than 200 years')
    }
    obj[prop] = value
    return true
  }
}

let person = new Proxy({}, validator)
person.age = 24
person.age = 'young'
person.age = 201

上述代码用proxy实现了一个验证:当给person对象设置年龄时只能设置为小于等于200的整数。

 

 3、ownKeys和getOwnPropertyDescriptor

Object.keys,for...in循环和大多数遍历对象属性的方法(包括Object.values)都由ownKeys拦截。此钩子返回一个数组。

let user = {
  name: 'John',
  age: 20,
  _password: '123456'
}

user = new Proxy(user, {
  ownKeys(target){
    return Object.keys(target).filter(key => !key.startsWith('_'))
  }
})

for(let key in user) console.log(key)
console.log(Object.keys(user))
console.log(Object.values(user))

 上述代码中的代理让对象在属性遍历时隐藏了以_开头的属性。

 

注意:如果返回对象中并不存在的键,Object.keys并不会列出该键。

let user = {}
user = new Proxy(user, {
  ownKeys(target) {
    return ['a', 'b', 'c']
  }
})

console.log(Object.keys(user)) // []
for(key in user) console.log(key) // undefined

因为Object.keys仅返回带有enumerable标记的属性。为了检查它,该方法会对每个属性调用[[GetOwnProperty]]来获得属性描述符。在这里,由于没有属性,其描述符为空,没有enumerable标记,因此会被略过。

 为了让Object.keys返回一个属性,可以将该属性和enumerable标记存入对象,或者拦截对它的调用[[GetOwnProperty]],使用钩子getOwnPropertyDescriptor并返回描述符enumerable: true

let user = { };
user = new Proxy(user, {
  ownKeys(target) {
    return ['a', 'b', 'c'];
  },

  getOwnPropertyDescriptor(target, prop) {
    return {
      enumerable: true,
      configurable: true
    };
  }
});

console.log(Object.keys(user))

 

 4、实现私有属性

JS没有private关键字,但私有属性和方法对某些场景是必要的。一个普遍的约定是以下划线_开头的属性和方法是内部的,不该从对象外部访问它们。可以使用Proxy实现这一需求。

私有属性的实现需要这些钩子:

  • get,读取私有属性时抛出错误
  • set,写入私有属性时抛出错误
  • deleteProperty,删除私有属性时抛出错误
  • ownKeys,遍历属性时排出私有属性
let user = {
  name: 'John',
  age: 18,
  _password: 'lock'
}

user = new Proxy(user, {
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    }
    let value = target[prop];
    return (typeof value === 'function') ? value.bind(target) : value;
  },
  set(target, prop, val) {
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      target[prop] = val;
      return true;
    }
  },
  deleteProperty(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error("Access denied");
    } else {
      delete target[prop];
      return true;
    }
  },
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

console.log(user._password)
user._password = 'test'
delete user._password
console.log(Object.keys(user))

 示例中,访问对象中方法时需要调用value.bind(target),将原始对象作为this,这样在调用方法中有关操作私有属性时不会触发拦截钩子。

这种方式存在很多问题,在任何地方都不该使用这种代理。对于类来说,新的ES标准原生支持私有属性,以#为前缀。

 

5、has

has(target, property)会在使用in运算符时调用。

let range = {
  start: 1,
  end: 10
}

range = new Proxy(range, {
  has(target, prop){
    return prop >= target.start && prop <= target.end
  }
})

console.log(5 in range)
console.log(20 in range)

 

6、apply

apply(target, thisArg, args)拦截函数的调用,call和apply操作。

  • target:目标对象
  • thisArg:目标this值
  • args:参数列表
let target = function(){
  return 'This is a target function'
}

target = new Proxy(target, {
  apply() { return 'This is a proxy' }
})

target()

 

7、可取消的Proxy

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

此调用返回一个带proxy和revoke函数的对象以将其禁用。

let object = {
  data: 'Valuable data'
}
let {proxy, revoke} = Proxy.revocable(object, {})

console.log(proxy.data) // Valuable data
revoke()
console.log(proxy.data) // Error

 

 

 

Reflect

一、概述

Reflect是一个内置对象,提供拦截JS操作的方法,可简化的创建Proxy。与多数全局对象不同,Reflect不是一个构造函数,不能通过new运算符调用,不能把它当作函数调用。Reflect所有属性和方法都是静态的,就像Math对象。

 

二、Reflect作用

  • 存放Object对象的一些语言内部的方法(如Object.defineProperty)。现在某些内部方法同时在Object和Reflect对象上存在,但未来的新方法只会放到Reflect对象上。
  • 修改Object方法的返回结果,如Object.defineProperty(obj, name, desc)在无法定义属性时会抛出错误,而Reflect.defineProperty(obj, name, desc)会返回false。
  • 把Object操作都变成函数行为。某些Object操作是命令式,如name in obj和delete obj[name],可以用Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)进行同种操作,变为函数行为。
  • 简化Proxy。只要是Proxy对象的方法就能在Reflect对象上找到对应方法。Proxy对象就可以方便调用对应的Reflect方法,完成默认行为,作为修改行为的基础。

 

let user = { name: 'John' }

user = new Proxy(user, {
  get(target, prop, receiver){
    console.log(`Get ${prop}`)
    return Reflect.get(target, prop, receiver)
  },
  
  set(target, prop, val, receiver){
    console.log(`Set ${prop}=${val}`)
    return Reflect.set(target, prop, val, receiver)
  }
})

let name = user.name
user.name = 'Tom'

上述示例简单来说,如果Proxy向把调用转发给目标对象,只需要使用相同参数调用Reflect的同名方法。多数情况下可以不用Reflect完成相同的事情。但Reflect的确可以解决一些问题

let user = {
  _name: 'Guest',
  get name() {
    return this._name
  }
}

let userProxy = new Proxy(user, {
  get(target, prop, receiver){
    return target[prop]
  }
})

let admin = {
  __proto__: userProxy,
  _name: 'Admin'
}

console.log(admin.name) // Guest

上述代码出现了预期外的结果,因为读取admin.name时admin对象会从原型链上找,从userProxy中触发get拦截,此时target是上下文的this,即原始的user对象。可以使用Reflect解决这个问题

let user = {
  _name: 'Guest',
  get name() {
    return this._name
  }
}

let userProxy = new Proxy(user, {
  get(target, prop, receiver){
    return Reflect.get(target, prop, receiver) // return Reflect.get(...arguments)
  }
})

let admin = {
  __proto__: userProxy,
  _name: 'Admin'
}

console.log(admin.name) // Admin

 

 

Proxy局限

内部插槽(Internal slots)

许多内置对象如Map,Set,Date,Promise都使用了内部插槽。它们类似于属性,但只能在内部使用,仅用于规范。例如,Map将项目存储在[[MapData]]中,内置方法直接访问它们而不通过[[Get]] [[Set]]等内部方法。因此Proxy不能进行拦截。

let map = new Map()
let proxy = new Proxy(map, {})

proxy.set('test', 1) // Error

但有一种解决方法

let map = new Map()

let proxy = new Proxy(map, {
  get(target, prop, receiver){
    let value = Reflect.get(...arguments)
    return typeof value == 'function' ? value.bind(target) : value
  }
})

proxy.set('test', 1)
console.log(proxy.get('test'))

 

类的私有字段也可能发生类似情况。

 

posted @ 2021-12-07 11:04  最期の詩  阅读(159)  评论(0)    收藏  举报