最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Capturing all chained methods and getters using a proxy (for lazy execution) - Stack Overflow

programmeradmin2浏览0评论

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.

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.

Share Improve this question edited May 23, 2017 at 12:02 CommunityBot 11 silver badge asked Jan 27, 2017 at 2:51 user993683user993683 4
  • The proxy shouldn't return a target (current object instance). It should always return another proxy that creates wrapper functions for real methods and properties and holds the entire chain within itself. Having something like capturedCalls in original object is the straight way to memory leaks, nothing else. The whole idea is utterly impractical (and also sluggish, and also prone to creepy infinite recursion stuff) and has value only as tricky proxy exercise. Which means that you steal all the fun from yourself by asking somebody to solve it for you. – Estus Flask Commented Jan 27, 2017 at 3:22
  • Hey, @estus. The capturedCalls isn't in the original object, it's in the proxy handler object. As I said in the question, this isn't a normal JS application. It's for a DSL - I'm willing to bend good practices to get this to work. Can you explain why you think it's impractical in my context? Can you suggest a better approach to achieving this sort of functionality? Can you explain how it is that I'm "stealing fun from myself" by asking about something that I misunderstand? I appreciate your ment, but it seems a little hasty and hasn't really contributed to my understanding of this problem – user993683 Commented Jan 27, 2017 at 3:43
  • Are you attempting to be able to proxy over any kind of object? – SethGunnells Commented Jan 27, 2017 at 5:21
  • @SethGunnells With my particular use case I have specific objects with specific methods, but a general solution would be good. I've just posted one potential solution below if you wanted to see if it makes sense? – user993683 Commented Jan 27, 2017 at 5:24
Add a ment  | 

1 Answer 1

Reset to default 13

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

发布评论

评论列表(0)

  1. 暂无评论