JavaScript的Proxy怎代理Map?

@张豪杰 的答案已经比较完善了,我就做一点宏观上的补充。

ES6的Proxy这个特性的设计目标是代理一个对象的所有基本语义,以允许构建membrane(膜)。

这里有几个关键词。

第一,被代理的目标(target)必须是对象(object),非对象(即primitive value)是不行的。(注意这一点其实会对未来JS的新特性扩展有影响,比如新的tuple/record提案如果设计为不是对象,就不能被proxy所代理。)

第二,对象语义。最常见的就是Get/Set,也就是 o.x 和 o.x = ... 。再如Apply 和 Construct,也就是o(...args) 和new o(...args)。 其他还有像定义属性、删除属性等。

第三,基本。有一些看上去是基本语义的,实际上是由多个基本语义复合而成,比如o.method(...args),在JS里是由f = Get(o, method)和Apply(f, o, args)复合而成。再如k in o,对应的是基本语义Has,但是for k in o却是由OwnKeys、GetPrototypeOf、 GetOwnPropertyDescriptor复合而成。

第四,所有。 「所有对象基本语义」即构成对象模型,Proxy可以完全地(基于被代理目标)实现对象的所有基本语义,使用者是无法区分一个对象是普通的对象还是proxy的。为此,每个基本语义在 proxy handler 上都有对应的 trap,并且在Reflect对象上也有对应的方法以方便实现默认行为(故而Proxy和Reflect是对称的)。注意,Proxy/Reflect覆盖所有基本语义,但不管那些复合语义。

第五,membrane。这是一个相当复杂的概念,简单说就是不仅代理一个对象,而且代理由该对象关联的整个对象图,具体这里不展开了。membrane是基于proxy构建的,并且有invariant性质称作membrane透明性,所有新的JS提案都必须保持这性质(对于几乎所有涉及对象模型的提案来说有非常重大的挑战)。

然后我们看一下 internal slot(内部槽)。内部槽并不是对象模型的一部分,也当然没有对应的Proxy trap。实际上,既然叫内部槽,从外部就是完全无感知的。也就是说,你拿到一个对象,理论上说你根本无从判断这个对象有没有内部槽。有没有内部槽,是否存取内部槽,纯粹是对象的内部实现细节,隐藏于对象相关方法(包括构造器和访问器)的实现中。

比如对于一个Map对象,理论上说,和一般对象(比如你在userland实现的)并没有区别。map.get(test) 从外部看,只是执行了两个基本语义:map_get = Get(map, get)、Apply(map_get, map, [test])。落实到具体的Map对象上,第一步默认情况下是获得Map.prototype.get方法, 第二步默认相当于执行Map.prototype.get.apply(map, [test])。

假如map是一个proxy,则你可以捕捉这两个操作,但显然和内部槽没有关系。真正调用内部槽的是Map.prototype.get方法的内部实现,它会存取发送给它的this参数上的[[MapData]]内部槽(因此,假若你传入的this参数不是Map的实例对象从而没有该内部槽,就会扔TypeError)。 存取内部槽总是一个内部实现细节,从对象的使用者角度说,是无从知晓的。

以上。

PS. 当然,虽说理论上内部槽是不可观测的,但实践上我们可以近似判断 —— 一个存取了内部槽的方法是不能简单地代理调用的。假设有对象o,let p = new Proxy(o, {}),对于普通对象,p.method()和o.method()结果通常是一致的,此即所谓代理透明性。 但如果method访问了o的内部槽,由于p(代理对象)并没有o的那些内部槽 ,故而会抛TypeError。注意,我们只能猜测o有内部槽而不可能严格断言,因为方法可能由于任何原因抛TypeError,或者访问内部槽的代码可以通过测试是否具有内部槽或catch掉error来不抛TypeError。

PPS. 目前(ES2020)为止,JS没有提供直接的实现类似内部槽语义的语法——尽管你可以用WeakMap来实现类似的语义,但除了极少数领域(如polyfill或如jsdom那样模拟host对象),一般开发者并不会那样写——所以可以说99.9%的userland代码是没有内部槽的,也就是说99%的userland对象是具有代理透明性的(扣掉另外0.9%是少数依赖类似的this同一性的代码),而代理透明性也是Vue3、Mobx等框架可以运作的前提。然而目前stage 3的private class fields提案提供类内部槽语义,因此任何一个使用了private fields的对象都丧失了代理透明性,从而无法和Vue3、Mobx等框架良好协作。一个方便的语法提供了一个看似原本没有的能力,听上去是件好事,然而绝大部分开发者并不能预期到其带来的副作用,而且这个副作用很可能是违背他们最终预期和利益的,这样,这个语法越方便(比如很容易从public field切换到private field),其对生态的破坏性反而就越大。这是我反对该提案的重要理由之一。

Proxy 的 get 捕获器确实捕获的是内部方法 [[Get]] 的执行,但是,这里的内部方法 [[Get]] 也确确实实是执行了的

如果不知道什么是内部方法、内部槽,请参见我之前的文章 ECMAScript 阅读指南(二)。

举个例子,有如下原始对象 obj 和它的代理对象 objProxy(代码段-1)。

const obj = { print() { console.log("执行 print 方法"); }, get() { console.log("执行 get 方法"); } } const objProxy = new Proxy(obj, { // 捕获内部方法 [[Get]] 过程 get(target, prop, rec) { console.log(`Proxy 捕获了对象的 ${prop} 属性`); return Reflect.get(...arguments); } }) // 通过代理对象调用原始对象的方法 objProxy.print(); objProxy.get();

在代码执行后,会打印出:

为什么会出现这个效果?

原理是,当执行代码 objProxy.print() 的时候,JS 引擎先会去尝试获取 objProxy 对象的 print 属性,然后判断 print 是不是一个函数对象。如果它不是函数对象,则抛出类型错误(TypeError);如果是函数对象,则调用这个函数对象。(注意此函数对象调用时,它的内部 this 默认为 objProxy,因为是 objProxy 在调用它。)

同理,objProxy.get() 也是相似的逻辑,只是这次是在访问、调用 objProxy 对象的 get 方法。

也就是说,你要调用 print 方法,首先你得获取 print 属性,这一获取过程,在 ES 规范中就是使用的内部方法 [[Get]]

当然这个论断不是随便说的,一切得立足于规范。

函数的调用由 ES 规范的 7.3.13 Call ( F, V [ , argumentsList ] )来定义。此处的 F 就是要调用的函数:

我们看一下这个 F 是如何决定的。我们找一个使用 Call 操作的场景,比如 7.3.20 Invoke ( V, P [ , argumentsList ] ):

我们看到它在用 Call 操作去执行 func 函数,而 func 则是由上一步的 GetV(V, P) 操作返回生成的。

我们再看一下 7.3.3 GetV ( V, P ) 的具体情况:

看,第三步它使用了 O.[[Get]](P, V) 这个内部方法来获取它要的属性,并返回出去,也就形成了在 Invoke 那儿要调用的 func。

由此可知,当我们通过代理对象 objProxy 调用原始对象的任何方法时,捕获器 get 始终是会工作的。因为你永远得先获取它,然后才能调用它。

至于后续的一些小细节,代理对象执行方法时,this 的绑定问题,理解起来应该啥难度,这里就不赘述了。

路过……

讲道理……

俺真没看懂你的问题……

俺猜测你应该压根儿就理解错了……乱成一锅粥那种的……

好好看看规范……

明明白白写着呢,proxy 的 get 等等方法是为了增强所对应的内部方法 [[xxx]] 的实现。

所以你继续看,比如你说的 [[Get]],写着只要是取 proxy 的属性值就会调用这个方法。

这些步骤里 handle 是啥,是 proxy 对象自身,然后拿 proxy 对象的方法 get ,这 get 是啥,就是 Proxy 的 get 方法,也就是你 get(target, prop, receiver) {....} 这个啊。没见后面就 Call 它并传入了 target p receiver 三个参数么,并获取结果么。

摆明是 ProxyHandle [[Get]] 操作的一部分用来增强 [[Get]] 功能。

最后返回的是get(target, prop, receiver) {....}调用的返回值,这个值给啥都行。

你这例子里,proxy.get 是 proxy 的属性值获取,就跑了 ProxyHandle [[Get]],得到原始 map 对象和 属性名 “get”,运行中跑 get(target, prop, receiver) {....}传入被代理对象 map 和属性名 “get”。

调用的返回值是 Reflect.get 方法拿到的 map 对象的 get 方法 。

别忘了你实际跑的是 proxy.get(...),这括号是是个函数调用呢,所以就执行了 map.get 方法调用呗……

这玩意跟你问的 Map [[MapData]] 和 Map 不通过 [[Get]]/[[Set]] 有啥关系么……

=================== 追加 ==============

刚才玩完了游戏,又扫听一眼其它回答和评论,才大概知道你问的是啥……

实际上你问的应该是为啥

let map = new Map(); let proxy = new Proxy(map, { get(target, prop, receiver) { return target[prop]; } }); proxy.set(test, 1) proxy.get(test);

或者

let map = new Map(); let proxy = new Proxy(map, {}); proxy.set(test, 1); proxy.get(test);

这种,proxy map 后 get 会报错,而为啥你的写法不会报错吧……

实际上

let map = new Map(); let proxy = new Proxy(map, { get(target, prop, receiver) { return target[prop].bind(target); } }); proxy.set(test, 1); proxy.get(test);

这样写也是没问题的。

为啥.bind(target) 下就行了,还是看规范……

Let trap be ? GetMethod(handler, "get"). If trap is , then Return ? target.[[Get]](P, Receiver). Let trapResult be ? Call(trap, handler, « target, P, Receiver »).

proxy [[Get]] 返回的 trapResult ,在没有传入增强的 get 方法时,返回对象属性值,在有传入时返回的是 Call get 后 return 的结果。这些例子里 proxy 有 get 方法定义时,返回的是 map.prototype.get/set 方法。再来看看 Map 里这俩哥们的调用规则:

23.1.3.6 Map.prototype.get ( key ) The following steps are taken: Let M be the this value. If Type(M) is not Object, throw a TypeError exception. If M does not have a [[MapData]] internal slot, throw a TypeError exception. Let entries be the List that is M.[[MapData]]. For each Record { [[Key]], [[Value]] } p that is an element of entries, do If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return p.[[Value]]. Return . 23.1.3.9 Map.prototype.set ( key, value ) The following steps are taken: Let M be the this value. If Type(M) is not Object, throw a TypeError exception. If M does not have a [[MapData]] internal slot, throw a TypeError exception. Let entries be the List that is M.[[MapData]]. For each Record { [[Key]], [[Value]] } p that is an element of entries, do If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, then Set p.[[Value]] to value. Return M. If key is -0, set key to +0. Let p be the Record { [[Key]]: key, [[Value]]: value }. Append p as the last element of entries. Return M.

它的调用是与 this 相关的,this 对象必须有 [[MapData]] 的实现才可以,否则抛错。

所以,你看,代码里bind target 的,都没问题。没做这步的都有问题。

明摆着,没 bind 这操作,this 是 proxy 对象,谁叫他在点前头呢…… 可 proxy 没[[MapData]] 这个内部实现啊(当然如果这玩意可以暴露覆盖的话,前端场景兴许能骗过去,不过实际上得在实现层改才有可能骗过去。回头看看 v8 里好不好给加一个假的,编译后骗下试试…… 追加:不用试了, v8 里至少现在是通过 ThrowIfNotInstanceType(context, receiver, JS_MAP_TYPE, "Map.prototype.set"); 来判断的,也就是必须是 Map 类型实例才行, 并不去查 [[MapData]] 有没有,因此没的骗。其他类型大致看了下,没看全,大致上也是分辨实例类型为主)

于是,proxy get 增强实现里不 bind 的, 实际调用中 call apply bind 着玩也是一样的……

proxy.set.call(map, test, 2); proxy.get.call(map, test);

同理,Map 其他方法运行规则中标着 “If M does not have a [[MapData]] internal slot, throw a TypeError exception. ”的也得这么玩儿……

前言

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

语法

var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中:

new Proxy() 表示生成一个 Proxy 实例target 参数表示所要拦截的目标对象handler 参数也是一个对象,用来定制拦截行为

如果handler没有设置任何拦截,那就等同于直接通向原对象。

let target = {}; let handler = {}; let proxy = new Proxy(target,handler); proxy.name = 编程三昧; console.log(target.name); // 编程三昧

还有一个技巧是将 Proxy 对象,设置到 object.proxy 属性,从而在 object 对象上调用:

let proxy = new Proxy({}, { get: function(target, property) { return 35; } }); let obj = Object.create(proxy); obj.time // 35

get()

get() 方法用于拦截某个属性的读取操作,可以接受三个参数,依次为:

目标对象属性名proxy 实例本身(严格地说,是操作行为所针对的对象),可选。

get() 方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子:

letperosn = { name:james, age:26, profession:software } var proxy = new Proxy(perosn,{ get:function(target,property) { if (property in target) { return target[property]; } else { throw new ReferenceError("propertype\""+property + "\" does no exit" ); } } }); console.log(proxy.name,proxy.profession); // james software console.log(proxy.sex); // Uncaught ReferenceError: propertype"sex" does no exit

set()

set() 方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为:

目标对象属性名属性值Proxy 实例本身,可选。

假定 person 对象有一个 age 属性,该属性应该是一个不大于 200 的整数,那么可以使用 Proxy 保证 age 的属性值符合要求。

let perosn = { name:james, age:26, profession:software } let proxy = new Proxy(perosn,{ get:function(target,property) { if (property in target) { return target[property]; } else { throw new ReferenceError("propertype\""+property + "\" does no exit" ); } }, set:function(target,key,value) { if(key === age) { if(value>80) { throw ReferenceError("invail"); } else { return target[key] = value; } } else { return target[key]; } } }); proxy.age = 60; console.log(proxy.name,proxy.profession,proxy.age); // james software 60 proxy.age = 99; // Uncaught ReferenceError: invail

apply()

apply() 方法拦截:

函数的调用call 操作apply 操作

apply() 方法可以接受三个参数,分别是:

目标对象目标对象的上下文对象(this)目标对象的参数数组。let twice = { apply(target, ctx, agrs) { return Reflect.apply(...arguments) * 2; } }; function sum (a, b) { return a + b; } let proxy5 = new Proxy(sum, twice); console.log(proxy5(1, 3));// 8 console.log(proxy5.apply(null, [1, 3])); // 8

另外,直接调用Reflect.apply方法,也会被拦截。

Reflect.apply(proxy5, null, [9, 10]) // 38

has()

has() 方法用来拦截 HasProperty 操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是 in 运算符。

has() 方法可以接受两个参数,分别是目标对象、需查询的属性名。

下面的例子使用 has 方法隐藏某些属性,不被 in 运算符发现。

let stu1 = {name: 张三, score: 59}; let stu2 = {name: 李四, score: 99}; let handler = { has(target, prop) { if (prop === score && target[prop] < 60) { console.log(`${target.name} 不及格`); return false; } return prop in target; } } let oproxy1 = new Proxy(stu1, handler); let oproxy2 = new Proxy(stu2, handler); score in oproxy1 // 张三 不及格 // false score in oproxy2 // true for (let a in oproxy1) { console.log(oproxy1[a]); } // 张三 // 59 for (let b in oproxy2) { console.log(oproxy2[b]); } // 李四 // 99

上面代码中,has 拦截只对 in 运算符生效,对 for...in 循环不生效,导致不符合要求的属性没有被 for...in 循环所排除。

Proxy 支持的拦截操作一览

Proxy 支持的拦截操作基本有 13 种。

get(target, propKey, receiver)

拦截对象属性的读取,比如:

proxy.fooproxy[foo]。

set(target, propKey, value, receiver)

拦截对象属性的设置,比如 proxy.foo = v 或 proxy[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)。

~

~ 本文完,感谢阅读!

~

学习有趣的知识,结识有趣的朋友,塑造有趣的灵魂!大家好,我是〖编程三昧〗的作者 隐逸王,我的是『编程三昧』,欢迎关注,希望大家多多指教!

原因

@张豪杰 @贺师俊 已经解释了

实现可以参考下面的代码,想做到真正的响应式目前必须得特殊处理,比如 set 某个 key 触发 get 某个 key 的 function 重新执行

const proxyHandler: ProxyHandler<object> = { get: bind(_this.collectionProxyHandler, _this), }; const proxy = new Proxy(raw, proxyHandler); private collectionProxyHandler(target: Collection, key: string) { const handlers = this.getCollectionHandlerMap(target, key); const targetObj = key in target && handlers[key] ? handlers : target; return Reflect.get(targetObj, key); } private getCollectionHandlerMap = (target: Collection, proxyKey: string) => { return { get size() { const proto = Reflect.getPrototypeOf(target); depCollector.collect(target, ESpecialReservedKey.ITERATE); return Reflect.get(proto, proxyKey, target); }, get: (key: any) => { const { get } = Reflect.getPrototypeOf(target) as MapType; depCollector.collect(target, key); return get.call(target, key); }, has: (key: any) => { const { has } = Reflect.getPrototypeOf(target) as NormalCollection; depCollector.collect(target, key); return has.call(target, key); }, forEach: (callbackfn: (value: any, key: any, map: Map<any, any>) => void) => { const { forEach } = Reflect.getPrototypeOf(target) as NormalCollection; depCollector.collect(target, ESpecialReservedKey.ITERATE); return forEach.call(target, callbackfn); }, values: () => { const { values } = Reflect.getPrototypeOf(target) as NormalCollection; depCollector.collect(target, ESpecialReservedKey.ITERATE); return values.call(target); }, keys: () => { const { keys } = Reflect.getPrototypeOf(target) as NormalCollection; depCollector.collect(target, ESpecialReservedKey.ITERATE); return keys.call(target); }, entries: () => { const { entries } = Reflect.getPrototypeOf(target) as NormalCollection; depCollector.collect(target, ESpecialReservedKey.ITERATE); return entries.call(target); }, [Symbol.iterator]: () => { if (target.constructor === Set) { return this.getCollectionHandlerMap(target, values).values(); } if (target.constructor === Map) { return this.getCollectionHandlerMap(target, entries).entries(); } }, add: (value: any) => { const { add, has } = Reflect.getPrototypeOf(target) as SetType; const rootKey = rootKeyCache.get(target)!; const hadValue = has.call(target, value); if (!hadValue) { triggerCollector.trigger(target, value, { type: ECollectType.SET_ADD, beforeUpdate: , didUpdate: value, }, this.reactorConfigMap[rootKey].isNeedRecord); triggerCollector.trigger(target, ESpecialReservedKey.ITERATE, { type: ECollectType.SET_ADD, }, this.reactorConfigMap[rootKey].isNeedRecord); } return add.call(target, value); }, set: (key: any, value: any) => { const { set, get, has } = Reflect.getPrototypeOf(target) as MapType; const rootKey = rootKeyCache.get(target)!; const hadKey = has.call(target, key); const oldValue = get.call(target, key); if (value !== oldValue) { triggerCollector.trigger(target, key, { type: ECollectType.MAP_SET, beforeUpdate: oldValue, didUpdate: value, }, this.reactorConfigMap[rootKey].isNeedRecord); } if (!hadKey) { triggerCollector.trigger(target, ESpecialReservedKey.ITERATE, { type: ECollectType.MAP_SET, }, this.reactorConfigMap[rootKey].isNeedRecord); } return set.call(target, key, value); }, delete: (key: any) => { const proto = Reflect.getPrototypeOf(target) as Collection; const rootKey = rootKeyCache.get(target)!; const hadKey = proto.has.call(target, key); if (!hadKey) { return proto.delete.call(target, key); } if (proto.constructor === Map || proto.constructor === WeakMap) { const oldValue = proto.get.call(target, key); triggerCollector.trigger(target, key, { type: ECollectType.MAP_DELETE, beforeUpdate: oldValue, }, this.reactorConfigMap[rootKey].isNeedRecord); triggerCollector.trigger(target, ESpecialReservedKey.ITERATE, { type: ECollectType.MAP_DELETE, }, this.reactorConfigMap[rootKey].isNeedRecord); } if (proto.constructor === Set || proto.constructor === WeakSet) { triggerCollector.trigger(target, key, { type: ECollectType.SET_DELETE, beforeUpdate: key, }, this.reactorConfigMap[rootKey].isNeedRecord); triggerCollector.trigger(target, ESpecialReservedKey.ITERATE, { type: ECollectType.SET_DELETE, }, this.reactorConfigMap[rootKey].isNeedRecord); } return proto.delete.call(target, key); }, clear: () => { const { clear, forEach } = Reflect.getPrototypeOf(target) as NormalCollection; forEach.call(target, (value: any, key: any) => { this.getCollectionHandlerMap(target, key).delete(key); }); return clear.call(target); }, } }