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

Context:

Let's say I have 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() { /*...*/ }});

objis chained, and chains will regularly include both methods and getters: obj.method2(a,b).getter1.method1(a).getter2(for example).

I understand that this chain use of getters is a bit strange and probably not recommended in most cases, but this is not a regular js application (it is for DSL ).

But what if (for some reason) we wanted to use these chain methods / getters really lazy? For example, execute them only when calling a specific "final" getter / method?

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

"" toString, , , (valueOf ). execute getter, , , .


:

, : obj ( ) . , execute -, getter/method :

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

, , , , , . apply , , , , , -, . , . !


, , .

+4
1

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

return function(...args) { callDesc.args = args; return receiver; }; - , . , " ", , , . p.getter1.method2(1,2).execute ( yeilds obj obj.counter===9)

, , , , - .

.. " " , obj. , obj "" - .

:

, , , , , . , this. " " , :

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

, , apply. , . , , , . , , DSL - , , , , .

+5

Source: https://habr.com/ru/post/1667937/


All Articles