124 votes

L’extension de fonction avec ES6 classes ?

ES6 permet de prolonger des objets spéciaux. Il est donc possible d'hériter de la fonction. Un tel objet peut être appelée comme une fonction, mais comment puis-je mettre en œuvre la logique de ces appels?

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

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

N'importe quelle méthode de la classe devient la référence à l'instance de classe via this. Mais quand elle est appelée comme une fonction, this désigne window. Comment puis-je obtenir la référence à l'instance de la classe quand elle est appelée comme une fonction?

PS: Même question en russe.

63voto

Bergi Points 104242

L' super appel d'invoquer l' Function constructeur, qui attend une chaîne de code. Si vous souhaitez accéder à vos données d'instance, vous pouvez simplement 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.

Ayant la fonction renvoyée être 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 voulez pas, vous pouvez return objets arbitraires à partir de votre ES6 les constructeurs de classe. Dans ce cas, nous aimerions le faire

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ésumé cette chose hors de l' 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 de native Function). Si vous voulez l'éviter, utiliser

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

mais remarquez que Smth ne sera pas dynamiquement hériter statique Function propriétés.

38voto

Adrien Points 369

C'est mon approche de la création de appelable objets correctement référence à leurs membres de l'objet, et de maintenir le bon niveau de l'héritage, sans vous embêter avec des prototypes.

Il vous suffit de:

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

Étendre cette classe et ajouter un __call__ méthode, plus bas...

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 ]

Vue sur repl.il

D'autres explications bind:

function.bind() fonctionne comme l' function.call(), et ils partagent une même signature de la méthode:

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

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

Dans le premier argument redéfinit l' this contexte à l'intérieur de la fonction. Des arguments supplémentaires peuvent également être lié à une valeur. Mais où call immédiatement les appels de la fonction avec les valeurs soumises, bind renvoie un "exotique" de la fonction de l'objet que de manière transparente encapsule l'original, this et les arguments de préréglage.

Ainsi, lorsque vous définissez une fonction d' bind certains de ses arguments:

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

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

Vous appelez la limite de la fonction avec seulement 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']`

23voto

Oriol Points 20803

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
 

4voto

Alexander O'Mara Points 23319

Mise à jour:

Malheureusement, ce n'est pas tout à fait parce que c'est maintenant retourner un objet de fonction au lieu d'une classe, donc il semble que cela ne peut se faire sans modifier le prototype. Boiteux.


Fondamentalement, le problème est qu'il n'ya aucun moyen de réglage de l' this de la valeur pour l' Function constructeur. La seule façon de vraiment faire ce qui serait d'utiliser l' .bind méthode par la suite, mais ce n'est pas très Classe de l'environnement.

Nous pourrions le faire dans un accompagnateur de la classe de base, toutefois this ne sont pas disponible jusqu'à ce que après la première super appel, donc c'est un peu délicat.

Exemple:

'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 navigateur moderne ou node --harmony.)

Fondamentalement, la fonction de base ClassFunction s'étend clôturera l' Function appel du constructeur avec une fonction personnalisée qui est similaire à l' .bind, mais permet de lier plus tard, sur le premier appel. Puis dans l' ClassFunction constructeur lui-même, il appelle la fonction renvoyée à partir de super qui est maintenant lié à la fonction, en passant this pour terminer la configuration de la coutume fonction de liaison.

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

C'est tout à fait un peu compliquée, mais elle n'exige pas de mutation du prototype, qui est considéré comme mauvais-forme pour des raisons d'optimisation, et peut générer des avertissements dans le navigateur consoles.

3voto

Ryan Patterson Points 156

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X