7

TL:DR; Is it possible to make a property of object to be invocable ( as a function ) only ?

What i mean by this

class Foo{
  bar(value){
    return value
  }
}

let newFoo = new Foo()

console.log(newFoo.bar(123))  // should work fine as function is invoked
console.log(newFoo.bar)  // here i need to throw or display an error instead of returning value

I tried to do this with Proxy and handler.get trap, but i have no clue how to capture whether it is a function call or just property access,

class Foo {
  bar(value) {
    return value
  }
}


const proxied = new Proxy(new Foo(), {
  get: function(target, prop, reciver) {
    if (prop === 'bar') {
      throw new Error('Bar is method need to be invoced')
    }
    return target[prop]
  }
})

console.log(proxied.bar(true))
console.log(proxied.bar)

I have also checked handler.apply but this also doesn't seems to be of no use as this is a trap on function, not on property

class Foo {
  bar(value) {
    return value
  }
}


const proxied = new Proxy(new Foo(), {
  apply: function(target, thisArg, argumentsList) {
    return target(argumentsList[0])
  },
  get: function(target, prop, reciver) {
    if (prop === 'bar') {
      throw new Error('Bar is method need to be invoced')
    }
    return target[prop]
  }
})

console.log(proxied.bar(true))
console.log(proxied.bar)
Code Maniac
  • 37,143
  • 5
  • 39
  • 60
  • I don't *think* so, `newFoo.bar` will be evaluated as an expression before the subsequent argument list gets applied to the prior expression, so if accessing `newFoo.bar` throws, calling `newFoo.bar` would have to throw too – CertainPerformance Sep 21 '19 at 07:47
  • @CertainPerformance yeah that's what i thought cause `.` accessor will return property and then that will be invoked, but not sure, as there are always way to achieve a thing in JS, so hoping if there's anyway to do so – Code Maniac Sep 21 '19 at 07:49
  • 1
    why do you need to do this, sounds like an [XY problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) – Jaromanda X Sep 21 '19 at 07:54
  • @JaromandaX i am trying to practice Proxies, and trying to mimic a strict environment ( or you can call it a linter kind of thing for the object ) just for learning purpose, this may be `XY` problem i am not sure though, i tired with whatever knowledge i have about proxies and couldn't succeed, so here i am asking from people if there's any other way it can be done, or in first place can it be done at all – Code Maniac Sep 21 '19 at 07:59
  • 2
    @Code Maniac Since you have to `get` before `call` as far as I know, you can check if its called after the `get`, so something like this https://jsfiddle.net/y52khqs8/ – Nenad Vracar Sep 21 '19 at 08:42
  • @NenadVracar wow didn't thought of something like this, you should post as answer IMO – Code Maniac Sep 21 '19 at 09:05

1 Answers1

2

No, this is not possible. There is no distinction between

const newFoo = new Foo()
newFoo.bar(123);

and

const newFoo = new Foo()
const bar = newFoo.bar;
Function.prototype.call.call(bar, newFoo, 123); // like `bar.call(newFoo, 123)`
// or Reflect.apply(bar, newFoo, [123]);

i.e. neither newFoo nor bar can distinguish these "from the inside". Now arbitrary things could happen in between the property access and the method call, and during the property access you cannot know what will happen next, so you cannot throw an exception prematurely. The method call might happen never (in newFoo.bar;), and there's no way to recognise that from newFoo alone.

The only approach would be to intercept all other accesses on newFoo and its properties, and throw after you detected a mischievous sequence; possibly having your "linter" check the sequence from the outside after the whole program ran:

const lint = {
  access: 0,
  call: 0,
  check() {
    console.log(this.access == this.call
      ? "It's ok"
      : this.access > this.call
        ? "method was not called"
        : "property was reused");
  },
  run(fn) {
    this.call = this.access = 0;
    try {
      fn();
    } finally {
      this.check();
    }
  }
}

function bar(value) {
  lint.call++; lint.check();
  return value;
}
class Foo {
  get bar() {
    lint.check(); lint.access++;
    return bar;
  }
}
lint.run(() => {
  const newFoo = new Foo;
  newFoo.bar(123);
});
lint.run(() => {
  const newFoo = new Foo;
  newFoo.bar;
});
lint.run(() => {
  const newFoo = new Foo;
  const bar = newFoo.bar;
  bar(123);
  bar(456);
});

The better solution would probably to write your own interpreter for simple expressions, which would only allow method calls.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375