Wie kann man die Funktion mit ES6-Klassen erweitern?

105

Mit ES6 können spezielle Objekte erweitert werden. Es ist also möglich, von der Funktion zu erben. Ein solches Objekt kann als Funktion aufgerufen werden, aber wie kann ich die Logik für einen solchen Aufruf implementieren?

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

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

Jede Klassenmethode erhält über einen Verweis auf die Klasseninstanz this. Aber wenn es als Funktion aufgerufen wird, thisbezieht sich auf window. Wie kann ich den Verweis auf die Klasseninstanz erhalten, wenn sie als Funktion aufgerufen wird?

PS: Gleiche Frage auf Russisch.

Qwertiy
quelle
17
Ah, endlich hat jemand diese Frage gestellt :-)
Bergi
1
Einfach tun super(x)(dh weitergeben an Function)? Ich bin mir nicht sicher, ob Functiones tatsächlich erweitert werden kann.
Felix Kling
Beachten Sie, dass es immer noch Probleme beim Erweitern integrierter Klassen gibt. Die Spezifikation legt nahe, dass dies möglich sein sollte, aber ich habe Errorunter anderem Probleme bei der Erweiterung .
ssube
1
Denken Sie daran, dass dies Functioneinfach ein Funktionskonstruktor ist. Die Implementierung der Funktion muss an den Konstruktor übergeben werden. Wenn Sie Smtheine Implementierung nicht akzeptieren möchten , müssen Sie sie im Konstruktor bereitstellen, d super('function implementation here'). H.
Felix Kling
1
@Qwertiy: Ich würde argumentieren, dass dies die Ausnahme ist , nicht der allgemeine Fall. Dies ist auch sehr spezifisch für Funktionsausdrücke , aber Sie verwenden den FunctionKonstruktor (Laufzeit), der sich stark von einem Funktionsausdruck (Syntax) unterscheidet.
Felix Kling

Antworten:

49

Der superAufruf ruft den FunctionKonstruktor auf, der eine Codezeichenfolge erwartet. Wenn Sie auf Ihre Instanzdaten zugreifen möchten, können Sie diese einfach fest codieren:

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

aber das ist nicht wirklich befriedigend. Wir wollen einen Verschluss verwenden.

Die zurückgegebene Funktion ist ein Abschluss, der auf Ihre Instanzvariablen zugreifen kann , ist möglich, aber nicht einfach. Das Gute ist, dass Sie nicht aufrufen müssen, superwenn Sie nicht möchten - Sie können immer noch returnbeliebige Objekte aus Ihren ES6-Klassenkonstruktoren. In diesem Fall würden wir tun

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

Aber wir können es noch besser machen und dieses Ding abstrahieren aus 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;
  }
}

Dies schafft zwar eine zusätzliche Indirektionsebene in der Vererbungskette, aber das ist nicht unbedingt eine schlechte Sache (Sie können sie anstelle der nativen erweitern Function). Wenn Sie es vermeiden möchten, verwenden Sie

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

Beachten Sie jedoch, dass Smthstatische FunctionEigenschaften nicht dynamisch geerbt werden.

Bergi
quelle
Ich möchte über die Funktion Zugriff auf den Klassenstatus erhalten.
Qwertiy
2
@Qwertiy: Dann benutze Bergis zweiten Vorschlag.
Felix Kling
@ AlexanderO'Mara: Sie kommen nicht darum herum, den Prototyp der Funktion zu mutieren, wenn Sie möchten, dass Ihre SmthInstanzen sind instanceof Smth(wie jeder erwarten würde). Sie können den Object.setPrototypeOfAufruf weglassen , wenn Sie diese oder eine Ihrer in Ihrer Klasse deklarierten Prototypmethoden nicht benötigen.
Bergi
@ AlexanderO'Mara: Auch Object.setPrototypeOfist kein so großes Optimierungsrisiko, solange es direkt nach dem Erstellen des Objekts durchgeführt wird. Nur wenn Sie den Prototyp eines Objekts während seiner Lebensdauer hin und her mutieren, wird es schlecht.
Bergi
1
@amn Nein, tun Sie nicht, wenn Sie nicht verwenden , thisund returnein Objekt.
Bergi
32

Dies ist ein Ansatz zum Erstellen aufrufbarer Objekte, die korrekt auf ihre Objektmitglieder verweisen und die korrekte Vererbung beibehalten, ohne mit Prototypen herumzuspielen.

Einfach:

class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)')
    var self = this.bind(this)
    this.__self__ = self
    return self
  }

  // Example `__call__` method.
  __call__(a, b, c) {
    return [a, b, c];
  }
}

Erweitern Sie diese Klasse und fügen Sie eine __call__Methode hinzu, mehr unten ...

Eine Erklärung in Code und Kommentaren:

// This is an approach to creating callable objects
// that correctly reference their own object and object members,
// without messing with prototypes.

// A Class that extends Function so we can create
// objects that also behave like functions, i.e. callable objects.
class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)');
    // Here we create a function dynamically using `super`, which calls
    // the `Function` constructor which we are inheriting from. Our aim is to create
    // a `Function` object that, when called, will pass the call along to an internal
    // method `__call__`, to appear as though the object is callable. Our problem is
    // that the code inside our function can't find the `__call__` method, because it
    // has no reference to itself, the `this` object we just created.
    // The `this` reference inside a function is called its context. We need to give
    // our new `Function` object a `this` context of itself, so that it can access
    // the `__call__` method and any other properties/methods attached to it.
    // We can do this with `bind`:
    var self = this.bind(this);
    // We've wrapped our function object `this` in a bound function object, that
    // provides a fixed context to the function, in this case itself.
    this.__self__ = self;
    // Now we have a new wrinkle, our function has a context of our `this` object but
    // we are going to return the bound function from our constructor instead of the
    // original `this`, so that it is callable. But the bound function is a wrapper
    // around our original `this`, so anything we add to it won't be seen by the
    // code running inside our function. An easy fix is to add a reference to the
    // new `this` stored in `self` to the old `this` as `__self__`. Now our functions
    // context can find the bound version of itself by following `this.__self__`.
    self.person = 'Hank'
    return self;
  }
  
  // An example property to demonstrate member access.
  get venture() {
    return this.person;
  }
  
  // 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 {
  constructor() {
    super()
    this.a = 'a1'
    this.b = 'b2'
    this.person = 'Dean'
  }

  ab() {
    return this.a + this.b
  }
  
  __call__(ans) {
    return [this.ab(), 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) );  // [ 'a1b2', Dean', 42 ]

// Test property and method access
console.log(callable2.a, callable2.b, callable2.ab())

Ansicht auf repl.it

Weitere Erklärung von bind:

function.bind()funktioniert ähnlich function.call()und sie haben eine ähnliche Methodensignatur:

fn.call(this, arg1, arg2, arg3, ...);mehr auf mdn

fn.bind(this, arg1, arg2, arg3, ...);mehr auf mdn

In beiden Fällen definiert das erste Argument den thisKontext innerhalb der Funktion neu. Zusätzliche Argumente können auch an einen Wert gebunden werden. Aber wo callsofort die Funktion mit dem gebundenen Wert nennt, bindgibt ein „exotisches“ Funktion Objekt , das transparent das Original Wraps, mit thisund alle Argumente vorgegeben.

Wenn Sie also eine Funktion definieren, dann bindeinige ihrer Argumente:

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

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

Sie rufen die gebundene Funktion nur mit den verbleibenden Argumenten auf, ihr Kontext ist voreingestellt, in diesem Fall auf ['hello'].

// We pass in arg `b` only because arg `a` is already set.
foo(2);  // returns 4, logs `['hello']`
Adrien
quelle
Können Sie bitte eine Erklärung hinzufügen, warum dies bindfunktioniert (dh warum eine Instanz von zurückgegeben wird ExFunc)?
Bergi
@Bergi bindgibt ein transparentes Funktionsobjekt zurück, das das aufgerufene Funktionsobjekt, das unser aufrufbares Objekt ist, nur mit dem thisKontext-Rebound umschließt. Es wird also wirklich eine transparent verpackte Instanz von zurückgegeben ExFunc. Beitrag aktualisiert mit mehr Infos zu bind.
Adrien
1
@Bergi Alle Getter / Setter und Methoden sind zugänglich, Eigenschaften / Attribute müssen im constructorAfter- bindIn zugewiesen werden ExFunc. In Unterklassen von ExFunc sind alle Mitglieder zugänglich. Wie für instanceof; in es6 werden gebundene Funktionen als exotisch bezeichnet, so dass ihr inneres Funktionieren nicht ersichtlich ist, aber ich denke, es leitet den Aufruf über an sein verpacktes Ziel weiter Symbol.hasInstance. Es ähnelt einem Proxy, ist jedoch eine einfache Methode, um den gewünschten Effekt zu erzielen. Ihre Unterschrift ist ähnlich, nicht gleich.
Adrien
1
@Adrien aber von innen __call__kann ich nicht zugreifen this.aoder this.ab(). zB repl.it/repls/FelineFinishedDesktopenvironment
rob
1
@rob gut entdeckt, es gibt einen Referenzfehler, ich habe die Antwort und den Code mit einem Fix und einer neuen Erklärung aktualisiert.
Adrien
20

Sie können die Smth-Instanz in einen Proxy mit einer apply(und möglicherweise construct) Falle einschließen :

class Smth extends Function {
  constructor (x) {
    super();
    return new Proxy(this, {
      apply: function(target, thisArg, argumentsList) {
        return x;
      }
    });
  }
}
new Smth(256)(); // 256
Oriol
quelle
Tolle Idee. So was. Sollte ich mehr Logik implementieren, anstatt sie in apply einzufügen?
Qwertiy
4
Ein Proxy würde ziemlich viel Aufwand verursachen, nicht wahr? Auch thisist noch eine leere Funktion (prüfen new Smth().toString()).
Bergi
2
@Bergi Keine Ahnung von Leistung. MDN hat eine große rote Fettdruckwarnung setPrototypeOfund sagt nichts über Proxys aus. Aber ich denke, Proxys können genauso problematisch sein wie setPrototypeOf. Und ungefähr toStringkann es mit einer benutzerdefinierten Methode in beschattet werden Smth.prototype. Die native ist sowieso implementierungsabhängig.
Oriol
@Qwertiy Sie können eine constructFalle hinzufügen , um das Verhalten von anzugeben new new Smth(256)(). Und fügen Sie benutzerdefinierte Methoden hinzu, die die nativen Methoden beschatten, die auf den Code einer Funktion zugreifen, wie toStringBergi bemerkte.
Oriol
Ich erwähne, dass Ihre applyMethode so implementiert ist, wie sie verwendet werden soll, oder nur eine Demonstration, und ich muss mehr Informationen darüber durchsehen Proxyund Reflectsie ordnungsgemäß verwenden.
Qwertiy
3

Ich nahm den Rat von Bergis Antwort und wickelte ihn in ein NPM-Modul ein .

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

Aktualisieren:

Leider funktioniert dies nicht ganz, da jetzt ein Funktionsobjekt anstelle einer Klasse zurückgegeben wird. Es scheint also, dass dies tatsächlich nicht möglich ist, ohne den Prototyp zu ändern. Lame.


Grundsätzlich besteht das Problem darin, dass es keine Möglichkeit gibt, den thisWert für den FunctionKonstruktor festzulegen. Der einzige Weg, dies wirklich zu tun, wäre, die .bindMethode danach zu verwenden, dies ist jedoch nicht sehr klassenfreundlich.

Wir könnten dies in einer Helfer-Basisklasse tun, werden jedoch thiserst nach dem ersten superAufruf verfügbar , daher ist es etwas schwierig.

Arbeitsbeispiel:

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

(Beispiel erfordert modernen Browser oder node --harmony.)

Grundsätzlich ClassFunctionwird der FunctionKonstruktoraufruf durch die erweiterte Basisfunktion mit einer benutzerdefinierten Funktion umschlossen .bind, die der Bindung beim ersten Aufruf ähnelt , diese jedoch später ermöglicht. Dann ClassFunctionruft es im Konstruktor selbst die zurückgegebene Funktion auf, von superder jetzt die gebundene Funktion ist, und übergibt thisdie Einrichtung der benutzerdefinierten Bindungsfunktion.

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

Dies ist alles ziemlich kompliziert, vermeidet jedoch das Mutieren des Prototyps, der aus Optimierungsgründen als fehlerhaft angesehen wird und Warnungen in Browserkonsolen generieren kann.

Alexander O'Mara
quelle
1
Sie sind zu kompliziert. boundverweist auf die Funktion, die Sie returnaus dieser anonymen Klasse. Nennen Sie es einfach und beziehen Sie sich direkt darauf. Ich würde auch empfehlen, die Weitergabe von Code-Strings zu vermeiden, da dies nur ein Chaos ist (in jedem Schritt des Entwicklungsprozesses).
Bergi
Das extendsscheint nicht wirklich wie erwartet zu funktionieren Function.isPrototypeOf(Smth)und ist auch new Smth instanceof Functionfalsch.
Bergi
@Bergi Welche JS-Engine verwenden Sie? console.log((new Smth) instanceof Function);ist truefür mich in Node v5.11.0 und dem neuesten Firefox.
Alexander O'Mara
Ups, falsches Beispiel. Es ist das new Smth instanceof Smth, was mit Ihrer Lösung nicht funktioniert. Es sind auch keine Methoden für SmthIhre Instanzen verfügbar - da Sie nur einen Standard zurückgeben Function, keinen Smth.
Bergi
1
@Bergi Verdammt, sieht so aus, als hättest du recht. Das Erweitern nativer Typen scheint jedoch das gleiche Problem zu haben. extend Functionmacht auch new Smth instanceof Smthfalsch.
Alexander O'Mara
1

Zuerst kam ich zur Lösung mit arguments.callee, aber es war schrecklich.
Ich habe erwartet, dass es im globalen strengen Modus kaputt geht, aber es scheint sogar dort zu funktionieren.

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

(new Smth(90))()

Es war ein schlechter Weg, weil arguments.calleeder Code verwendet, als Zeichenfolge übergeben und seine Ausführung im nicht strengen Modus erzwungen wurde. Aber dann erschien die Idee zum Überschreiben apply.

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

Und der Test zeigt, dass ich dies als Funktion auf verschiedene Arten ausführen kann:

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 mit

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

in der Tat enthält bindFunktionalität:

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

Version mit

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

macht callund applyauf windowinkonsistent:

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

Daher sollte der Scheck verschoben werden in apply:

super('return arguments.callee.apply(this, arguments)');
...
me = me !== global && me || this;
Qwertiy
quelle
1
Was versuchst du eigentlich zu tun?
Vielen Dank, dass Sie
2
Ich denke, Klassen sind immer im strengen Modus: stackoverflow.com/questions/29283935/…
Alexander O'Mara
@ AlexanderO'Mara ist übrigens ein thisFenster, nicht undefiniert, daher befindet sich die erstellte Funktion nicht im strengen Modus (zumindest in Chrome).
Qwertiy
Bitte hören Sie auf, diese Antwort herunterzuschreiben. Ich habe bereits geschrieben, dass es ein schlechter Weg ist. Aber es ist wirklich eine Antwort - es funktioniert sowohl in FF als auch in Chrome (Edge muss nicht überprüft werden).
Qwertiy
Ich vermute, das funktioniert, weil Functiones nicht im strengen Modus ist. Obwohl schrecklich, ist es interessant +1. Sie würden wahrscheinlich nicht weiter eine Kette laufen können.
Alexander O'Mara
1

Dies ist die Lösung, die ich ausgearbeitet habe und die alle meine Anforderungen an die Erweiterung von Funktionen erfüllt und die mir recht gut gedient hat. Die Vorteile dieser Technik sind:

  • Beim Erweitern ExtensibleFunctionist der Code idiomatisch für das Erweitern einer ES6-Klasse (nein, mit vorgetäuschten Konstruktoren oder Proxys herumspielen).
  • Die Prototypkette wird in allen Unterklassen beibehalten und instanceof/ oder gibt .constructordie erwarteten Werte zurück.
  • .bind() .apply()und .call()alle funktionieren wie erwartet. Dies erfolgt durch Überschreiben dieser Methoden, um den Kontext der "inneren" Funktion im Gegensatz zu der zu ändernExtensibleFunction Instanz (oder ihrer Unterklasse) .
  • .bind()Gibt eine neue Instanz des Funktionskonstruktors zurück (sei es ExtensibleFunctionoder eine Unterklasse). Es wird verwendet, Object.assign()um sicherzustellen, dass die in der gebundenen Funktion gespeicherten Eigenschaften mit denen der Ursprungsfunktion übereinstimmen.
  • Abschlüsse werden berücksichtigt, und die Pfeilfunktionen behalten weiterhin den richtigen Kontext bei.
  • Die "innere" Funktion wird über a gespeichert Symbol, was durch Module oder ein IIFE (oder eine andere übliche Technik zur Privatisierung von Referenzen) verschleiert werden kann.

Und ohne weiteres der 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);

Bearbeiten

Da ich in der Stimmung war, dachte ich , ich würde ein Paket dafür auf npm veröffentlichen.

Aaron Levine
quelle
1

Es gibt eine einfache Lösung , die die Vorteile von JavaScript funktionalen Fähigkeiten nimmt: Übergeben Sie die „Logik“ als Funktion-Argument an den Konstruktor Ihrer Klasse, ordnen Sie die Methoden dieser Klasse auf diese Funktion, dann bringe diese Funktion aus dem Konstruktor als Ergebnis ::

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

Das Obige wurde auf Node.js 8 getestet.

Ein Nachteil des obigen Beispiels ist, dass es keine Methoden unterstützt, die von der Oberklassenkette geerbt wurden. Um dies zu unterstützen, ersetzen Sie einfach "Object. GetOwnPropertyNames (...)" durch etwas, das auch die Namen der geerbten Methoden zurückgibt. Wie das geht, wird meiner Meinung nach in einer anderen Frage-Antwort zu Stack Overflow erklärt :-). Übrigens. Es wäre schön, wenn ES7 eine Methode hinzufügen würde, um auch die Namen geerbter Methoden zu erzeugen ;-).

Wenn Sie geerbte Methoden unterstützen müssen, besteht eine Möglichkeit darin, der obigen Klasse eine statische Methode hinzuzufügen, die alle geerbten und lokalen Methodennamen zurückgibt. Rufen Sie das dann vom Konstruktor aus auf. Wenn Sie dann diese Klasse Funk erweitern, wird diese statische Methode ebenfalls vererbt.

Panu Logic
quelle
Ich denke, dieses Beispiel gibt eine einfache Antwort auf die ursprüngliche Frage "... wie kann ich die Logik für einen solchen Aufruf implementieren". Übergeben Sie es einfach als funktionswertiges Argument an den Konstruktor. Im obigen Code erweitert die Klasse Funk Function nicht explizit, obwohl dies möglich ist, sondern nicht unbedingt erforderlich ist. Wie Sie sehen können, können Sie die "Instanzen" wie bei jeder normalen Funktion aufrufen.
Panu Logic