关于ES6的Proxy详解

3,750 阅读21分钟

概述

之前在使用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中每个可以被拦截的方法都会有对应的 ReflectAPI, 该对象的函数签名以及方法名都跟被拦截的原始行为一毛一样, 因此可以通过下例来实现透传代理

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之后, 记住

  1. Reflect API 不仅仅只能在陷阱处理函数中使用
  2. 大多数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 使用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存在, 并且是不可配置的, 那么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 目标对象
  • 返回值 必须返回一个包含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))

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

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

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

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

参考资料:

  1. MDN proxy
  2. MDN handler
  3. MDN Reflect