【问题标题】:Why is Proxy to a Map object in ES2015 not working为什么 ES2015 中映射对象的代理不起作用
【发布时间】:2017-08-31 08:54:46
【问题描述】:

我正在通过 Google Chrome 版本 57.0.2987.133 运行以下脚本:

var loggingProxyHandler = {
    "get" : function(targetObj, propName, receiverProxy) {
        let ret = Reflect.get(targetObj, propName, receiverProxy);
        console.log("get("+propName.toString()+"="+ret+")");
        return ret;
     },

     "set" : function(targetObj, propName, propValue, receiverProxy) {
         console.log("set("+propName.toString()+"="+propValue+")");
         return Reflect.set(targetObj, propName, propValue, receiverProxy);
     }
};

function onRunTest()
{
    let m1 = new Map();
    let p1 = new Proxy(m1, loggingProxyHandler);
    p1.set("a", "aval");   // Exception thrown from here
}

onRunTest();
NOTE: Requires a browser supporting ES2015's Proxy

运行时,我看到处理程序的 get 陷阱被调用以返回 Map 的 set 函数 然后我收到以下错误:

"Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]"
at Proxy.set (native)
...

我尝试从 loggingProxyHandler 中删除陷阱函数(使其成为空对象),但仍然收到相同的错误。

我的理解是应该能够为所有原生 ES5 和 ES2015 javascript 对象生成代理对象。 Array 似乎在同一个代理处理程序下运行良好。 我误解了规格吗?
我的代码缺少什么吗? Chrome 中是否存在已知错误? (我进行了搜索,发现 Chrome 在这个主题上没有任何缺陷。)

【问题讨论】:

  • 听起来您实际上想要做的是覆盖(拦截)setget 调用,而不是通过代理路由所有属性访问
  • 明确一点:不要使用Proxy 拦截异常行为,这超出了正常的Object 语义。改用子类化。
  • 为了更清楚地说明:在某些情况下您可能需要最终代理地图,尤其是当您不拥有原始对象图但希望观察它的任何更改时。

标签: javascript google-chrome dictionary proxy


【解决方案1】:

您收到错误的原因是代理没有参与p1.set() 方法调用(除了get 陷阱用于检索函数引用)。因此,一旦检索到函数引用,就会在 this 设置为代理 p1 的情况下调用它,而不是映射 m1 - 这是 Mapset 方法不喜欢的。

如果您真的想拦截Map 上的所有属性访问调用,您可以通过绑定从get 返回的任何函数引用来修复它(参见*** 行):

const loggingProxyHandler = {
    get(target, name/*, receiver*/) {
        let ret = Reflect.get(target, name);
        console.log(`get(${name}=${ret})`);
        if (typeof ret === "function") {    // ***
          ret = ret.bind(target);           // ***
        }                                   // ***
        return ret;
     },

     set(target, name, value/*, receiver*/) {
         console.log(`set(${name}=${value})`);
         return Reflect.set(target, name, value);
     }
};

function onRunTest() {
    const m1 = new Map();
    const p1 = new Proxy(m1, loggingProxyHandler);
    p1.set("a", "aval");
    console.log(p1.get("a")); // "aval"
    console.log(p1.size);     // 1
}

onRunTest();
NOTE: Requires a browser supporting ES2015's Proxy

请注意,当调用 Reflect.getReflect.set 时,我们不会传递接收者(事实上,我们根本没有使用 receiver 参数,所以我已经将参数注释掉了)。这意味着他们将使用目标本身作为接收者,如果属性是访问器(如Mapsize 属性)并且他们需要他们的this 作为实际实例(如Mapsize 确实如此)。


如果您的目标只是拦截Map#getMap#set,那么您根本不需要代理。要么:

  1. 创建一个Map 子类并实例化它。不过,假设您控制 Map 实例的创建。

  2. 创建一个继承自Map实例的新对象,并覆盖getset;您不必控制原始 Map 的创建。

  3. 用您自己的版本替换Map 实例上的setget 方法。

这里是#1:

class MyMap extends Map {
  set(...args) {
    console.log("set called");
    return super.set(...args);
  }
  get(...args) {
    console.log("get called");
    return super.get(...args);
  }
}

const m1 = new MyMap();
m1.set("a", "aval");
console.log(m1.get("a"));

#2:

const m1 = new Map();
const p1 = Object.create(m1, {
  set: {
    value: function(...args) {
      console.log("set called");
      return m1.set(...args);
    }
  },
  get: {
    value: function(...args) {
      console.log("get called");
      return m1.get(...args);
    }
  }
});

p1.set("a", "aval");
console.log(p1.get("a"));

#3:

const m1 = new Map();
const m1set = m1.set; // Yes, we know these are `Map.prototype.set` and
const m1get = m1.get; // `get`, but in the generic case, we don't necessarily
m1.set = function(...args) {
  console.log("set called");
  return m1set.apply(m1, args);
};
m1.get = function(...args) {
  console.log("get called");
  return m1get.apply(m1, args);
}

m1.set("a", "aval");
console.log(m1.get("a"));

【讨论】:

  • 如果我想在这里实现某种程度的 AOP,我似乎需要使用子类机制。但是访问 Map 的 size 属性是有问题的,因为它不是通过方法调用来实现的。当我添加推荐的绑定时,它确实绕过了异常,但是一旦函数通过 get 陷阱绑定到 targetObj,将不会调用 set 陷阱,从而有效地使 Proxy 无效。我想如果我想追求代理,我可以尝试生成一个全新的函数,绑定到 targetObject 但调用代理处理程序陷阱(通​​过闭包)。
  • @Rand:是的。没有理由调用set 陷阱,没有什么是设置 属性。我不明白为什么访问size 是有问题的:属性是继承的,只需访问它。
  • @T.J.Crowder 你说“我不明白为什么访问大小是有问题的:属性是继承的,只需访问它。”我也不明白为什么,但确实如此。在 Chrome 59 上,您的第一个示例有效,但如果我添加 console.log(p1.size),它会给出 Uncaught TypeError: Method Map.prototype.size called on incompatible receiver [object Object]。有任何想法吗?您的示例是我见过的最接近工作的地图代理。
  • 嗯,显然我可以通过在第一个示例中调用 Reflect.get() 之前插入 if (name === 'size') { return targetObj.size; } 来使代理完全正常工作。不知道为什么,但这很棒!我认为它会解决我正在解决的另一个问题:stackoverflow.com/questions/43801605
  • @DonHatch 这似乎表明.size 属性是getter,它的作用类似于this 的函数。如果我没记错的话,另一种方法是从描述符中获取getter,调用它:Object.getOwnPropertyDescriptor(target, 'size').get.call(target),这应该可以解决问题,这实际上与target.size 相同。 :)
【解决方案2】:

让我添加更多内容。

许多内置对象,例如MapSetDatePromise 等都使用所谓的内部插槽

这些类似于属性,但保留用于内部、仅用于规范目的。例如,Map 将项目存储在内部插槽 [[MapData]] 中。内置方法直接访问它们,而不是通过[[Get]]/[[Set]] 内部方法。所以Proxy 无法拦截。

例如:

let map = new Map();

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

proxy.set('name', 'Pravin'); // Error

在内部,Map 将所有数据存储在其[[MapData]] 内部插槽中。 proxy 没有这样的插槽。内置方法Map.prototype.set 方法尝试访问内部属性this.[[MapData]],但是因为this=proxy,在代理中找不到它,就失败了。

有办法解决它:

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('name','Pravin');
console.log(proxy.get('name')); //Pravin (works!)

现在它可以正常工作了,因为get trap 将函数属性(例如 map.set)绑定到目标对象(地图)本身。所以proxy.set(...)里面的this的值不会是proxy,而是原来的map。所以当set的内部实现尝试访问this.[[MapData]]内部槽时,就成功了。

【讨论】:

  • 感谢您的回答。你如何判断一个变量是否有这些内部槽之一?
  • @Andrew ECMAScript 引擎中的每个对象都与一组定义其运行时行为的内部方法相关联。这些内部方法不是 ECMAScript 语言的一部分。它们由本规范定义,纯粹是为了说明目的。但是,ECMAScript 实现中的每个对象都必须按照与其关联的内部方法所指定的行为。具体实现方式由实现决定。
  • 内部槽对应于与对象关联并被各种 ECMAScript 规范算法使用的内部状态。内部插槽不是对象属性,它们不会被继承。根据特定的内部槽规范,这种状态可能由任何 ECMAScript 语言类型的值或特定的 ECMAScript 规范类型值组成。
  • “内部插槽”是一个实现细节。它们不是对象属性。 JavaScript 程序员不需要了解内部插槽,但对于解释一些 JavaScript 行为非常有用。希望这会有所帮助。
猜你喜欢
  • 2016-09-08
  • 2023-03-25
  • 1970-01-01
  • 2023-04-05
  • 2022-01-03
  • 1970-01-01
  • 2015-12-19
  • 1970-01-01
相关资源
最近更新 更多