Edit
Since I was in the mood, I figured I'd publish a package for this on npm.
Find centralized, trusted content and collaborate around the technologies you use most.
Learn more about CollectivesStack Internal
Knowledge at work
Bring the best of human thought and AI automation together at your work.
Explore Stack InternalThis is the solution I've worked out that serves all my needs of extending functions and has served me quite well. The benefits of this technique are:
ExtensibleFunction, the code is idiomatic of extending any ES6 class (no, mucking about with pretend constructors or proxies).instanceof / .constructor return the expected values..bind() .apply() and .call() all function as expected. This is done by overriding these methods to alter the context of the "inner" function as opposed to the ExtensibleFunction (or it's subclass') instance..bind() returns a new instance of the functions constructor (be it ExtensibleFunction or a subclass). It uses Object.assign() to ensure the properties stored on the bound function are consistent with those of the originating function.Symbol, which can be obfuscated by modules or an IIFE (or any other common technique of privatizing references).And without further ado, the code:
// The Symbol that becomes the key to the "inner" function const EFN_KEY = Symbol('ExtensibleFunctionKey'); // Here it is, the `ExtensibleFunction`!!! class ExtensibleFunction extends Function { // Just pass in your function. constructor (fn) { // This essentially calls Function() making this function look like: // `function (EFN_KEY, ...args) { return this[EFN_KEY](...args); }` // `EFN_KEY` is passed in because this function will escape the closure super('EFN_KEY, ...args','return this[EFN_KEY](...args)'); // Create a new function from `this` that binds to `this` as the context // and `EFN_KEY` as the first argument. let ret = Function.prototype.bind.apply(this, [this, EFN_KEY]); // For both the original and bound funcitons, we need to set the `[EFN_KEY]` // property to the "inner" function. This is done with a getter to avoid // potential overwrites/enumeration Object.defineProperty(this, EFN_KEY, {get: ()=>fn}); Object.defineProperty(ret, EFN_KEY, {get: ()=>fn}); // Return the bound function return ret; } // We'll make `bind()` work just like it does normally bind (...args) { // We don't want to bind `this` because `this` doesn't have the execution context // It's the "inner" function that has the execution context. let fn = this[EFN_KEY].bind(...args); // Now we want to return a new instance of `this.constructor` with the newly bound // "inner" function. We also use `Object.assign` so the instance properties of `this` // are copied to the bound function. return Object.assign(new this.constructor(fn), this); } // Pretty much the same as `bind()` apply (...args) { // Self explanatory return this[EFN_KEY].apply(...args); } // Definitely the same as `apply()` call (...args) { return this[EFN_KEY].call(...args); } } /** * Below is just a bunch of code that tests many scenarios. * If you run this snippet and check your console (provided all ES6 features * and console.table are available in your browser [Chrome, Firefox?, Edge?]) * you should get a fancy printout of the test results. */ // Just a couple constants so I don't have to type my strings out twice (or thrice). const CONSTRUCTED_PROPERTY_VALUE = `Hi, I'm a property set during construction`; const ADDITIONAL_PROPERTY_VALUE = `Hi, I'm a property added after construction`; // Lets extend our `ExtensibleFunction` into an `ExtendedFunction` class ExtendedFunction extends ExtensibleFunction { constructor (fn, ...args) { // Just use `super()` like any other class // You don't need to pass ...args here, but if you used them // in the super class, you might want to. super(fn, ...args); // Just use `this` like any other class. No more messing with fake return values! let [constructedPropertyValue, ...rest] = args; this.constructedProperty = constructedPropertyValue; } } // An instance of the extended function that can test both context and arguments // It would work with arrow functions as well, but that would make testing `this` impossible. // We pass in CONSTRUCTED_PROPERTY_VALUE just to prove that arguments can be passed // into the constructor and used as normal let fn = new ExtendedFunction(function (x) { // Add `this.y` to `x` // If either value isn't a number, coax it to one, else it's `0` return (this.y>>0) + (x>>0) }, CONSTRUCTED_PROPERTY_VALUE); // Add an additional property outside of the constructor // to see if it works as expected fn.additionalProperty = ADDITIONAL_PROPERTY_VALUE; // Queue up my tests in a handy array of functions // All of these should return true if it works let tests = [ ()=> fn instanceof Function, // true ()=> fn instanceof ExtensibleFunction, // true ()=> fn instanceof ExtendedFunction, // true ()=> fn.bind() instanceof Function, // true ()=> fn.bind() instanceof ExtensibleFunction, // true ()=> fn.bind() instanceof ExtendedFunction, // true ()=> fn.constructedProperty == CONSTRUCTED_PROPERTY_VALUE, // true ()=> fn.additionalProperty == ADDITIONAL_PROPERTY_VALUE, // true ()=> fn.constructor == ExtendedFunction, // true ()=> fn.constructedProperty == fn.bind().constructedProperty, // true ()=> fn.additionalProperty == fn.bind().additionalProperty, // true ()=> fn() == 0, // true ()=> fn(10) == 10, // true ()=> fn.apply({y:10}, [10]) == 20, // true ()=> fn.call({y:10}, 20) == 30, // true ()=> fn.bind({y:30})(10) == 40, // true ]; // Turn the tests / results into a printable object let table = tests.map((test)=>( {test: test+'', result: test()} )); // Print the test and result in a fancy table in the console. // F12 much? console.table(table);