简体   繁体   English

使用代理捕获所有链式方法和getter(用于延迟执行)

[英]Capturing all chained methods and getters using a proxy (for lazy execution)

Context: 语境:

Say I've got an object, obj , with some methods and some getters: 说我有一个对象, obj ,有一些方法和一些getter:

var obj = {
    method1: function(a) { /*...*/ },
    method2: function(a, b) { /*...*/ },
}
Object.defineProperty(obj, "getter1", {get:function() { /*...*/ }});
Object.defineProperty(obj, "getter2", {get:function() { /*...*/ }});

obj is chainable and the chains will regularly include both methods and getters: obj.method2(a,b).getter1.method1(a).getter2 (for example). obj是可链接的,链将定期包括方法和getter: obj.method2(a,b).getter1.method1(a).getter2 (例如)。

I understand that this chained usage of getters is a bit strange and probably inadvisable in most cases, but this isn't a regular js application (it's for a DSL ). 我知道这个链接使用getter有点奇怪,在大多数情况下可能是不可取的,但这不是一个普通的js应用程序(它适用于DSL )。

But what if (for some reason) we wanted to execute these chained methods/getters really lazily? 但是,如果(出于某种原因)我们想要执行这些链式方法/ getter真的很懒散呢? Like, only execute them when a certain "final" getter/method is called? 比如,只有在调用某个“最终”getter /方法时才执行它们?

obj.method2(a,b).getter1.method1(a).getter2.execute

In my case this "final" method is toString which can be called by the explicitly by the user, or implicitly when they try to join it to a string ( valueOf also triggers evaluation). 在我的例子中,这个“最终”方法是toString ,可以由用户显式调用,或者在他们尝试将其连接到字符串时隐式调用( valueOf也会触发评估)。 But we'll use the execute getter example to keep this question broad and hopefully useful to others. 但是我们将使用execute getter示例来保持这个问题的广泛性,并希望对其他人有用。


Question: 题:

So here's the idea: proxy obj and simply store all getter calls and method calls (with their arguments) in an array. 所以这里的想法是:代理obj并简单地将所有getter调用和方法调用(及其参数)存储在一个数组中。 Then, when execute is called on the proxy, apply all the stored getter/method calls to the original object in the correct order and return the result: 然后,当在代理上调用execute时,以正确的顺序将所有存储的getter /方法调用应用于原始对象并返回结果:

var p = new Proxy(obj, {
    capturedCalls: [],
    get: function(target, property, receiver) {
        if(property === "execute") {
            let result = target;
            for(let call of this.capturedCalls) {
                if(call.type === "getter") {
                    result = result[call.name]
                } else if(call.type === "method") {
                    result = result[call.name](call.args)
                }
            }
            return result;
        } else {
            let desc = Object.getOwnPropertyDescriptor(target, property);
            if(desc.value && typeof desc.value === 'function') {
                this.capturedCalls.push({type:"method", name:property, args:[/* how do I get these? */]});
                return receiver;
            } else {
                this.capturedCalls.push({type:"getter", name:property})
                return receiver;
            }
        }
    },
});

So as you can see I understand how to capture the getters and the names of the methods, but I don't know how to get the arguments of the methods. 因此,您可以看到我了解如何捕获getter和方法的名称 ,但我不知道如何获取方法的参数。 I know about the apply trap, but am not quite sure how to use it because as I understand it, it's only for proxies that are actually attached to function objects. 我知道应用陷阱,但我不太清楚如何使用它,因为据我所知,它只适用于实际附加到函数对象的代理。 Would appreciate it if a pro could point me in the right direction here. 如果专业人士能指出我在这里正确的方向,我将不胜感激。 Thanks! 谢谢!


This question seems to have had similar goals. 这个问题似乎有类似的目标。

I was almost there! 我快到了! I was assuming that there was some special way of handling methods and so that led me to the apply trap and other distractions, but as it turns out you can do everything with the get trap: 我假设有一些特殊的方法来处理方法,所以这导致我apply陷阱和其他干扰,但事实证明你可以用get陷阱做任何事情:

var obj = {
    counter: 0,
    method1: function(a) { this.counter += a; return this; },
    method2: function(a, b) { this.counter += a*b; return this; },
};
Object.defineProperty(obj, "getter1", {get:function() { this.counter += 7; return this; }});
Object.defineProperty(obj, "getter2", {get:function() { this.counter += 13; return this; }});

var p = new Proxy(obj, {
    capturedCalls: [],
    get: function(target, property, receiver) {
        if(property === "execute") {
            let result = target;
            for(let call of this.capturedCalls) {
                if(call.type === "getter") {
                    result = result[call.name]
                } else if(call.type === "method") {
                    result = result[call.name].apply(target, call.args)
                }
            }
            return result;
        } else {
            let desc = Object.getOwnPropertyDescriptor(target, property);
            if(desc.value && typeof desc.value === 'function') {
                let callDesc = {type:"method", name:property, args:null};
                this.capturedCalls.push(callDesc);
                return function(...args) { callDesc.args = args; return receiver; };
            } else {
                this.capturedCalls.push({type:"getter", name:property})
                return receiver;
            }
        }
    },
});

The return function(...args) { callDesc.args = args; return receiver; }; return function(...args) { callDesc.args = args; return receiver; }; return function(...args) { callDesc.args = args; return receiver; }; bit is where the magic happens. 位是神奇发生的地方。 When they're calling a function we return them a "dummy function" which captures their arguments and then returns the proxy like normal. 当他们调用一个函数时,我们返回一个“虚函数”,它捕获他们的参数然后像正常一样返回代理。 This solution can be tested with commands like p.getter1.method2(1,2).execute (which yeilds obj with obj.counter===9 ) 可以使用p.getter1.method2(1,2).execute等命令测试此解决方案(使用obj.counter===9可以使用obild obj

This seems to work great, but I'm still testing it and will update this answer if anything needs fixing. 这似乎很有效,但我仍在测试它,如果有任何需要修复的话,我会更新这个答案。

Note: With this approach to "lazy chaining" you'll have to create a new proxy each time obj is accessed. 注意:使用这种“懒惰链接”方法,每次访问obj都必须创建一个新代理。 I do this by simply wrapping obj in a "root" proxy, and spawning the above-described proxy whenever one of its properties are accessed. 我这样做只需将obj包装在“根”代理中,并在访问其中一个属性时生成上述代理。

Improved version: 改良版:

This is probably useless to everyone in the world except me, but I figured I'd post it here just in case. 除了我之外,这对世界上的每个人都没用,但我想我会在这里发布以防万一。 The previous version could only handle methods that returned this . 以前的版本只能处理返回this方法的方法。 This version fixes that and gets it closer to a "general purpose" solution for recording chains and executing them lazily only when needed: 这个版本修复了这个问题并使其更接近于“通用”解决方案,用于记录链并仅在需要时懒惰地执行它们:

var fn = function(){};

var obj = {
    counter: 0,
    method1: function(a) { this.counter += a; return this; },
    method2: function(a, b) { this.counter += a*b; return this; },
    [Symbol.toPrimitive]: function(hint) { console.log(hint); return this.counter; }
};
Object.defineProperty(obj, "getter1", {get:function() { this.counter += 7; return this; }});
Object.defineProperty(obj, "getter2", {get:function() { this.counter += 13; return this; }});

  let fn = function(){};
  fn.obj = obj;
  let rootProxy = new Proxy(fn, {
      capturedCalls: [],
      executionProperties: [
        "toString",
        "valueOf",
        Symbol.hasInstance,
        Symbol.isConcatSpreadable,
        Symbol.iterator,
        Symbol.match,
        Symbol.prototype,
        Symbol.replace,
        Symbol.search,
        Symbol.species,
        Symbol.split,
        Symbol.toPrimitive,
        Symbol.toStringTag,
        Symbol.unscopables,
        Symbol.for,
        Symbol.keyFor
      ],
      executeChain: function(target, calls) {
        let result = target.obj;

        if(this.capturedCalls.length === 0) {
          return target.obj;
        }

        let lastResult, secondLastResult;
        for(let i = 0; i < capturedCalls.length; i++) {
          let call = capturedCalls[i];

          secondLastResult = lastResult; // needed for `apply` (since LAST result is the actual function, and not the object/thing that it's being being called from)
          lastResult = result;

          if(call.type === "get") {
            result = result[call.name];
          } else if(call.type === "apply") {
            // in my case the `this` variable should be the thing that the method is being called from
            // (this is done by default with getters)
            result = result.apply(secondLastResult, call.args);
          }

          // Remember that `result` could be a Proxy
          // If it IS a proxy, we want to append this proxy's capturedCalls array to the new one and execute it
          if(result.___isProxy) {
            leftOverCalls = capturedCalls.slice(i+1);
            let allCalls = [...result.___proxyHandler.capturedCalls, ...leftOverCalls];
            return this.executeChain(result.___proxyTarget, allCalls);
          }

        }
        return result;
      },
      get: function(target, property, receiver) {

        //console.log("getting:",property)

        if(property === "___isProxy") { return true; }
        if(property === "___proxyTarget") { return target; }
        if(property === "___proxyHandler") { return this; }

        if(this.executionProperties.includes(property)) {

          let result = this.executeChain(target, this.capturedCalls);

          let finalResult = result[property];
          if(typeof finalResult === 'function') {
                finalResult = finalResult.bind(result);
          }
          return finalResult;

        } else {
            // need to return new proxy
            let newHandler = {};
            Object.assign(newHandler, this);
            newHandler.capturedCalls = this.capturedCalls.slice(0);
            newHandler.capturedCalls.push({type:"get", name:property});
            let np = new Proxy(target, newHandler)
            return np;
        }
      },
      apply: function(target, thisArg, args) {
          // return a new proxy:
          let newHandler = {};
          Object.assign(newHandler, this);
          newHandler.capturedCalls = this.capturedCalls.slice(0);
          // add arguments to last call that was captured
          newHandler.capturedCalls.push({type:"apply", args});
          let np = new Proxy(target, newHandler);
          return np;
      },
      isExtensible: function(target) { return Object.isExtensible(this.executeChain(target)); },
      preventExtensions: function(target) { return Object.preventExtensions(this.executeChain(target)); },
      getOwnPropertyDescriptor: function(target, prop) { return Object.getOwnPropertyDescriptor(this.executeChain(target), prop); },
      defineProperty: function(target, property, descriptor) { return Object.defineProperty(this.executeChain(target), property, descriptor); },
      has: function(target, prop) { return (prop in this.executeChain(target)); },
      set: function(target, property, value, receiver) { Object.defineProperty(this.executeChain(target), property, {value, writable:true, configurable:true}); return value; },
      deleteProperty: function(target, property) { return delete this.executeChain(target)[property]; },
      ownKeys: function(target) { return Reflect.ownKeys(this.executeChain(target)); }
  });

Note that it proxies a function so that it can capture apply s easily. 请注意,它代理一个函数,以便它可以轻松捕获apply Note also that a new Proxy needs to be made at every step in the chain. 另请注意,需要在链中的每个步骤都进行新的代理。 It may need some tweaking to suit purposes that aren't exactly the same as mine. 可能需要进行一些调整以适应与我的不完全相同的目的。 Again, I don't doubt it uselessness outside of DSL building and other meta-programming stuff - I'm mostly putting it here to perhaps give inspiration to others who are trying to achieve similar things. 同样,我不怀疑它在DSL构建和其他元编程之外的无用 - 我主要是把它放在这里,或许为那些试图实现类似事物的人提供灵感。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM