This 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:
- When extending
ExtensibleFunction, the code is idiomatic of extending any ES6 class (no, mucking about with pretend constructors or proxies). - The prototype chain is retained through all subclasses, and
instanceof/.constructorreturn 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 theExtensibleFunction(or it's subclass') instance..bind()returns a new instance of the functions constructor (be itExtensibleFunctionor a subclass). It usesObject.assign()to ensure the properties stored on the bound function are consistent with those of the originating function.- Closures are honored, and arrow functions continue to maintain the proper context.
- The "inner" function is stored via a
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);