【问题标题】:Nested ES6 Proxies not working as expected嵌套的 ES6 代理无法按预期工作
【发布时间】:2016-06-03 08:52:47
【问题描述】:

我正在开发一个 API 客户端,它允许在提供 foo 的 ID 时调用特定的 API 方法,如下所示:

apiClient.myApiMethod('myFooId', 'firstApiArg', 'nthApiArg');

为了方便开发者,我正在尝试实现自定义代理对象:

var myFoo = apiClient.registerFoo('myFoo', 'myFooId');
myFoo.myApiMethod('firstApiArg', 'nthApiArg');

经过一段时间的搜索,我认为 ES6 代理可能最适合这种情况,因为需要将 fooId 作为方法调用的第一个参数插入以支持两种工作方式。
因此,我创建了以下代码。如果调用Foo.myFoos 的对象属性(例如Foo.myFoos.example),则在_myFooItems 中搜索它,如果存在,则返回另一个代理对象。
现在,如果在 that 对象上调用了一个方法,则会在 Foo 的属性中搜索它,如果找到,则以 myFooId 作为其第一个参数调用 Foo 方法。 也就是说,你应该可以Foo.myFoos.example.parentMethodX('bar', 'baz')

var Foo = function() {

  // parent instance
  _self = this;

  // custom elements dictionary
  _myFooItems = {};

  // to call parent methods directly on custom elements
  this.myFoos = Object.create(new Proxy({}, {

      // property getter function (proxy target and called property name as params)
      get: function(target, myFooName) {

        // whether called property is a registered foo
        if (_myFooItems.hasOwnProperty(myFooName)) {

          // create another proxy to intercept method calls on previous one
          return Object.create(new Proxy({}, {

              // property getter function (proxy target and called property name as params)
              get: function(target, methodName) {

                // whether parent method exists
                if (_self.hasOwnProperty(methodName)) {

                  return function(/* arguments */) {

                    // insert custom element ID into args array
                    var args = Array.prototype.slice.call(arguments);
                    args.unshift(_myFooItems[ myFooName ]);

                    // apply parent method with modified args array
                    return _self[ methodName ].apply(_self, args);
                  };
                } else {

                  // parent method does not exist
                  return function() {
                    throw new Error('The method ' + methodName + ' is not implemented.');
                  }
                }
              }
            }
          ));
        }
      }
    }
  ));


  // register a custom foo and its ID
  this.registerFoo = function(myFooName, id) {

    // whether the foo has already been registered
    if (_myFooItems.hasOwnProperty(myFooName)) {
      throw new Error('The Foo ' + myFooName + ' is already registered in this instance.');
    }

    // register the foo
    _myFooItems[ myFooName ] = id;

    // return the created foo for further use
    return this.myFoos[ myFooName ];
  };
};

module.exports = Foo;

虽然如果您运行代码并尝试注册 foo(上面的代码在 Node>=6.2.0 中工作)会发生什么情况,但会引发以下错误:

> var exampleFoo = Foo.registerFoo('exampleFoo', 123456)
Error: The method inspect is not implemented.
  at null.<anonymous> (/path/to/module/nestedProxyTest.js:40:31)
  at formatValue (util.js:297:21)
  at Object.inspect (util.js:147:10)
  at REPLServer.self.writer (repl.js:366:19)
  at finish (repl.js:487:38)
  at REPLServer.defaultEval (repl.js:293:5)
  at bound (domain.js:280:14)
  at REPLServer.runBound [as eval] (domain.js:293:12)
  at REPLServer.<anonymous> (repl.js:441:10)
  at emitOne (events.js:101:20)

在花了很多时间思考为什么第二个代理甚至尝试调用一个方法(如果没有给它)之后,我最终放弃了。我希望 exampleFoo 是一个代理对象,如果被调用则接受 Foo 方法。
是什么导致了这里的实际行为?

【问题讨论】:

  • 乍一看,这种架构很难理解。 bind 方法不能满足您的需要吗?
  • "我认为 ES6 代理可能最适合这种情况" - 不,绝对不是。使用简单的class 来关闭apiClient。请重新开始。
  • @Bergi JS 类没有添加任何功能。由于我不想为一些 200 多种 API 方法编写修改后的继承函数,并将 ID 作为它们的第一个参数,因此我需要一种动态调用它们的方法。据我所知,代理是拦截函数调用和修改参数列表同时保持动态属性名称的唯一方法。
  • @trincot 也试过了,但是我无法绑定到 Foo 的当前实例(它包含 API 凭据和配置)
  • @MoFriedrich:如果您有 200 多个 API 方法以 ID 作为其第一个参数,您可以轻松枚举原始类以动态创建新方法(API 也很臃肿)。您不需要也不应该在这里使用代理。

标签: javascript node.js ecmascript-6 es6-proxy


【解决方案1】:

我认为您根本不应该在这里使用代理。假设你有一个可怕的 API

class Foo {
    …
    myApiMethod(id, …) { … }
    … // and so on
}

那么实现您正在寻找的最干净的方法是

const cache = new WeakMap();
Foo.prototype.register = function(id) {
    if (!cache.has(this))
        cache.set(this, new Map());
    const thisCache = cache.get(this);
    if (!thisCache.get(id))
        thisCache.set(id, new IdentifiedFoo(this, id));
    return thisCache.get(id);
};

class IdentifiedFoo {
    constructor(foo, id) {
        this.foo = foo;
        this.id = id;
    }
}
Object.getOwnPropertyNames(Foo.prototype).forEach(function(m) {
    if (typeof Foo.prototype[m] != "function" || m == "register") // etc
        return;
    IdentifiedFoo.prototype[m] = function(...args) {
        return this.foo[m](this.id, ...args);
    };
});

这样你就可以了

var foo = new Foo();
foo.myApiMethod(id, …);
foo.register(id).myApiMethod(…);

【讨论】:

  • 这完全实现了我的目标,甚至更多...谢谢!
【解决方案2】:

首先,我不确定代理模式是处理您的问题的最有效和最干净的方法,但它当然应该可以做到。

我看到的第一个问题是您的实际测试尝试在 Foo 原型(类)本身上调用 registerFoo,而您只为 Foo 的实例定义了它。因此,您必须首先创建一个实例,如下所示:

var foo = new Foo();
var exampleFoo = foo.registerFoo('exampleFoo', 123456);

然后要完成测试,您必须调用一个应该存在的方法。所以为了测试它,我会在Foo 中添加这样的内容:

  // Define an example method on a Foo instance:
  this.myMethod = function (barName /* [, arguments] */) {
    var args = Array.prototype.slice.call(arguments);
    return 'You called myMethod(' + args + ') on a Foo object';
  }

虽然不是问题,但我认为没有必要在 new Proxy(...) 上应用 Object.create,因为后者已经创建了一个对象,我认为将其用作原型而不是将其用作原型并没有什么好处直接你的对象。

通过这些细微的调整,我得到了这段代码,它似乎在浏览器中产生了正确的结果(在此处使用 FireFox):

var Foo = function() {
  // parent instance
  _self = this;
  // example method
  this.myMethod = function (barName /* [, arguments] */) {
    var args = Array.prototype.slice.call(arguments);
    return 'You called myMethod(' + args + ') on a Foo object';
  }
  // custom elements dictionary
  _myFooItems = {};
  // to call parent methods directly on custom elements
  this.myFoos = new Proxy({}, {
      // property getter function (proxy target and called property name as params)
      get: function(target, myFooName) {
        // whether called property is a registered foo
        if (_myFooItems.hasOwnProperty(myFooName)) {
          // create another proxy to intercept method calls on previous one
          return new Proxy({}, {
              // property getter function (proxy target and called property name as params)
              get: function(target, methodName) {
                // whether parent method exists
                if (_self.hasOwnProperty(methodName)) {
                  return function(/* arguments */) {
                    // insert custom element ID into args array
                    var args = Array.prototype.slice.call(arguments);
                    args.unshift(_myFooItems[ myFooName ]);
                    // apply parent method with modified args array
                    return _self[ methodName ].apply(_self, args);
                  };
                } else {
                  // parent method does not exist
                  return function() {
                    throw new Error('The method ' + methodName + ' is not implemented.');
                  }
                }
              }
          });
        }
      }
  });

  // register a custom foo and its ID
  this.registerFoo = function(myFooName, id) {
    // whether the foo has already been registered
    if (_myFooItems.hasOwnProperty(myFooName)) {
      throw new Error('The Foo ' + myFooName + ' is already registered in this instance.');
    }
    // register the foo
    _myFooItems[ myFooName ] = id;
    // return the created foo for further use
    return this.myFoos[ myFooName ];
  };
};

// Test it:
var foo = new Foo();
var exampleFoo = foo.registerFoo('exampleFoo', 123456);
var result = exampleFoo.myMethod(13);
console.log(result);

【讨论】:

  • 好吧,解决前两个问题 - 我把我的例子简化了一点,也许太多了......我刚刚在 Chrome 中尝试了你的代码,它确实有效,但在 NodeJS 中无效。事实证明,一旦 inspect 方法被创建为 Foo 的属性,我就再也没有异常了。我猜这是 NodeJS 代理实现中的一些错误。
  • 好的,有趣的“错误” ;-) 无论如何,@Bergie 为最初的挑战提供了更好的解决方案。
猜你喜欢
  • 1970-01-01
  • 2018-10-13
  • 2015-02-20
  • 1970-01-01
  • 1970-01-01
  • 2015-05-19
  • 2021-11-17
  • 2016-10-11
  • 1970-01-01
相关资源
最近更新 更多