5

I currently have a Proxy object that I want to capture property calls to if the property is not defined. A basic version of my code would be something like this.

var a = new Proxy({}, {
    get: function(target, name, receiver) {
        if (target in name) {
            return target[name];
        } else {    
            function a() {
                return arguments;
            }
            var args = a();
            return [target, name, receiver, args];
        }
    }
});

Property calls to a here (i.e: a.b; a.c() etc) should return the target, name, receiver and arguments of the property call.

The problem I wish to solve, however, requires me to know whether the property call is for a property or a function, such that I can apply different treatments to each. Checking the length of the arguments object does not work, as calling a.c() would yield a length of 0 just like a.b, so it would be treated as a plain property and not a method.

Is there a way, therefore, to identify whether the property attempting to be accessed is being called as a function or not.

UPDATE: I should clarify, this method needs to work if the accessed property/method is undefined, as well as existing.

dbr
  • 661
  • 1
  • 8
  • 21
  • 2
    No, the `get` handler only handles the `a.b` and `a.c` property access. It does not - and *cannot* - know whether the result of the access will be invoked. What exactly do you need this for? – Bergi Jun 09 '17 at 14:16

4 Answers4

3

It's possible in a very hacky way. We return a function if the property is undefined. If this function is called, then we know the user was trying to call the property as a function. If it never is, it was called as a property. To check if the function was called, we take advantage of the fact that a Promise's callback is called in the next iteration of the event loop. This means that we won't know if it's a property or not until later, as the user needs a chance to call the function first (as our code is a getter).

One drawback of this method is that the value returned from the object will be the new function, not undefined, if the user was expecting a property. Also this won't work for you if you need the result right away and can't wait until the next event loop iteration.

const obj = {
  func: undefined,
  realFunc: () => "Real Func Called",
  prop: undefined,
  realProp: true
};

const handlers = {
  get: (target, name) => {
    const prop = target[name];
    if (prop != null) { return prop; }

    let isProp = true;
    Promise.resolve().then(() => {
      if (isProp) {
        console.log(`Undefined ${name} is Prop`)
      } else {
        console.log(`Undefined ${name} is Func`);
      }
    });
    return new Proxy(()=>{}, {
      get: handlers.get,
      apply: () => {
        isProp = false;
        return new Proxy(()=>{}, handlers);
      }
    });
  }
};

const proxied = new Proxy(obj, handlers);

let res = proxied.func();
res = proxied.func;
res = proxied.prop;
res = proxied.realFunc();
console.log(`realFunc: ${res}`);
res = proxied.realProp;
console.log(`realProp: ${res}`);
proxied.propC1.funcC2().propC3.funcC4().funcC5();
quw
  • 2,844
  • 1
  • 26
  • 35
  • How would I extend this to capture all property access in a chained call? My attempts to do this have failed as this currently returns a promise object, of which the next property access is identified as 'then' as opposed to whatever the next property access in the initial chain was. – dbr Jun 09 '17 at 06:18
  • You can just return a `Proxy` with the same get handler (and move the logic that was previously in the function into the apply handler). This should allow arbitrary property access/function call chains. See my edit. – quw Jun 09 '17 at 14:10
  • The point of the promise is to give the property time to be called as a function. If the function (now proxy with apply handler) which we returned is called, the isProp state variable will be updated. When the promise then callback is called in the next tick, we will know if the user treated it as a property or as a function. – quw Jun 09 '17 at 14:27
2

You can't know ahead of time whether it's a call expression or just a member expression, but you can deal with both situations simultaneously.

By returning a proxy targeting a deep clone of the original property that reflects all but two trap handlers to the original property, you can either chain or invoke each member expression.

The catch is that the proxy target also needs to be callable so that the handler.apply trap does not throw a TypeError:

function watch(value, name) {
  // create handler for proxy
  const handler = new Proxy({
    apply (target, thisArg, argsList) {
      // something was invoked, so return custom array
      return [value, name, receiver, argsList];
    },
    get (target, property) {
      // a property was accessed, so wrap it in a proxy if possible
      const {
        writable,
        configurable
      } = Object.getOwnPropertyDescriptor(target, property) || { configurable: true };
      return writable || configurable 
        ? watch(value === object ? value[property] : undefined, property)
        : target[property];
    }
  }, {
    get (handler, trap) {
      if (trap in handler) {
        return handler[trap];
      }
      // reflect intercepted traps as if operating on original value
      return (target, ...args) => Reflect[trap].call(handler, value, ...args);
    }
  });
  
  // coerce to object if value is primitive
  const object = Object(value);
  // create callable target without any own properties
  const target = () => {};
  delete target.length;
  delete target.name;
  // set target to deep clone of object
  Object.setPrototypeOf(
    Object.defineProperties(target, Object.getOwnPropertyDescriptors(object)),
    Object.getPrototypeOf(object)
  );
  // create proxy of target
  const receiver = new Proxy(target, handler);
  
  return receiver;
}

var a = watch({ b: { c: 'string' }, d: 5 }, 'a');

console.log(a('foo', 'bar'));
console.log(a.b());
console.log(a.b.c());
console.log(a.d('hello', 'world'));
console.log(a.f());
console.log(a.f.test());
Open Developer Tools to view Console.

The Stack Snippets Console attempts to stringify the receiver in a weird way that throws a TypeError, but in the native console and Node.js it works fine.

Try it online!

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
1

Would the typeof operator work for you?

For example:

if(typeof(a) === "function")
{
    ...
}
else
{
    ...
}
drone6502
  • 433
  • 2
  • 7
  • Sadly not. a is always a function as it's simply a conduit to get any arguments associated with the property call. – dbr Jun 08 '17 at 16:43
  • 1
    `typeof` is an operator, so it should be used as one `typeof a === "function"`; no parentheses since it's not a function... – Heretic Monkey Jun 08 '17 at 18:42
  • @MikeMcCaughan I am aware of this fact, and so as to avoid confusion for anyone viewing this question later on I would like to clarify one function. Even when using `typeof` as it should be as an operator, the problem still persists as `a` is a function regardless of whether the called property is a function or not. – dbr Jun 08 '17 at 19:42
  • @DeanBrunt My comment was directed to the author of the answer. – Heretic Monkey Jun 08 '17 at 19:44
  • @MikeMcCaughan My apologies, I misunderstood to whom you were referring. – dbr Jun 08 '17 at 19:44
-1

Some ideas I've come up with, which achieve a similar result at a small cost:


A

typeof(a.b) === "function" //`false`, don't call it.
typeof(a.c) === "function" //`true`, call it.

//Maybe you're not intending to try to call non-functions anyways?
a.c();

B

get: function(target, property) {
  //For this, it would have to already be set to a function.
  if (typeof(target[property] === "function") {
    
  }
}

C

a.b;
//Simply change the structuring a little bit for functions, e.g.:
a.func.c();
//Then, `func` would be set and handled as a special property.
Andrew
  • 5,839
  • 1
  • 51
  • 72