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:
Using
Object.create
:That's the standard way to create custom-prototyped objects. However, it isn't capable of creating callable objects (functions).
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
-likeFunction
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 thereforeObject.prototype
) in their prototype chain
- Uses the
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 theObject.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]]
usingObject.create()
.Harder-to-understand, unsemantic, hacky-looking code
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:
- Unsupported
getPrototypeOf
trap in Safari - Harder-to-understand, unsemantic, hacky-looking code
- Creation of multiple additional functions and objects (the handler and the traps)
- Unsupported
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?