0

To avoid misunderstanding, let's agree on the meaning of certain words first. The following meanings are not universally accepted ones, I only suggest them as a context for this question.

  • function -- an instance of Function. It has a procedure associated with it.
  • object -- an instance of any other class. Does not have a procedure associated with it.
  • procedure -- a block of code that can be executed. In JS procedures can only be found associated with functions.
  • method -- a function that is stored in a property of another object. Methods execute in their host objects' contexts.

In JavaScript it is common that objects are customized with properties and methods, while functions don't. In other words, if you see this in code:

foo.bar   // Accessing a property on an instance
foo.baz() // Calling a method on an instance

then foo is almost certainly an object and not a function.

But technically functions are no different from objects (in fact, functions in JS are considered to be objects, it's just me distinguishing them for clarity).

And if you do this:

const foo = function () { /*...*/ }
foo()     // Calling the function's procedure
foo.bar   // Accessing a property on the function
foo.baz() // Calling a method on the function

...it will work just fine.

I would like to introduce two more terms so that you can understand what I mean. I'm not trying to alter the rules of JavaScript here, just want to be understood.

  • function-object -- an function that is used as an object: has its own properties and/or methods.
  • main method -- the procedure of a function-object itself, as opposed to methods stored as properties on a function-object.

Many JS libraries offer function-objects, for example jQuery and LoDash. I. e. you can do both $() and $.map().


Okay, now to the matter.

I would like to be able to create my own function-objects with the following requirements:

  1. I should be able to use the ES2015 class declaration to describe the class of my function-object. Or at least the way of describing the class should be convenient enough. Imperatively assigning properties and methods to a function is not convenient.
  2. The main method of my function object should execute in the same context as its other methods.
  3. The main method and other methods should be bound to the same instance. For example if I have a main method and a normal method that both do console.log(this.foo), then assigning to myFunctionObject.foo should modify the behavior of both the main mehtod and the other method.

In other words, I want to be able to do this:

class Foo {
  constructor   () { this.bar = 'Bar'; }
  mainMethod    () { console.log(this.bar); }
  anotherMethod () { console.log(this.bar); }
}

...but an instance of Foo should have foo.mainMethod() available simply as foo().

And doing foo.bar = 'Quux' should modify the behavior of both foo() and foo.anotherMethod().

Maybe there's a way to achieve it with class Foo extends Function?

Andrey Mikhaylov - lolmaus
  • 23,107
  • 6
  • 84
  • 133
  • possible duplicate of [Create a class that creates Function objects as instance using es6 class syntax](http://stackoverflow.com/q/29248034/1048572) – Bergi Oct 11 '15 at 20:47

2 Answers2

2

If you don't override anything, new Foo will return an object type, not a function type. You can potentially do

class Foo {
  constructor(){
    const foo = () => foo.mainMethod();
    Object.setPrototypeOf(foo, this);

    foo.bar = 'Bar';

    return foo;
  }
  mainMethod    () { console.log(this.bar); }
  anotherMethod () { console.log(this.bar); }
}

so that new Foo will return a function that has explicitly been set up so that it returns a function with the proper prototype chain configuration. Note that I'm explicitly setting foo.bar instead of this.bar so that hasOwnProperty will still work as expected throughout the code.

You could also work around this by setting it up via a base class, e.g.

class CallableClass {
  constructor(callback){
      const fn = (...args) => callback(...args);
      Object.setPrototypeOf(fn, this);
      return fn;
  }
}

class Foo extends CallableClass {
  constructor(){
    super(() => this.mainMethod());
    this.bar = 'Bar';
  }
  mainMethod    () { console.log(this.bar); }
  anotherMethod () { console.log(this.bar); }
}

I'm writing this in real ES6, but you'll have to change the rest/spread args to a normal function if you want it to work in Node v4. e.g.

const foo = function(){ callback.apply(undefined, arguments); };

The downside to both of these approaches is that they rely on Object.setPrototypeOf, which does not work in IE<=10, so it will depend what support you need. The general understanding is that Object.setPrototypeOf is also very slow, so I'm not sure you have a ton of options here.

The main alternative that ES6 provides for this case would be Proxies, but they are not at all well supported yet.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • 2
    (*The dowside to both...*) and the impact on performance of using [`.setPrototypeOf()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf). – Amit Oct 11 '15 at 20:03
  • 1
    I would recommend `Object.setPrototypeOf(foo, Object.getPrototypeOf(this))` so that not every function has its own prototype. Or more explicitly, `Object.setPrototypeOf(foo, new.target.prototype)`. – Bergi Oct 11 '15 at 20:49
  • 1
    When I call `new Foo()()` it logs `undefined` instead of `Bar`. –  Oct 11 '15 at 23:27
  • @torazaburo Quite right, my mistake for changing something without testing it :) Had `this.mainMethod()` instead of `foo.mainMethod()` in the arrow function of the first example. – loganfsmyth Oct 11 '15 at 23:46
1

AFAIK there is no way to add a [[Call]] method to an existing object.

Proxies might seem a potential solution, but the problems are that the proxy object is a different object than the [[ProxyHandler]], and that the [[ProxyHandler]] must already have a [[Call]] method in order to be able to set an "apply" trap.

Therefore, the only viable solution is creating a new function with the desired [[Call]], replace its [[Prototype]], and make the constructor return that function instead of the "real" instance.

class Foo {
  constructor   () {
    var instance = function(){ return instance.mainMethod(); };
    Object.setPrototypeOf(instance, Foo.prototype);
    return Object.assign(instance, {bar: 'Bar'});
  }
  mainMethod    () { console.log(this.bar); }
  anotherMethod () { console.log(this.bar); }
}
Community
  • 1
  • 1
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • Uhm, I should have refreshed the page before answering, I didn't see a similar answer appeared after I opened this page some hours ago. Well, I will leave mine because the other doesn't work. – Oriol Oct 11 '15 at 23:27