Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
155 views
in Technique[技术] by (71.8m points)

javascript - 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:

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).

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).

But what if (for some reason) we wanted to execute these chained methods/getters really lazily? Like, only execute them when a certain "final" getter/method is called?

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). But we'll use the execute getter example to keep this question broad and hopefully useful to others.


Question:

So here's the idea: proxy obj and simply store all getter calls and method calls (with their arguments) in an array. 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:

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. 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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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:

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; }; 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)

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. I do this by simply wrapping obj in a "root" proxy, and spawning the above-described proxy whenever one of its properties are accessed.

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 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 applys easily. 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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...