How to extend Function with ES6 classes?

42,114

Solution 1

The super call will invoke the Function constructor, which expects a code string. If you want to access your instance data, you could just hardcode it:

class Smth extends Function {
  constructor(x) {
    super("return "+JSON.stringify(x)+";");
  }
}

but that's not really satisfying. We want to use a closure.

Having the returned function be a closure that can access your instance variables is possible, but not easy. The good thing is that you don't have to call super if you don't want to - you still can return arbitrary objects from your ES6 class constructors. In this case, we'd do

class Smth extends Function {
  constructor(x) {
    // refer to `smth` instead of `this`
    function smth() { return x; };
    Object.setPrototypeOf(smth, Smth.prototype);
    return smth;
  }
}

But we can do even better, and abstract this thing out of Smth:

class ExtensibleFunction extends Function {
  constructor(f) {
    return Object.setPrototypeOf(f, new.target.prototype);
  }
}

class Smth extends ExtensibleFunction {
  constructor(x) {
    super(function() { return x; }); // closure
    // console.log(this); // function() { return x; }
    // console.log(this.prototype); // {constructor: …}
  }
}
class Anth extends ExtensibleFunction {
  constructor(x) {
    super(() => { return this.x; }); // arrow function, no prototype object created
    this.x = x;
  }
}
class Evth extends ExtensibleFunction {
  constructor(x) {
    super(function f() { return f.x; }); // named function
    this.x = x;
  }
}

Admittedly, this creates an additional level of indirection in the inheritance chain, but that's not necessarily a bad thing (you can extend it instead of the native Function). If you want to avoid it, use

function ExtensibleFunction(f) {
  return Object.setPrototypeOf(f, new.target.prototype);
}
ExtensibleFunction.prototype = Function.prototype;

but notice that Smth will not dynamically inherit static Function properties.

Solution 2

This is an approach to creating callable objects that correctly reference their object members, and maintain correct inheritance, without messing with prototypes.

Simply:

class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)')
    var self = this.bind(this)
    this.__self__ = self
    return self
  }

  // Example `__call__` method.
  __call__(a, b, c) {
    return [a, b, c];
  }
}

Extend this class and add a __call__ method, more below...

An explanation in code and comments:

// This is an approach to creating callable objects
// that correctly reference their own object and object members,
// without messing with prototypes.

// A Class that extends Function so we can create
// objects that also behave like functions, i.e. callable objects.
class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)');
    // Here we create a function dynamically using `super`, which calls
    // the `Function` constructor which we are inheriting from. Our aim is to create
    // a `Function` object that, when called, will pass the call along to an internal
    // method `__call__`, to appear as though the object is callable. Our problem is
    // that the code inside our function can't find the `__call__` method, because it
    // has no reference to itself, the `this` object we just created.
    // The `this` reference inside a function is called its context. We need to give
    // our new `Function` object a `this` context of itself, so that it can access
    // the `__call__` method and any other properties/methods attached to it.
    // We can do this with `bind`:
    var self = this.bind(this);
    // We've wrapped our function object `this` in a bound function object, that
    // provides a fixed context to the function, in this case itself.
    this.__self__ = self;
    // Now we have a new wrinkle, our function has a context of our `this` object but
    // we are going to return the bound function from our constructor instead of the
    // original `this`, so that it is callable. But the bound function is a wrapper
    // around our original `this`, so anything we add to it won't be seen by the
    // code running inside our function. An easy fix is to add a reference to the
    // new `this` stored in `self` to the old `this` as `__self__`. Now our functions
    // context can find the bound version of itself by following `this.__self__`.
    self.person = 'Hank'
    return self;
  }
  
  // An example property to demonstrate member access.
  get venture() {
    return this.person;
  }
  
  // Override this method in subclasses of ExFunc to take whatever arguments
  // you want and perform whatever logic you like. It will be called whenever
  // you use the obj as a function.
  __call__(a, b, c) {
    return [this.venture, a, b, c];
  }
}

// A subclass of ExFunc with an overridden __call__ method.
class DaFunc extends ExFunc {
  constructor() {
    super()
    this.a = 'a1'
    this.b = 'b2'
    this.person = 'Dean'
  }

  ab() {
    return this.a + this.b
  }
  
  __call__(ans) {
    return [this.ab(), this.venture, ans];
  }
}

// Create objects from ExFunc and its subclass.
var callable1 = new ExFunc();
var callable2 = new DaFunc();

// Inheritance is correctly maintained.
console.log('\nInheritance maintained:');
console.log(callable2 instanceof Function);  // true
console.log(callable2 instanceof ExFunc);  // true
console.log(callable2 instanceof DaFunc);  // true

// Test ExFunc and its subclass objects by calling them like functions.
console.log('\nCallable objects:');
console.log( callable1(1, 2, 3) );  // [ 'Hank', 1, 2, 3 ]
console.log( callable2(42) );  // [ 'a1b2', Dean', 42 ]

// Test property and method access
console.log(callable2.a, callable2.b, callable2.ab())

View on repl.it

Further explanation of bind:

function.bind() works much like function.call(), and they share a similar method signature:

fn.call(this, arg1, arg2, arg3, ...); more on mdn

fn.bind(this, arg1, arg2, arg3, ...); more on mdn

In both the first argument redefines the this context inside the function. Additional arguments can also be bound to a value. But where call immediately calls the function with the bound values, bind returns an "exotic" function object that transparently wraps the original, with this and any arguments preset.

So when you define a function then bind some of its arguments:

var foo = function(a, b) {
  console.log(this);
  return a * b;
}

foo = foo.bind(['hello'], 2);

You call the bound function with only the remaining arguments, its context is preset, in this case to ['hello'].

// We pass in arg `b` only because arg `a` is already set.
foo(2);  // returns 4, logs `['hello']`

Solution 3

You can wrap the Smth instance in a Proxy with an apply (and maybe construct) trap:

class Smth extends Function {
  constructor (x) {
    super();
    return new Proxy(this, {
      apply: function(target, thisArg, argumentsList) {
        return x;
      }
    });
  }
}
new Smth(256)(); // 256

Solution 4

Update:

Unfortunately this doesn't quite work because it's now returning a function object instead of a class, so it seems this actually can't be done without modifying the prototype. Lame.


Basically the problem is there is no way of setting the this value for the Function constructor. The only way to really do this would be to use the .bind method afterwards, however this is not very Class-friendly.

We could do this in a helper base class, however this does does not become available until after the initial super call, so it's a bit tricky.

Working Example:

'use strict';

class ClassFunction extends function() {
    const func = Function.apply(null, arguments);
    let bound;
    return function() {
        if (!bound) {
            bound = arguments[0];
            return;
        }
        return func.apply(bound, arguments);
    }
} {
    constructor(...args) {
        (super(...args))(this);
    }
}

class Smth extends ClassFunction {
    constructor(x) {
        super('return this.x');
        this.x = x;
    }
}

console.log((new Smth(90))());

(Example requires modern browser or node --harmony.)

Basically the base function ClassFunction extends will wrap the Function constructor call with a custom function which is similar to .bind, but allows binding later, on the first call. Then in the ClassFunction constructor itself, it calls the returned function from super which is now the bound function, passing this to finish setting up the custom bind function.

(super(...))(this);

This is all quite a bit complicated, but it does avoid mutating the prototype, which is considered bad-form for optimization reasons and can generate warnings in browser consoles.

Solution 5

I took the advice from Bergi's answer and wrapped it into an NPM module.

var CallableInstance = require('callable-instance');

class ExampleClass extends CallableInstance {
  constructor() {
    // CallableInstance accepts the name of the property to use as the callable
    // method.
    super('instanceMethod');
  }

  instanceMethod() {
    console.log("instanceMethod called!");
  }
}

var test = new ExampleClass();
// Invoke the method normally
test.instanceMethod();
// Call the instance itself, redirects to instanceMethod
test();
// The instance is actually a closure bound to itself and can be used like a
// normal function.
test.apply(null, [ 1, 2, 3 ]);
Share:
42,114
Qwertiy
Author by

Qwertiy

Updated on July 08, 2022

Comments

  • Qwertiy
    Qwertiy almost 2 years

    ES6 allows to extend special objects. So it's possible to inherit from the function. Such object can be called as a function, but how can I implement the logic for such call?

    class Smth extends Function {
      constructor (x) {
        // What should be done here
        super();
      }
    }
    
    (new Smth(256))() // to get 256 at this call?
    

    Any method of class gets reference to the class instance via this. But when it is called as a function, this refers to window. How can I get the reference to the class instance when it is called as a function?

    PS: Same question in Russian.

  • Qwertiy
    Qwertiy about 8 years
    I want to get access to class state from the function.
  • Felix Kling
    Felix Kling about 8 years
    @Qwertiy: Then use Bergi's second suggestion.
  • Mulan
    Mulan about 8 years
    What are you actually trying to do?
  • Alexander O'Mara
    Alexander O'Mara about 8 years
    I think Classes are always in strict mode: stackoverflow.com/questions/29283935/…
  • Qwertiy
    Qwertiy about 8 years
    @AlexanderO'Mara, by the way, this is window, not undefined, so the function created is not in strict mode (at least in chrome).
  • Qwertiy
    Qwertiy about 8 years
    Cool idea. Like this. Should I implement somemore logic instead of placing in inside of apply?
  • Bergi
    Bergi about 8 years
    A proxy would incur quite some overhead, wouldn't it? Also, this is still an empty function (check new Smth().toString()).
  • Oriol
    Oriol about 8 years
    @Bergi No idea about performance. MDN has a big red bold warning about setPrototypeOf and doesn't say anything about proxies. But I guess proxies can be as problematic as setPrototypeOf. And about toString, it can be shadowed with a custom method in Smth.prototype. The native one is implementation-dependent anyways.
  • Oriol
    Oriol about 8 years
    @Qwertiy You can add a construct trap to specify the behavior of new new Smth(256)(). And add custom methods that shadow the native ones which access the code of a function, like toString as Bergi noted.
  • Qwertiy
    Qwertiy about 8 years
    Please, stop downwoting this answer. I've already wrote that it is a bad way. But it really is an answer - it works both in FF and Chrome (don't have Edge to check).
  • Qwertiy
    Qwertiy about 8 years
    I ment is your apply method implemented in the way it is supposed to be used, or it is just a demonstration and I need to look through more information about Proxy and Reflect to use it in proper way?
  • Alexander O'Mara
    Alexander O'Mara about 8 years
    I'm guessing this works because Function is not in strict mode. Though awful, it is interesting +1. You probably wouldn't be able to walk a chain any further though.
  • Oriol
    Oriol about 8 years
    @Qwertiy The value of apply is a function which will be called when you call the proxy object. It can be more powerful than in the example (notice I didn't use target, thisArg nor argumentsList). But yes, it's supposed to be used like this.
  • Qwertiy
    Qwertiy about 8 years
    @AlexanderO'Mara, what do you mean by chain?
  • Alexander O'Mara
    Alexander O'Mara about 8 years
    @Qwertiy You can walk the callee chain until you get to a function in strict mode, so I guess this works because the function it creates is not in strict mode, only the calling function will be.
  • Qwertiy
    Qwertiy about 8 years
    @AlexanderO'Mara, what do you think about updated answer?
  • Bergi
    Bergi about 8 years
    You are overcomplicating things. bound will refer to the function that you return from that anonymous class. Just name it, and refer to it directly. I also would recommend to avoid passing code strings around, they're just a mess to work with (in every step of the development process).
  • Bergi
    Bergi about 8 years
    That extends doesn't really seem to work as expected, as Function.isPrototypeOf(Smth) and also new Smth instanceof Function are false.
  • Bergi
    Bergi about 8 years
    @AlexanderO'Mara: You don't get around mutating the prototype of the function if you want your Smth instances to be instanceof Smth (as everyone would expect). You can omit the Object.setPrototypeOf call if you don't need this or any of your prototype methods declared in your class.
  • Alexander O'Mara
    Alexander O'Mara about 8 years
    @Bergi What JS engine are you using? console.log((new Smth) instanceof Function); is true for me in Node v5.11.0 and the latest Firefox.
  • Bergi
    Bergi about 8 years
    Oops, wrong example. It's new Smth instanceof Smth that is not working with your solution. Also no methods of Smth will be avaible on your instances - as you just return a standard Function, not a Smth.
  • Bergi
    Bergi about 8 years
    @AlexanderO'Mara: Also Object.setPrototypeOf is not that much of an optimisation hazard as long it is done right after creating the object. It's just if you mutate the [[prototype]] of an object back and forth during its lifetime that it will be bad.
  • Qwertiy
    Qwertiy about 8 years
    new Anth(100) and new Evth(100) - Uncaught ReferenceError: this is not defined - Google Chrome 49.0.2623.112
  • Bergi
    Bergi about 8 years
    I think you really should pass this instead of arguments.callee to your apply method. That way, me (the parameter) is not the same as this (the Smth instance) any more.
  • Alexander O'Mara
    Alexander O'Mara about 8 years
    @Bergi Darn it, looks like you're right. However extending any native types seems to have the same problem. extend Function also makes new Smth instanceof Smth false.
  • Qwertiy
    Qwertiy about 8 years
    @Bergi, super('/* `this` is window here */') - what this are you talking about?
  • Bergi
    Bergi about 8 years
    @Qwertiy: Oh my, of course. Thanks for hint, I fixed it.
  • Bergi
    Bergi about 8 years
    @Qwertiy: Yes, exactly that this. It's window (because you're in sloppy mode to get arguments.callee) in most cases, yes, but it might not be. You still could .call() your new Smth or use it as a method of an object or as an event handler etc.
  • Qwertiy
    Qwertiy about 8 years
    @Bergi, (new Smth(200)).call(new Smth(100), 1) is 201 instead of 101 - are you talking about that case? Let's go to the chat?
  • Bergi
    Bergi about 8 years
    That sounds like an implementation bug. class X extends Function {}; new X instanceof X should return true according to the spec.
  • Bergi
    Bergi about 8 years
    Yes, a case such as that. It should end up with this.x == 200 and me.x == 100, so that you can choose in the apply method. The call should be equivalent to (new Smth(200)).apply(new Smth(100), [1])
  • Alexander O'Mara
    Alexander O'Mara about 8 years
    @Bergi I just posted a question on this issue: stackoverflow.com/questions/36875299/…
  • Bergi
    Bergi about 8 years
    @Qwertiy: I don't think you should do that global object detection. The function could reasonably be explicitly called on window (or whatever), and in that case you would have to not replace it with null. Just accept that it's sloppy and don't try to do magic :-)
  • Qwertiy
    Qwertiy about 8 years
    @Bergi, but it will always be called on window implicitly - how to handle it than? Or just move detection into apply to make different calls consistent?
  • Bergi
    Bergi about 8 years
    @Qwertiy: I mean that you cannot distinguish these calls in sloppy mode, and you should better leave this as it is.
  • Qwertiy
    Qwertiy about 8 years
    @Bergi, then apply will be always getting window as me on normal calls - in that cases it should be ignored.
  • Bergi
    Bergi over 7 years
    Can you please add an explanation why bind works (i.e. why it returns an instance of ExFunc)?
  • Adrien
    Adrien over 7 years
    @Bergi bind returns a transparent function object that wraps the function object it was called on, which is our callable object, just with the this context rebound. So it really returns a transparently wrapped instance of ExFunc. Post updated with more info on bind.
  • Bergi
    Bergi over 7 years
    "transparent wrapper" sounds too much like a proxy, especially as if property accesses would be forwarded. But they are not! The bound function is a totally distinct object with its own properties, it only does forward calls. But if its a different object, how can it still be instanceof ExFunc? That's what I wanted you to explain. (And please remove the statement about the similarity between call and bind. Their signature is not the same)
  • Adrien
    Adrien over 7 years
    @Bergi All getters/setters and methods are accessible, properties/attributes must be assigned in the constructor after bind in ExFunc. In subclasses of ExFunc, all members are accessible. As for instanceof; in es6 bound functions are referred to as exotic, so their inner workings are not apparent, but I'm thinking it passes the call on to its wrapped target, via Symbol.hasInstance. It is much like a Proxy, but it's a simple method to accomplish the desired effect. Their signature is similar not the same.
  • Bergi
    Bergi over 7 years
    Yes, all properties must be assigned to the bound function. The original (super(…) return value) function's properties are not accessible. Yes, it's an exotic object, but not a host object, so it's inner working are detailed in the spec. Especially the inheritance one which is so important one, that makes prototype methods available and instanceof work. But you shouldn't take this for granted…
  • Panu Logic
    Panu Logic over 6 years
    I think this example gives a simple answer to the original question "... how can I implement the logic for such call". Just pass it as a function-valued argument to the constructor. In the code above the class Funk does not explicitly extend Function although it could, it doesn't really need to. As you can see you can call its "instances" juts like you call any ordinary function.
  • Artyer
    Artyer almost 6 years
    I wanted to do something like this (Have the real function be in a property of the function object) but I needed to preserve this. To do that, instead of .bind and using this (Which I thought would break (new ExFunc()) instanceof ExFunc but doesn't), I used arguments.callee, which is a reference to the original function. So super('return arguments.callee.__call__.apply(this, arguments)'). This works because the Function constructor is always in the global scope, so 'use strict'; only applies if you write it in the string.
  • rob
    rob almost 5 years
    doesn't seem to work if you add any functions/varialbes to the class
  • Adrien
    Adrien almost 5 years
    @rob You need to add the props and methods to a subclass of ExFunc, I've updated the repl with an example.
  • rob
    rob almost 5 years
    @Adrien but from inside __call__ I can't access this.a or this.ab(). e.g. repl.it/repls/FelineFinishedDesktopenvironment
  • Adrien
    Adrien almost 5 years
    @rob well spotted, there is a reference error, I've updated the answer and code with a fix and a new explanation.
  • halfbit
    halfbit over 4 years
    Nice work thx. - but - please - don't write "simply" in this context ;)
  • Monsieur Pierre Dunn
    Monsieur Pierre Dunn over 4 years
    You're genious! This is the best of all solutions.
  • seanlinsley
    seanlinsley over 4 years
    The function prototype defines a read-only name attribute that child classes end up inheriting. I worked around that by redefining it: Object.defineProperty(f, 'name', {value: null, writable: true})
  • Armen Michaeli
    Armen Michaeli about 4 years
    You have to call the super constructor when extending a class.
  • Bergi
    Bergi about 4 years
    @amn No, you do not, when you don't use this and return an object.
  • KInGcC
    KInGcC over 3 years
    It doesn't work properly under TS and says: TypeError: this.__self__.__call__ is not a function
  • Marvin Brouwer
    Marvin Brouwer over 3 years
    I'm wondering about how secure this is. Isn't using new Function almost the same as using eval() ? I realize you abstract away the string constructor but I'm still wondering if this has the same security flaws. Does anyone know?
  • Someone
    Someone over 2 years
    Shouldn't you also assign this to the proxy? That way, you can still get other data about the function? Or would it act like it's a proxy, not a function?