1

My goal is to create callable JavaScript objects (functions), that have a custom [[Prototype]], and may (or may not) inherit from Function.prototype.

This question has been asked many times (even on Stack Overflow, like this, this or this), but none of these has good solutions (some has no answers, while others have the solutions listed below).

Some ways came to my mind. Here they are:

  1. Using Object.create:

    That's the standard way to create custom-prototyped objects. However, it isn't capable of creating callable objects (functions).

  2. Subclassing Function:

    That's probably the most straightforward way, that most people think of. Something like:

    class MyFunction extends Function{
      constructor(id){
        super(
          '...args',
          'return args.length'
        )
        this.id=id
      }
      logCall(...args){
        console.log('Calling MyFunction #%i with arguments: %o', this.id, args)
        return Reflect.apply(this, this, args)
      }
    }
    
    const fn = new MyFunction(1)
    console.log(fn('foo', 'bar'))   //2
    fn.logCall('foo', 'bar', 'baz') //Calling MyFunction #1 with arguments: ['foo', 'bar', 'baz']
    //Works, so far so good!

    However, that has several downsides:

    • Uses the eval-like Function constructor
    • Runtime compilation of the function's body, every time a new instance constructed
    • Lack of access to lexical/closure scope inside the function body
    • Lack of compile-time errors and syntax highlighting in the function body: harder to maintain
    • Impossible to create functions that don't have Function.prototype (and therefore Object.prototype) in their prototype chain
  3. Object.setPrototypeOf():

    class MyFunction{
      constructor(id){
        const _this = function (...args){
          return args.length
        }
        Object.setPrototypeOf(_this, Object.getPrototypeOf(this))
        _this.id=id
        return _this
      }
      logCall(...args){
        console.log('Calling MyFunction #%i with arguments: %o', this.id, args)
        return Reflect.apply(this, this, args)
      }
    }
    
    const fn = new MyFunction(1)
    console.log(fn('foo', 'bar'))   //2
    fn.logCall('foo', 'bar', 'baz') //Calling MyFunction #1 with arguments: ['foo', 'bar', 'baz']
    //Works as well, so far so good!

    Downsides:

    • Scary red warning on MDN about performance:

      Warning: Changing the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, currently a very slow operation in every browser and JavaScript engine. In addition, the effects of altering inheritance are subtle and far-flung, and are not limited to simply the time spent in the Object.setPrototypeOf(...) statement, but may extend to any code that has access to any object whose [[Prototype]] has been altered.

      Because this feature is a part of the language, it is still the burden on engine developers to implement that feature performantly (ideally). Until engine developers address this issue, if you are concerned about performance, you should avoid setting the [[Prototype]] of an object. Instead, create a new object with the desired [[Prototype]] using Object.create().

    • Harder-to-understand, unsemantic, hacky-looking code

  4. Proxy with faked traps:

    class MyFunction{
      constructor(id){
        const _this = new Proxy(
          function (...args){
            return args.length
          }, 
          Object.assign(
            Reflect
              .ownKeys(Reflect)
              .reduce((handler, trap) => ( //Forward operations to real `this`
                handler[trap] = (...args) => Reflect[trap](this, ...args.slice(1)),
                handler
              ), {}),
            {
              apply(...args){
                return Reflect.apply(...args)
              }
            }
          )
        )
        _this.id=id
        return _this
      }
      logCall(...args){
        console.log('Calling MyFunction #%i with arguments: %o', this.id, args)
        return Reflect.apply(this, this, args)
      }
    }
    
    const fn = new MyFunction(1)
    console.log(fn('foo', 'bar'))   //2
    fn.logCall('foo', 'bar', 'baz') //Calling MyFunction #1 with arguments: ['foo', 'bar', 'baz']
    //Works as well, so far so good!

    Downsides:

So, as far as I know, there's no good way to do that.

My question is:

Is there any alternative, that's better than these, or, if there isn't any, which of these has the most advantages in terms of performance, readability and support?

FZs
  • 16,581
  • 13
  • 41
  • 50
  • First off, prototype mutation isn't harmful if it's done immediately, in fact, benchmarks have shown the sooner it is done, the better performance one can achieve! Secondly, you may be interested in subclassing [`WebAssembly.Function`](https://github.com/WebAssembly/js-types), but it won't be out for... a few months at minimum. –  Dec 24 '20 at 04:47

0 Answers0