web-dev-qa-db-fra.com

Comment étendre Function avec les classes ES6?

ES6 permet d'étendre des objets spéciaux. Il est donc possible d'hériter de la fonction. Un tel objet peut être appelé en tant que fonction, mais comment puis-je implémenter la logique pour un tel appel?

class Smth extends Function {
  constructor (x) {
    // What should be done here
    super();
  }
}

(new Smth(256))() // to get 256 at this call?

Toute méthode de classe obtient une référence à l'instance de classe via this. Mais lorsqu'il est appelé en tant que fonction, this fait référence à window. Comment puis-je obtenir la référence à l'instance de classe lorsqu'elle est appelée en tant que fonction?

PS: Même question en russe.

89
Qwertiy

L'appel super appellera le constructeur Function, qui attend une chaîne de code. Si vous souhaitez accéder à vos données d'instance, vous pouvez simplement le coder en dur:

class Smth extends Function {
  constructor(x) {
    super("return "+JSON.stringify(x)+";");
  }
}

mais ce n'est pas vraiment satisfaisant. Nous voulons utiliser une fermeture.

Avoir la fonction retournée est une fermeture qui peut accéder à votre instance variables est possible, mais pas facile. La bonne chose est que vous n'avez pas à appeler super si vous ne le souhaitez pas, vous pouvez toujours utiliser return des objets arbitraires à partir de vos constructeurs de classe ES6. Dans ce cas, nous ferions

class Smth extends Function {
  constructor(x) {
    // refer to `smth` instead of `this`
    function smth() { return x; };
    Object.setPrototypeOf(smth, Smth.prototype);
    return smth;
  }
}

Mais nous pouvons faire encore mieux, et résumer cette chose en 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;
  }
}

Certes, cela crée un niveau supplémentaire d'indirection dans la chaîne d'héritage, mais ce n'est pas nécessairement une mauvaise chose (vous pouvez l'étendre à la place du Function natif). Si vous voulez l'éviter, utilisez

function ExtensibleFunction(f) {
  return Object.setPrototypeOf(f, new.target.prototype);
}
ExtensibleFunction.prototype = Function.prototype;

mais notez que Smth n'héritera pas dynamiquement des propriétés statiques Function.

42
Bergi

C’est mon approche de la création d’objets appelables qui référencent correctement leurs membres et maintiennent un héritage correct, sans nuire aux prototypes.

Simplement:

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

Étendez cette classe et ajoutez une méthode __call__, Plus ci-dessous ...

Une explication dans le code et les commentaires:

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

Voir sur repl.it

Explication supplémentaire de bind:

function.bind() fonctionne beaucoup comme function.call(), et ils partagent une signature de méthode similaire:

fn.call(this, arg1, arg2, arg3, ...); plus sur mdn

fn.bind(this, arg1, arg2, arg3, ...); plus sur mdn

Dans les deux cas, le premier argument redéfinit le contexte this à l'intérieur de la fonction. Des arguments supplémentaires peuvent également être liés à une valeur. Mais lorsque call appelle immédiatement la fonction avec les valeurs liées, bind renvoie un objet de fonction "exotique" qui enveloppe de manière transparente l'original, avec this et tous les arguments prédéfinis.

Donc, lorsque vous définissez une fonction, alors bind certains de ses arguments:

var foo = function(a, b) {
  console.log(this);
  return a * b;
}

foo = foo.bind(['hello'], 2);

Vous appelez la fonction liée avec uniquement les arguments restants, son contexte est prédéfini, dans ce cas à ['hello'].

// We pass in arg `b` only because arg `a` is already set.
foo(2);  // returns 4, logs `['hello']`
22
Adrien

Vous pouvez envelopper l'instance Smth dans un proxy avec un piège apply (et peut-être construct ):

class Smth extends Function {
  constructor (x) {
    super();
    return new Proxy(this, {
      apply: function(target, thisArg, argumentsList) {
        return x;
      }
    });
  }
}
new Smth(256)(); // 256
18
Oriol

Mise à jour:

Malheureusement, cela ne fonctionne pas correctement car il retourne maintenant un objet fonction au lieu d'une classe. Il semble donc que cela ne peut pas être fait sans modifier le prototype. Boiteux.


Le problème est qu’il n’ya aucun moyen de définir la valeur this du constructeur Function. La seule façon de le faire serait d’utiliser le .bind méthode par la suite, mais ce n’est pas très favorable à la classe.

Nous pourrions faire cela dans une classe de base d'assistance, cependant, this ne sera disponible qu'après l'appel initial super, c'est donc un peu délicat.

Exemple de travail:

'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))());

(Exemple nécessite un navigateur moderne ou node --harmony.)

Fondamentalement, la fonction de base ClassFunction extend encapsule l'appel de constructeur Function avec une fonction personnalisée similaire à .bind, mais autorise la liaison ultérieurement, lors du premier appel. Puis, dans le constructeur lui-même ClassFunction, il appelle la fonction renvoyée de super, qui est maintenant la fonction liée, en transmettant this pour terminer la configuration de la fonction de liaison personnalisée.

(super(...))(this);

Tout cela est un peu compliqué, mais cela évite de muter le prototype, considéré comme une mauvaise forme pour des raisons d'optimisation et pouvant générer des avertissements dans les consoles de navigateur.

3
Alexander O'Mara

J'ai suivi les conseils de la réponse de Bergi et les ai intégrés dans un module NPM .

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 ]);
2
Ryan Patterson

C’est la solution que j’ai élaborée qui répond à tous mes besoins d’extension de fonctions et m’a très bien servi. Les avantages de cette technique sont:

  • Lors de l'extension de ExtensibleFunction, le code est idiomatique de l'extension de toute classe ES6 (non, bidouiller avec des constructeurs simulés ou des mandataires).
  • La chaîne de prototypes est conservée dans toutes les sous-classes et instanceof/.constructor Renvoient les valeurs attendues.
  • .bind().apply() et .call() fonctionnent comme prévu. Ceci est fait en surchargeant ces méthodes pour modifier le contexte de la fonction "interne" par opposition à l'instance ExtensibleFunction (ou sa sous-classe ').
  • .bind() renvoie une nouvelle instance du constructeur de fonctions (que ce soit ExtensibleFunction ou une sous-classe). Il utilise Object.assign() pour s'assurer que les propriétés stockées dans la fonction liée sont cohérentes avec celles de la fonction d'origine.
  • Les fermetures sont respectées et les fonctions de flèche continuent de conserver le contexte approprié.
  • La fonction "interne" est stockée via un Symbol, qui peut être masqué par des modules ou un IIFE (ou toute autre technique courante de privatisation des références).

Et sans plus tarder, le 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);

Modifier

Depuis que j'étais d'humeur d'humeur, j'ai pensé que je publierais un paquet pour cela sur npm.

1
Aaron Levine

Tout d'abord je suis venu à la solution avec arguments.callee, mais c'était affreux.
Je m'attendais à ce qu'il casse en mode global strict, mais il semble que cela fonctionne même là.

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

(new Smth(90))()

C'était une mauvaise façon d'utiliser arguments.callee, en passant le code sous forme de chaîne et en forçant son exécution en mode non strict. Mais cette idée de remplacer apply est apparue.

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;
  }
}

Et le test, montrant que je suis capable d’exécuter cette fonction de différentes manières:

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 avec

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

contient en fait la fonctionnalité bind:

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

Version avec

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

rend call et apply sur window incohérent:

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

le chèque doit donc être déplacé dans apply:

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

Il existe une solution simple qui tire parti des fonctionnalités de JavaScript: passez la "logique" sous forme d'argument de fonction au constructeur de votre classe, affectez les méthodes de cette classe à cette fonction, puis retourne cette fonction = du constructeur comme résultat:

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

Ce qui précède a été testé sur Node.js 8.

L’inconvénient de l’exemple ci-dessus est qu’il ne prend pas en charge les méthodes héritées de la superclass-chain. Pour supporter cela, remplacez simplement "Object. GetOwnPropertyNames (...)" par quelque chose qui retourne également les noms des méthodes héritées. Je pense que la façon de faire est expliquée dans une autre question-réponse sur Stack Overflow :-). BTW. Ce serait bien si ES7 ajoutait une méthode pour produire également les noms des méthodes héritées ;-).

Si vous devez prendre en charge les méthodes héritées, une possibilité consiste à ajouter une méthode statique à la classe ci-dessus, laquelle renvoie tous les noms de méthodes héritées et locales. Appelez ensuite cela depuis le constructeur. Si vous étendez ensuite cette classe Funk, vous héritez également de cette méthode statique.

0
Panu Logic