How to extend Function with ES6 classes?

人盡茶涼 提交于 2019-11-26 06:57:58
Bergi

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.

This is my 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.__call__(...args)');
    return this.bind(this);
  }

  // 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:

// A Class that extends Function so we can create
// objects that also behave like functions, i.e. callable objects.
class ExFunc extends Function {
  constructor() {
    // Here we create a dynamic function with `super`,
    // which calls the constructor of the parent class, `Function`.
    // The dynamic function simply passes any calls onto
    // an overridable object method which I named `__call__`.
    // But there is a problem, the dynamic function created from
    // the strings sent to `super` doesn't have any reference to `this`;
    // our new object. There are in fact two `this` objects; the outer
    // one being created by our class inside `constructor` and an inner
    // one created by `super` for the dynamic function.
    // So the reference to this in the text: `return this.__call__(...args)`
    // does not refer to `this` inside `constructor`.
    // So attempting:
    // `obj = new ExFunc();` 
    // `obj();`
    // Will throw an Error because __call__ doesn't exist to the dynamic function.
    super('...args', 'return this.__call__(...args)');
    
    // `bind` is the simple remedy to this reference problem.
    // Because the outer `this` is also a function we can call `bind` on it
    // and set a new inner `this` reference. So we bind the inner `this`
    // of our dynamic function to point to the outer `this` of our object.
    // Now our dynamic function can access all the members of our new object.
    // So attempting:
    // `obj = new Exfunc();` 
    // `obj();`
    // Will work.
    // We return the value returned by `bind`, which is our `this` callable object,
    // wrapped in a transparent "exotic" function object with its `this` context
    // bound to our new instance (outer `this`).
    // The workings of `bind` are further explained elsewhere in this post.
    return this.bind(this);
  }
  
  // An example property to demonstrate member access.
  get venture() {
    return 'Hank';
  }
  
  // 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 {
  get venture() {
    return 'Dean';
  }
  
  __call__(ans) {
    return [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) );  // [ 'Dean', 42 ]

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']`

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
Alexander O'Mara

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.

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 ]);

Firstly I came to solution with arguments.callee, but it was awful.
I expected it to break in global strict mode, but seems like it works even there.

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

(new Smth(90))()

It was a bad way because of using arguments.callee, passing the code as a string and forcing its execution in non-strict mode. But than idea to override apply appeared.

var global = (1,eval)("this");

class Smth extends Function {
  constructor(x) {
    super('return arguments.callee.apply(this, arguments)');
    this.x = x;
  }
  apply(me, [y]) {
    me = me !== global && me || this;
    return me.x + y;
  }
}

And the test, showing I'm able to run this as function in different ways:

var f = new Smth(100);

[
f instanceof Smth,
f(1),
f.call(f, 2),
f.apply(f, [3]),
f.call(null, 4),
f.apply(null, [5]),
Function.prototype.apply.call(f, f, [6]),
Function.prototype.apply.call(f, null, [7]),
f.bind(f)(8),
f.bind(null)(9),
(new Smth(200)).call(new Smth(300), 1),
(new Smth(200)).apply(new Smth(300), [2]),
isNaN(f.apply(window, [1])) === isNaN(f.call(window, 1)),
isNaN(f.apply(window, [1])) === isNaN(Function.prototype.apply.call(f, window, [1])),
] == "true,101,102,103,104,105,106,107,108,109,301,302,true,true"

Version with

super('return arguments.callee.apply(arguments.callee, arguments)');

in fact contains bind functionality:

(new Smth(200)).call(new Smth(300), 1) === 201

Version with

super('return arguments.callee.apply(this===(1,eval)("this") ? null : this, arguments)');
...
me = me || this;

makes call and apply on window inconsistent:

isNaN(f.apply(window, [1])) === isNaN(f.call(window, 1)),
isNaN(f.apply(window, [1])) === isNaN(Function.prototype.apply.call(f, window, [1])),

so the check should be moved into apply:

super('return arguments.callee.apply(this, arguments)');
...
me = me !== global && me || this;

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 / .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.
  • 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);

Edit

Since I was in the mood, I figured I'd publish a package for this on npm.

There is a simple solution which takes advantage of JavaScript's functional capabilities: Pass the "logic" as a function-argument to the constructor of your class, assign the methods of that class to that function, then return that function from the constructor as the result:

class Funk
{
    constructor (f)
    { let proto       = Funk.prototype;
      let methodNames = Object.getOwnPropertyNames (proto);
      methodNames.map (k => f[k] = this[k]);
      return f;
    }

    methodX () {return 3}
}

let myFunk  = new Funk (x => x + 1);
let two     = myFunk(1);         // == 2
let three   = myFunk.methodX();  // == 3

The above was tested on Node.js 8.

A shortcoming of the example above is it does not support methods inherited from the superclass-chain. To support that, simply replace "Object . getOwnPropertyNames(...)" with something that returns also the names of inherited methods. How to do that I believe is explained in some other question-answer on Stack Overflow :-). BTW. It would be nice if ES7 added a method to produce inherited methods' names as well ;-).

If you need to support inherited methods one possibility is adding a static method to the above class which returns all inherited and local method names. Then call that from the constructor. If you then extend that class Funk, you get that static method inherited along as well.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!