invokable
A way to make invokable JavaScript objects. Reminiscent of Python's __call__
.
Installation
npm install invokable
Usage
Using with a class:
import {Invokable} from 'invokable';
class Rect {
constructor(width, height) {
this.width = width;
this.height = height;
return Invokable.create(this);
}
get area() {
return this.width * this.height;
}
[Invokable.call](depth = 1) {
return this.area * depth;
}
}
- Implement a method with the computed name
[Invokable.call]
with any
signature of your choice.
- End the constructor with
return Invokable.create(this)
Also works with a plain object:
import {Invokable} from 'invokable';
const Rect = (width, height) =>
Invokable.create({
width,
height,
get area() {
return this.width * this.height;
},
[Invokable.call](depth = 1) {
return this.area * depth;
},
});
- Declare a property with the computed name
[Invokable.call]
with any
function of your choice.
- Pass the entire object to
Invokable.create
Capabilities and Limitations
Invokable.create
can:
- Preserve the
this
context of the object.
- Do the right thing when an object's
[Invokable.call]
method has been
replaced at runtime.
- Replicate all properties without triggering getters and setters.
- Inherit the same prototype, ensuring things like
constructor
and the
instanceof
operator still work.
- Create functions whose
name
property is writable. Simply define a name
property in the original target object.
It cannot:
- Modify the original object. A new object that masquerades as the original is
returned.
- Do anything with an object that does not implement
[Invokable.call]
. A
TypeError
is thrown when such an object is given.
- Work on JavaScript engines that don't support
Object.setPrototypeOf
,
Object.getPrototypeOf
, Object.getOwnPropertyDescriptors
, and
Object.defineProperties
, unless they have been polyfilled.
- Be rebound using
.bind(...)
. However, the [Invokable.call]
method can
still be rebound.
API
Invokable.create(target)
takes a target
object that conforms to the
Invokable
interface and returns a new object that is a function that
masquerades as the original target
object. All properties, values, getters,
setters, or otherwise are replicated, including the prototype.
Normal function objects have a read-only property called name
. However, if
the target
object defines its own value or getter called name
, then the
name
property will be made writable in the result. In all cases, the name
property will default to the original function or surrounding class name.
If the target
object does not implement [Invokable.call]
, a TypeError
is thrown.
Invokable.call
is a tag that denotes callability. It is a constant used by
an object to conform to the Invokable
interface. Its current value is the
string __call__
, but it may become a symbol in the future.
TypeScript Support
TypeScript support comes out of the box without any additional setup. Here is
the TypeScript version of the class example:
import {Invokable} from 'invokable';
class Rect {
constructor(public width: number, public height: number) {
return Invokable.create(this);
}
get area() {
return this.width * this.height;
}
[Invokable.call](depth = 1) {
return this.area * depth;
}
}
interface Rect {
(depth?: number): number;
}
Note that it is necessary to modify the interface generated by the class in
order make class instances callable to the type system.
The TypeScript version of the plain object example:
import {Invokable} from 'invokable';
const Rect = (width: number, height: number) =>
Invokable.create({
width,
height,
get area() {
return this.width * this.height;
},
[Invokable.call](depth = 1) {
return this.area * depth;
},
});
Performance
The following observations are based on the benchmarks listed further below.
- Creating an invokable object is very slow. Doing so for a plain object incurs
around a 7x slowdown, whereas doing so for a class instance can suffer from
around a 700x slowdown. Therefore, avoid performing a huge number of calls to
Invokable.create
in a hot code path.
- Property access, regardless of whether or not through the prototype, is not
significantly impacted.
- Invoking the invokable object itself is also not guaranteed to be faster than
directly invoking the method it points to.
Inspiration
invokable
is inspired by the
callable-object
project.
Benchmarks
To run these on your machine, run npm run bench
. The process typically takes
a few minutes, and directly writes results to STDOUT as they finish running.
Results were measured on an Intel Core M @ 1.2GHz with 8GB of DDR3-1600 on
Node.JS v8.3.0. Throughput numbers are expressed in operations per second (ops).
Class instantiation, no args
Test description |
Throughput |
Error |
Percent of best |
plain |
107464004 |
±1.71% |
100.00% |
invokable |
147857 |
±3.32% |
0.14% |
Class instantiation, 5 args
Test description |
Throughput |
Error |
Percent of best |
plain |
12722162 |
±1.78% |
100.00% |
invokable |
144240 |
±3.45% |
1.13% |
Class instantiation, 10 args
Test description |
Throughput |
Error |
Percent of best |
plain |
9185841 |
±1.48% |
100.00% |
invokable |
144790 |
±3.58% |
1.58% |
Class instance: own property access
Test description |
Throughput |
Error |
Percent of best |
plain |
450120750 |
±1.48% |
100.00% |
invokable |
448748906 |
±1.51% |
99.70% |
Class instance: method call on prototype
Test description |
Throughput |
Error |
Percent of best |
plain |
443185849 |
±1.68% |
98.26% |
invokable |
451041317 |
±1.64% |
100.00% |
Class instance: getter on prototype
Test description |
Throughput |
Error |
Percent of best |
plain |
448271681 |
±2.17% |
100.28% |
invokable |
447015599 |
±1.31% |
100.00% |
Class instance: setter on prototype
Test description |
Throughput |
Error |
Percent of best |
plain |
356063076 |
±3.93% |
96.11% |
invokable |
370482861 |
±1.55% |
100.00% |
Object creation, no args
Test description |
Throughput |
Error |
Percent of best |
plain |
495464 |
±5.21% |
100.00% |
invokable |
73149 |
±3.58% |
14.76% |
Object creation, 5 args
Test description |
Throughput |
Error |
Percent of best |
plain |
455371 |
±8.85% |
100.00% |
invokable |
71283 |
±3.99% |
15.65% |
Object creation, 10 args
Test description |
Throughput |
Error |
Percent of best |
plain |
470126 |
±6.42% |
100.00% |
invokable |
70540 |
±4.37% |
15.00% |
Object: own property access
Test description |
Throughput |
Error |
Percent of best |
plain |
416151276 |
±0.96% |
99.76% |
invokable |
417164274 |
±0.87% |
100.00% |
Object: own property call
Test description |
Throughput |
Error |
Percent of best |
plain |
83609556 |
±2.94% |
100.00% |
invokable |
54168693 |
±1.27% |
64.79% |
Object: own property getter
Test description |
Throughput |
Error |
Percent of best |
plain |
51863522 |
±1.21% |
100.00% |
invokable |
51401432 |
±5.03% |
99.11% |
Object: own property setter
Test description |
Throughput |
Error |
Percent of best |
plain |
267193097 |
±0.85% |
100.00% |
invokable |
197801204 |
±18.43% |
74.03% |
Invocation, no args
Test description |
Throughput |
Error |
Percent of best |
instance.__call__() |
17807793 |
±1.19% |
95.57% |
instance[Invokable.call]() |
17446234 |
±1.55% |
93.63% |
instance() |
18633571 |
±1.51% |
100.00% |
Invocation, 5 args
Test description |
Throughput |
Error |
Percent of best |
instance.__call__() |
8164700 |
±3.61% |
97.58% |
instance[Invokable.call]() |
8367181 |
±1.47% |
100.00% |
instance() |
7318022 |
±5.51% |
87.46% |
Invocation, 10 args
Test description |
Throughput |
Error |
Percent of best |
instance.__call__() |
6898069 |
±2.00% |
94.93% |
instance[Invokable.call]() |
7040076 |
±3.05% |
96.88% |
instance() |
7266453 |
±2.24% |
100.00% |