概述
之前在使用JSONRPC
做为与后端通讯标准的时候一直在思考究竟怎么样才算RPC(Remote Procedure Call远程过程调用)?。其核心在于像调用本地函数一样来完成远程的调用。因为多是一些硬件指令的发送, 所以会有一个Hardware
的类
类中抽象出一个实例writeCode
的方法,新建硬件的时候便新建一个hardware
实例, 在这个方法中传入需要写入的指令。比如 “回零” 就是 setHome
。
const hardware = new Hardware()
hardware.writeCode('setHome')
但这样并不是我理解的完全意义上的远程调用. 我理解的是如果是回零的功能只需要以下调用即可:
hardware.setHome()
看起来实现很简单, 只需要在Hardware
这个类中再新建一个setHome
的实例属性, 但有个问题:
如何知道会有多少个硬件方法呢? 难道没新增一个方法就需要新增一个实例属性? 实际上也破坏了面向对象三大原则的 封装性
python 中有一个__getAttr__
这样的语法形式, 其本质就是实现元编程 meta program.举个例子:
class Dummy(object):
def __getattr__(self, attr):
return attr.upper()
d = Dummy()
d.does_not_exist # 'DOES_NOT_EXIST'
d.what_about_this_one # 'WHAT_ABOUT_THIS_ONE'
那么js中有没有类似的语法支持呢? 我在找到Proxy
之前没有找到对应内容, 如果有小伙伴知道的还请留言指教.
学习Proxy
是在学习ES6语法的时候,并没有太深的印象,因为总觉得用不太上, 工作中也很少见有人写.但在全球首届VUE Conf上尤大公开了开发进度, 其中有这么一张PPT展示Vue3.0会比Vue2.x更快的原因.(截图来自VUE Conf)
Object.defineProperty
这样的方式, 而是使用Proxy
.
说了那么多, 其实就是Proxy
真的蛮有用的, 希望这篇文章能帮助大家对Proxy
有个更深的认识. 进入正题
Proxy详解
简介
Proxy源自ES6, 并且没有向下兼容的polyfills, 也就是说, 你如果要使用Babel编译成ES5的语法, 那么就无法使用Proxy了. Proxy的支持程度见下:(截图来自mdn)不出人意料的IE没戏. 所以请酌情使用.
Proxy
是一个全新结构, 可以赋予我们在一些基本操作中进行拦截以及添加新行为的能力. (具体是哪些基本操作, 之后会详述.) 更具体的讲就是可以定义一个代理(proxy)对象, 关联到一个目标(target)对象, 那么这个代理对象就可以视为目标对象的抽象, 从而实现在一些对目标对象的基本操作实现之前进行拦截和控制.
从很多方面来看, 很类似与C++的指针, 可以代表目标对象的所指, 但是实际上又彻底跟目标对象完全不同. 目标对象可以通过直接操作(manipulate directly ), 也可以通代理去操作, 直接操作的话就会失去代理的功能了.
创建一个透传(passThrough)的Proxy
如上所属, 所有对代理的操作最终都会到目标对象上, 因此可以在任何使用目标对象的地方使用代理对象.
一个代理对象是通过Proxy
的构造函数来生成的. 需要同时提供一个目标对象以及一个处理(handler)对象,没有的话会产生一个 TypeError
的错误.一个透传的Proxy
, 可以使用一个{}
作为处理对象来实现
const target = {
id: 'target'
}
const handler = {}
const proxy = new Proxy(target, handler)
console.log(target.id) // target
console.log(proxy.id) // target
// 对目标对象直接赋值会同时改变
target.id = 'foo'
console.log(target.id) // foo
console.log(proxy.id) // foo
// 对代理对象赋值也一样
proxy.id = 'bar'
console.log(target.id) // bar
console.log(proxy.id) // bar
// 两者肯定是不等的
console.log(target === proxy) // false
// 注意不能对Proxy使用instanceof
proxy instanceof Proxy // Uncaught TypeError: Function has non-object prototype 'undefined' in instanceof check
定义陷阱(traps)
如上述创建一个透传的代理是没什么意义的, 跟操作目标对象没有任何的区别. 接下来将会分别介绍几种常用的陷阱
get
示例如下
const target = {
name: 'lorry'
}
const handler = {
get() {
return 'override name'
}
}
const proxy = new Proxy(target, handler)
target.name // lorry
proxy.name // override name
当proxy的get()
被调用的时候, 陷阱函数对应的get方法将会被触发. 当然get函数不是显式触发的(你没有看到我使用proxy.get
方法吧?), 以下的操作均会隐式的触发get的操作
proxy[property], proxy.property, Object.create(property)[property]
等都会触发. 但是目标对象的调用不会有任何的影响.
那么, 现在问题是, 我拦截了, 也许并不想返回一个错误的值, 而只是知道一下是否有人获取值, 从中做一个通知操作什么的. 我该怎么返回目标对象对应的值呢?有两种方法
陷阱参数
所有的陷阱都可以访问到原始行为的所有方法. 比如get参数就有三个值: target, property, reciever
将上例改写成:
const handler = {
get(trapTarget, property, reciever) {
console.log(trapTarget === target)
console.log(reciever === proxy)
console.log(property)
}
}
// 省略target和proxy的创建,同上
proxy.name
// true
// true
// name
所以, 要返回目标对象的值很简单,return trapTarget.property
即可.
上述的策略可以使用与所有的陷阱, 但并不是所有的行为都会像get()
那么简单, 这不是一个上策. 除了手动实现被拦截方法的内容之外, 原始行为是被封装在一个Reflect
的对象中.
Reflect
在handler中每个可以被拦截的方法都会有对应的 Reflect
API, 该对象的函数签名以及方法名都跟被拦截的原始行为一毛一样, 因此可以通过下例来实现透传代理
const handler = {
get(){
return Reflect.get(...arguments) // 这里的arguments实际上就是之前例子中的trapTarget, property, reciever
}
}
proxy.name // lorry
如果仅仅是透传而没有别的操作(虽然这种可能性为0, 但此处是为了演示器使用方法)
// 方式一
const handler = {
get: Reflect.get
}
// 方式二
const proxy = new Proxy(target, Reflect)
有此拦截之道岂不是可以为所欲为?
const target = {
name: 'lorry',
age: 26
}
const handler = {
get(trapTarget, property, reciever) {
let decoration = ''
if (property === 'name') {
decoration = '!!!'
}
return Reflect.get(...arguments) + decoration
}
}
const proxy = new Proxy(target, handler)
proxy.name // lorry!!!
proxy.age // 26
陷阱不可变量
陷阱给与了我们如此强大的能力, 几乎是可以改变任意的基本方法, 但是他们也不是没有限制. 每个陷阱都知道target对象的上下文, 以及陷阱函数签名, 而且陷阱处理函数必须遵循ECMAScript定义的陷阱不变量, 陷阱不变量根据不同的方法而不同,但基本上来说都不允许陷阱去定义展现任何非预期的行为(unexpected behavior).
上栗子, 如果目标对象有一个禁止配置和禁止写入, 那么当尝试从陷阱中返回不同于源目标值的时候便会报 TypeError
的错误
const target = {}
Object.defineProperty(target, 'name', {
configurable: false,
writable: false,
value: 'lorry'
})
const handler = {
get() {
return 'jiang'
}
}
const proxy = new Proxy(target, handler)
proxy.name //
便会报出如下错误
但是如果handler是返回源值的话
const handler = {
get() {
return Reflect.get(...arguments)
}
}
这样是不会报错的.
可撤销的代理
世上最贵的就是后悔药, 那么我们拦截了之后如果某个特定的场景忽然不想再拦截了呢?有没有办法解除代理对象和目标对象的关系? 如果是按照 new Proxy()
来创建的代理对象, 那么会在整个代理对象的生命周期中都维持这个关系, 无法接触.
不过Proxy
也暴露了一个revocable
的方法, 他提供了解除这种代理关系的能力. 但是这种关系的解除是不可逆的, 不要想着离婚了还能复婚. 而且跟promise
的reject
和resolve
一样, 都只能有效调用一次, 之后的调用都是无效的(但不会报错). 在撤销了之后再调用代理的方法, 就会抛出一个TypeError
const target = {
name: 'lorry'
}
const handler = {
get() {
return 'intercepted'
}
}
const {proxy, revoke} = Proxy.revocable(target, handler)
console.log(proxy.name) // intercepted
console.log(target.name) // lorry
revoke()
console.log(proxy.name) // TypeError
Reflect API
之前在说到如何拿到target数据的时候有提出Reflect
的基本使用, 以下是几个使用Reflect
的理由.
Reflect API VS Object API
当深入了解Reflect
之后, 记住
- Reflect API 不仅仅只能在陷阱处理函数中使用
- 大多数Reflect API方法都在
Object
类型上有一个模拟
总体来说, 对象方法是被大多数应用使用的, 而Reflect
方法是被对对象控制的微调和操作.
状态标识 Statues Flags
许多Reflect
返回一个布尔值, 表示该操作是否成功或失败. 在某种情况下, 这是相比与其他Reflect API, 比如返回一个被修改对象, 或抛出一个错误的行为更有用.
来看个例子, 如果不使用Proxy
const o = {}
try {
Object.defineProperty(o, 'name', {value: 'lorry'})
console.log('success')
} catch(e) {
console.log('failed')
}
如果使用Proxy
是这样的
const o = {}
if(Reflect.defineProperty(o, 'name', {value: 'lorry'})) {
console.log('success')
} else {
console.log('failed')
}
// success
以下的Reflect
方法均提供了状态标识
Reflect.defineProperty
Reflect.preventExtensions
Reflect.setPrototypeOf
Reflect.set
Reflect.deleteProperty
取代头等函数的操作
以下几个Reflect方法是只能通过操作符来
Reflect.get
获取对象属性时[]
, 或.
Reflect.set
设置对象属性时,=
Reflect.has
使用in
或with
Reflect.deleteProperty
删除对象属性, 使用delete
时Reflect.constructor
创建实例, 使用new
时
安全函数的应用
这里想提一下apply
, 因为任何方法都可以自己去实现apply
从而override掉原生行为.
function test(name) {return 'Hello' + name}
test.apply = console.log
test.apply(this, 'lorry') // 将会打印window对象和'lorry''
所以有一个办法时借用
Function.prototype.apply.call(myFn, thisVal, argumentsList)
这样很长...这里也可以使用Reflect.apply(myFn, thisVal, argumentsList)
代理一个代理
代理也有能力拦截Reflect
的API, 也就意味着创建一个代理的代理理论上是没问题, 但要结合实际场景去考虑. 这种能力给予了我们在一个目标对象上创建多层指令的可能性.
const target = {
name: 'lorry'
}
const firstProxy = new Proxy(target, {
get() {
console.log('first proxy')
return Reflect.get(...arguments)
}
})
const secondProxy = new Proxy(firstProxy, {
get() {
console.log('second proxy')
return Reflect.get(...arguments)
}
})
secondProxy.name // lorry
// second proxy
// first proxy
代理的思考和缺点
如刚开始所说, 代理是一个全新的内建API, 它是被写进了ECMAScript, 也就意味着他们会被最好实现, 大多数情况下, 代理在对象的抽象层的功能做得很好. 但是某些情况下不能无缝集成到ECMAScript的结构中.
proxy的this
可能你以为这个方法内的this
是指向它被调用的对象, 就像下面这样.如果是调用proxy.outerMethod()
这将会反过来调用target里对应的方法, this.innerMethod()
,this
是被proxy.innerMethod()
触发调用的.
const target = {
thisValEqualsProxy() {
return this === proxy
}
}
const proxy = new Proxy(target, {})
console.log(target.thisValEqualsProxy()) // false
console.log(proxy.thisValEqualsProxy()) // true
在大多数情况下都是符合这样的预期行为, 但是如果target 依赖于对象标识符, 就有意料之外.比如 WeakMap
, 它也是 ES6 的新数据结构, 可以方便的创建私有变量.
const wm = new WeakMap()
class User {
constructor(userId) {
wm.set(this, userId)
}
set id (userId) {
wm.set(this, userId)
}
get id() {
return wm.get(this)
}
}
如果加上代理
const user = new User(123)
console.log(user.id) // 123
const userProxy = new Proxy(user, {})
console.log(userProxy.id) // undefined
user
的实例, 也就是目标对象最初是与 WeakMap
保持引用, this
是 user
, 但是如果代理去获取, this
是代理对象. 解决这个问题的方法是往上提一级, 直接代理类而不是实例(还记得上面说的可以代理任何对象吗? 类也是一个对象), 然后实例化一个这样的代理类. 这样就可以将WeakMap
的关联放在代理实例中实现.
const UserClassProxy = new Proxy(User, {})
const userProxy = new UserClassProxy(123)
console.log(userProxy.id) // 123
代理和内部槽
什么是内部槽(internal slot)? 详情请参见ECMAScript2015标准以下是我的理解:
- 很像内部私有变量, 储存内部使用的数据, 不对外暴露, 不可直接访问
- 有外部暴露的接口可以由该接口间接调用到该数据. 比如
[[StringData]]
,toString
的时候就可获取到该值.
这里需要说明的是, 代理通常情况都能够正常代理那些需要访问内部槽的属性, 比如 Array
. 但是也有部分是不可以的, 一个很典型的例子就是Date
对象. 它有一个叫做[[NumberData]]
的内部槽, 因为代理对象没有这个私有槽,并且也不能通过get
或set
的方法访问到这个内部槽(否则就可以通过拦截或重定向到目标对象中实现, Reflect.get/set
),所以访问这个内部槽就会报出错误
const target = new Date()
const proxy = new Proxy(target, {})
console.log(proxy instanceof Date) // true
proxy.getDate() // Uncaught TypeError: this is not a Date object.
代理陷阱和反射方法
现在来总结一下, 代理一共可以拦截13种不同的基本操作, 每一个都在Reflect
上有对应的API, 参数, 关联的ECMAScript操作, 以及不变量.
get()
- 参数
target
目标对象property
拦截的目标函数属性, 字符串reciever
代理对象, 或者代理对象的继承对象
- 返回值 非严格的
- 可被拦截的操作
proxy.property
proxy[property]
Object.create(proxy).property
Reflect.get(proxy, property, receiver)
- 陷阱不变量(invariant)
- 如果
target.property
被配置为不可写, 或者不可被配置, 那么返回值必须返回target.property
- 如果
target.property
不可被配置, 并且[[Get]]的属性还是undefined
, 那么必须返回undefined
- 如果
const target = {}
const proxy = new Proxy(target, {
get(target, property, receiver) {
console.log('get')
return Reflect.get(...arguments) // 注意与Reflect.get(proxy, property, receiver)不同
}
})
proxy.foo // get
set()
- 参数
target
目标对象property
拦截的目标函数属性, 字符串value
待设置的值reciever
代理对象, 或者代理对象的继承对象
- 返回值
true
表示设置成功false
表示设置失败, 在严格模式下抛出TypeError
的异常
- 可被拦截的操作
proxy.property = value
proxy[property] = value
Object.create(proxy).property = value
Reflect.set(proxy, property, value, receiver)
- 陷阱不变量(invariant)
- 如果
target.property
被配置为不可写, 或者不可被配置, 那么属性值无法被更改 - 如果
target.property
不可被配置, 并且[[Set]]的属性还是undefined
, 那么属性值无法被更改 - 返回false的handler在严格模式下会抛出
TypeError
的错误
- 如果
'use strict'
const target = {age: 18}
const proxy = new Proxy(target, {
set(target, property, value, reciever) {
console.log('set',value, 'to', property)
Reflect.set(...arguments)
if (property === 'age') {
return false
}
return true
}
})
proxy.name = 'lorry' // set lorry to name
proxy.age = 24 // set 24 to age
// Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'age'
console.log(target) // {age: 24, name: "lorry"}
可以看到, 虽然在设置age
时报错, 但是因为我们已经使用了Reflect.set
方法, 并不会影响对target的设置, 要阻止设置的话, 可以使用Object.defineProperty
设置writable
和configurable
均为false
, 也可以在Reflect.set
函数调用之前就return掉.
has()
- 参数
target
目标对象property
拦截的目标函数属性, 字符串
- 返回值 必须返回一个布尔值表示该属性是否存在. 非布尔值会被强制转换为布尔值
- 可被拦截的操作
property in proxy
with(proxy) {(property)}
property in Object.create(proxy)
Reflect.has(proxy, property)
- 陷阱不变量(invariant)
- 如果
target.property
存在且是不可被配置的, handler必须返回true - 如果
target.property
存在但是目标对象是不可扩展的(Object.isExtensible(target) === true
, 可以通过Object.preventExtensions(target)
设置可扩展性), 那么handler必须返回true
- 如果
const target = {name: 'lorry'}
const proxy = new Proxy(target, {
has(target, property) {
console.log('has', property)
Reflect.has(target, property)
return false
}
})
'name' in proxy// has name
// 如果是不可扩展
Object.preventExtensions(target)
'name' in proxy // 2 Uncaught TypeError: 'has' on proxy: trap returned falsish for property 'name' but the proxy target is not extensible
defineProperty()
- 参数
target
目标对象property
拦截的目标函数属性, 字符串descriptor
对象包含以下可选定义- enumerable
- configurable
- writable
- value
- get
- set
- 返回值 必须返回一个布尔值, 表示该属性是否被成功定义, 非布尔值会转成布尔值
- 可被拦截的操作
Object.defineProperty(proxy, property, descriptor)
Reflect.defineProperty(proxy, property, descriptor)
- 陷阱不变量(invariant)
- 如果目标对象不可扩展, 那么属性不可以被添加
- 如果目标函数的属性已经被设置为可配置, 那么便不可对其进行更改, 即添加不可配置的相同属性是无效的
- 同理, 如果目标函数的该属性已经被设置为不可配置, 那么可配置的属性便不可被添加.
const target = {name: 'lorry'}
const proxy = new Proxy(target, {
defineProperty(target, property, descriptor) {
console.log('define property', property)
return Reflect.defineProperty(...arguments)
}
})
Object.defineProperty(proxy, 'name', {
value: 'jiang'
})
// define property name
Object.defineProperty(target, 'age', {
value: 18,
configurable: false
})
Object.defineProperty(proxy, 'age', {
value: 24,
configurable: true
})
// Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property 'age'
getOwnPropertyDescriptor()
- 参数
target
目标对象property
拦截的目标函数属性, 字符串
- 返回值 必须返回一个对象, 或者如果该属性不存在, 返回一个
undefined
- 可被拦截的操作
Object.getOwnPropertyDescriptor(proxy, property)
Reflect.getOwnPropertyDescriptor(proxy, property)
- 陷阱不变量(invariant)
- 如果
target.property
存在, 并且是不可配置的, 那么handler必须返回一个对象来表示该属性存在 - 如果
target.property
存在并是可配置的, handler不能返回一个表示该属性可配置的对象 - 如果
target.property
存在并且target是不可扩展的, handler必须返回一个对象表示该对象存在 - 如果
target.property
不存在, 并且target不可扩展, 那么handler必须返回undefined表示该属性不存在 - 如果
target.property
不存在, handler不能返回一个表明该属性可配置的对象
- 如果
const target = {name: 'lorry'}
const proxy = new Proxy(target, {
getOwnPropertyDescriptor(target, property) {
console.log('getOwnPropertyDescriptor', property)
return Reflect.getOwnPropertyDescriptor(...arguments)
}
})
Object.getOwnPropertyDescriptor(proxy, 'name')
// getOwnPropertyDescriptor name
Object.defineProperty(target, 'age', {
configurable: false,
value: 17
})
const proxy2 = new Proxy(target, {
getOwnPropertyDescriptor(target, property) {
console.log('getOwnPropertyDescriptor', property)
const obj = Reflect.getOwnPropertyDescriptor(...arguments)
obj.configurable = true
return obj
}
})
Object.getOwnPropertyDescriptor(proxy, 'age')
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'age' that is incompatible with the existing property in the proxy target
deleteProperty
- 参数
target
目标对象property
拦截的目标函数属性, 字符串
- 返回值 必须返回一个布尔值表示操作是否成功, 非布尔会转换成布尔
- 可被拦截的操作
delete proxy.property
delete proxy[property]
Reflect.deleteProperty(proxy, property)
- 陷阱不变量(invariant)
- 如果
target.property
存在且是不可配置的 handler不可删除该属性
- 如果
const target = {name: 'lorry'}
const proxy = new Proxy(target, {
deleteProperty(target, property) {
console.log('deleteProperty', property)
return Reflect.deleteProperty(...arguments)
}
})
delete proxy.name // deleteProperty name
Object.defineProperty(target, 'age', {
value: 18,
configurable: false
})
delete proxy.age // deleteProperty age
console.log(target) // {age: 18}
ownKeys()
- 参数
target
目标对象
- 返回值 必须返回一个包含
string
或symbol
的可枚举对象 - 可被拦截的操作
Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
Object.keys(proxy)
Reflect.ownKeys(proxy)
- 陷阱不变量(invariant)
- 返回的可枚举对象必须包含
target
的所有不可编辑属性 - 如果
target
是不可扩展的, 返回的可枚举对象必须包含target
的属性键(keys)
- 返回的可枚举对象必须包含
const target = {name: 'lorry'}
const proxy = new Proxy(target, {
ownKeys(target) {
console.log('ownKeys')
return Reflect.ownKeys(...arguments)
}
})
Object.keys(proxy) // ownKeys
// ["name"]
getPrototypeOf()
- 参数
target
目标对象
- 返回值 必须返回一个对象或者
null
- 可被拦截的操作
Object.getPrototypeOf(proxy)
Reflect.getPrototypeOf(proxy)
proxy.__proto__
Object.prototype.isPrototypeOf(proxy)
proxy instanceof object
- 陷阱不变量(invariant)
- 如果
target
是不可扩展的, 返回Object.getPrototypeOf(proxy)
的唯一有效值为Object.getPrototypeOf(target)
- 如果
const target = function(name){this.name = name}
target.prototype.getName = function () {return this.name}
const targetIns = new target('lorry')
const proxy = new Proxy(targetIns, {
getPrototypeOf(target) {
console.log('getPrototypeOf')
return Reflect.getPrototypeOf(...arguments)
}
})
Object.getPrototypeOf(proxy) === target.prototype // getPrototypeOf
// true
setPrototypeOf()
- 参数
target
目标对象prototype
: 待替换的原型对象, 如果是顶级原型则可设置为null
- 返回值 必须返回一个布尔
- 可被拦截的操作
Object.setPrototypeOf(proxy, prototype)
Reflect.setPrototypeOf(proxy, prototype)
- 陷阱不变量(invariant)
- 如果
target
是不可扩展的, 可用的原型只能设置为Object.getPrototypeOf(target)
- 如果
const target = {}
const proxy = new Proxy(target, {
setPrototypeOf(target, prototype) {
console.log('setPrototypeOf')
return Reflect.setPrototypeOf(...arguments)
}
})
Object.setPrototypeOf(proxy, Object) // setPrototypeOf
isExtensible
- 参数
target
目标对象
- 返回值 必须返回一个布尔
- 可被拦截的操作
Object.isExtensible(proxy)
Reflect.isExtensible(proxy)
- 陷阱不变量(invariant)
- 如果
target
是不可扩展的, 必须返回false
, 反之必须返回true
- 如果
const target = {}
const proxy = new Proxy(target, {
isExtensible(target) {
console.log('isExtensible')
Reflect.isExtensible(...arguments)
// 返回值为undefined, 会被转为false, 报出下面的错误
}
})
Object.isExtensible(proxy) // isExtensible
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
preventExtensions()
- 参数
target
目标对象
- 返回值 必须返回一个布尔
- 可被拦截的操作
Object.preventExtension(proxy)
Reflect.preventExtension(proxy)
- 陷阱不变量(invariant)
- 如果
target
是不可扩展的, 必须返回true
- 如果
const target = {}
const proxy = new Proxy(target, {
preventExtensions(target) {
console.log('preventExtensions')
Reflect.preventExtensions(...arguments)
// 已经设置为不可扩展了, 必须返回true, false会报下面的错误
return false
}
})
Object.preventExtensions(proxy) // preventExtensions
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned falsish
apply()
- 参数
target
目标对象thisArg
: 函数调用上下文argumentsList
:函数调用参数列表
- 返回值 未严格限制
- 可被拦截的操作
proxy(...argumentsList)
Function.prototype.apply(thisArg, argumentsList)
Function.prototype.call(thisArg, ...argumentsList)
Reflect.apply(proxy, thisArg, argumentsList)
- 陷阱不变量(invariant)
target
必须是函数
const target = function(name) {console.log(name, this.age)}
const proxy = new Proxy(target, {
apply(target, thisArg, argumentsList) {
console.log('apply')
Reflect.apply(...arguments)
}
})
// 挂载到window
var age = 18
proxy('lorry') // apply
// lorry 18
const obj = {age: 24}
Reflect.apply(proxy, obj, ['lorry'])
// lorry 24
construct()
- 参数
target
目标对象argumentsList
传给构造函数的此参数newTarget
最初被调用的构造器
- 返回值 必须返回一个对象
- 可被拦截的操作
new Proxy(...argumentsList)
Reflect.construct(target, argumentsList, newTarget)
- 陷阱不变量(invariant)
target
必须是函数
const target = class {
constructor(name) {
this.name = name
}
}
const proxy = new Proxy(target, {
construct(target, argumentsList, newTarget) {
console.log('construct')
return Reflect.construct(...arguments)
}
})
new proxy('lorry') // construct
// target {name: "lorry"}
代理模式
跟踪属性访问
get
, set
, 和has
这三者的结合使用可以达到跟踪对象属性访问的效果
const user = {name: 'lorry'}
const proxy = new Proxy(user, {
get(target, property, receiver) {
console.log(`Getting ${property}`)
return Reflect.get(...arguments)
},
set(target, property, value, receiver){
console.log(`Setting ${property} to ${value}`)
Reflect.set(...arguments)
}
})
proxy.name // Getting name
proxy.name = 'jiang' // Setting name to jiang
这就可以实现数据的监听了.类似于设置Object.defineProperty({get, set})
. 这也是之前所说Vue3的基础.
隐藏属性
const hiddenProperties = ['age']
const target = {
name: 'lorry',
age: 18
}
const proxy = new Proxy(target, {
get(target, property, receiver) {
if(hiddenProperties.includes(property)) {
return undefined
}
return Reflect.get(...arguments)
},
has(target, property) {
if(hiddenProperties.includes(property)) {
return false
}
return Reflect.has(arguments)
}
})
proxy.age // undefined
// 但是打印proxy会显式name
console,log(proxy) // Proxy {name: "lorry", age: 18}
属性验证
const target = {
onlyNumbers: 0
}
const proxy = new Proxy(target, {
set(target, property, value) {
if(property === 'onlyNumbers' && isNaN(value)) {
return false
}
return Reflect.set(...arguments)
}
})
proxy.onlyNumbers = 'aaa' // 不会报错
console.log(proxy.onlyNumbers) // 0
函数和构造器参数验证
函数参数验证
// 求中位数
function median(...nums) {
return nums.sort()[nums.length >> 1]
}
const proxy = new Proxy(median, {
apply(target, thisArg, argsList) {
if (argsList.some(arg => isNaN(arg))) {
return Error('请输入数字')
}
return Reflect.apply(...arguments)
}
})
proxy(1,2,3,4,5) // 3
proxy(1,'a',3) // Error: 请输入数字
构造器参数验证
const target = function(age) {this.age = age}
const proxy = new Proxy(target, {
construct(target, argsList, newTarget) {
// 注意与isNaN的区别
if(argsList.some(arg => typeof(arg) !== 'number')){
return Error('请输入数字')
}
console.log(newTarget)
return Reflect.construct(...arguments)
}
})
new proxy(18) // {age: 18}
new proxy('18') // Error: 请输入数字
数据绑定和监听
一个代理的类可以监听每一次的实例化, 并将其添加到一个全局的包含该类实例的集合中
const userList = []
class User {
constructor(name) {
this.name = name
}
}
const proxy = new Proxy(User, {
construct(target, argsList, newTarget) {
const newUser = Reflect.construct(...arguments)
userList.push(newUser)
return newUser
}
})
new proxy('foo')
new proxy('bar')
console.log(userList) // [User, User]
一个集合也可以绑定一个emitter
, 每当有实例被加进这个集合时触发
const userList = []
function emit(newValue) {
console.log(newValue)
}
const proxy = new Proxy(userList,{
set(target, property, value, reciever) {
const result = Reflect.set(...arguments)
if(result) {
emit(Reflect.get(target, property, reciever))
}
return result
}
})
proxy.push('lorry')
// lorry
// 1
proxy.push('jiang')
// jiang
// 2
// 由上可以看出, 先设置了索引, 再设置了length
实际使用
回到开篇提出的问题, 如何设计使得更符合RPC的调用?
class Hardware {
writeCode(code) {
// 负责发送jsonRPC数据
console.log(code)
}
proxyWrite(method) {
return (code = '') => this.writeCode(method+code)
}
}
const hardware = new Hardware()
const hardwareProxy = new Proxy(hardware, {
get(target, property, receiver) {
if (!target[property]) {
return receiver.proxyWrite(property)
}
return Reflect.get(...arguments)
}
})
hardwareProxy.setHome() // setHome
hardwareProxy.setPTPCmd(JSON.stringify({x: 1, y:2})) // setPTPCmd{"x":1,"y":2}
这样就完全抽象出来了, 是不是比在类中写一个个具体的实现elegant得多呢? :-)
总结
代理是ECMAScript6中的一个非常令人激动也是一个动态的新增功能, 尽管它不支持向后兼容, 但是它打开了一个全新的前所未有的元编程和抽象性
在高层一上, 代理是一个真实js对象的透明可视化. 当一个代理创建的时候, 可以有能力定义包含各种陷阱的handlers, 这可以劫持几乎所有的基本js操作和方法(但是要满足陷阱不可变性(invariant))
跟代理一起出现的还有ReflectAPI, 它提供了每个陷阱行为的一毛一样的封装, 可以被视作基本操作的集合, 这些基本操作是几乎所有js对象APIs的基石
代理的使用无限的想象空间, 这里只是一些最基本操作的示例. 有了它, 就可以让我们开发者更elegent的实现一些代理模式, 比如包括但不限于上述的
- 跟踪属性访问
- 隐藏属性
- 阻止修改或删除属性
- 函数参数的验证
- 构造器函数的验证
- 数据绑定
- 观察者
以上便是我对proxy的理解, 有任何想法请与我留言交流
参考资料: