关于ES6的Proxy详解

之前在使用 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 的方法, 他提供了解除这种代理关系的能力. 但是这种关系的解除是不可逆的, 不要想着离婚了还能复婚. 而且跟 promiserejectresolve 一样, 都只能有效调用一次, 之后的调用都是无效的(但不会报错). 在撤销了之后再调用代理的方法, 就会抛出一个 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 之后, 记住

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 使用 inwith
  • 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 保持引用, thisuser , 但是如果代理去获取, 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]] 的内部槽, 因为代理对象没有这个私有槽,并且也不能通过 getset 的方法访问到这个内部槽(否则就可以通过拦截或重定向到目标对象中实现, 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 设置 writableconfigurable 均为 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
    target.property
    target.property
    target.property
    target.property
    
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 目标对象
  • 返回值 必须返回一个包含 stringsymbol 的可枚举对象
  • 可被拦截的操作
    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))

跟代理一起出现的还有 Reflect API, 它提供了每个陷阱行为的一毛一样的封装, 可以被视作基本操作的集合, 这些基本操作是几乎所有js对象APIs的基石

代理的使用无限的想象空间, 这里只是一些最基本操作的示例. 有了它, 就可以让我们开发者更elegent的实现一些代理模式, 比如包括但不限于上述的

  • 跟踪属性访问
  • 隐藏属性
  • 阻止修改或删除属性
  • 函数参数的验证
  • 构造器函数的验证
  • 数据绑定
  • 观察者

以上便是我对proxy的理解, 有任何想法请与我留言交流

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章