Come estendere la funzione con le classi ES6?

ES6 consente di estendere oggetti speciali. Quindi è ansible ereditare dalla funzione. Tale object può essere chiamato come una funzione, ma come posso implementare la logica per tale chiamata?

class Smth extends Function { constructor (x) { // What should be done here super(); } } (new Smth(256))() // to get 256 at this call? 

Qualsiasi metodo di class ottiene riferimento all’istanza della class tramite this . Ma quando viene chiamato come una funzione, this riferisce alla window . Come posso ottenere il riferimento all’istanza della class quando viene chiamata come funzione?

PS: stessa domanda in russo.

La super chiamata invocherà il costruttore Function , che si aspetta una stringa di codice. Se vuoi accedere ai tuoi dati di istanza, puoi semplicemente indicizzarli:

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

ma non è davvero soddisfacente. Vogliamo usare una chiusura.

Avere la funzione restituita è una chiusura che può accedere alle variabili di istanza è ansible, ma non è facile. La cosa buona è che non devi chiamare super se non vuoi – puoi ancora return oggetti arbitrari dai costruttori di class ES6. In questo caso, lo faremmo

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

Ma possiamo fare ancora meglio e astrarre questa cosa da 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 fx; }); // named function this.x = x; } } 

Certo, questo crea un ulteriore livello di riferimento indiretto nella catena ereditaria, ma non è necessariamente una cosa negativa (è ansible estenderla invece della Function nativa). Se vuoi evitarlo, usa

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

ma si noti che Smth non erediterà in modo dinamico le proprietà Function proprietà statiche.

Questo è il mio approccio alla creazione di oggetti chiamabili che fanno riferimento correttamente ai membri degli oggetti e mantengono l’ereditarietà corretta, senza scherzare con i prototipi.

Semplicemente:

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

Estendi questa class e aggiungi un metodo __call__ , più sotto …

Una spiegazione nel codice e nei commenti:

 // A Class that extends Function so we can create // objects that also behave like functions, ie 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 subclasss 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 ] 

Puoi racchiudere l’istanza Smth in un Proxy con una trappola apply (e forse construct ):

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

Aggiornare:

Sfortunatamente questo non funziona, perché ora restituisce un object funzione invece di una class, quindi sembra che in realtà questo non possa essere fatto senza modificare il prototipo. Noioso.


Fondamentalmente il problema è che non c’è modo di impostare this valore per il costruttore Function . L’unico modo per farlo davvero sarebbe utilizzare il metodo .bind seguito, tuttavia questo non è molto adatto per la Classe.

Potremmo farlo in una class base helper, tuttavia this non è disponibile fino a dopo la super chiamata iniziale, quindi è un po ‘complicato.

Esempio di lavoro:

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

Ho preso il consiglio dalla risposta di Bergi e l’ho avvolto in un modulo 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 ]); 

Per prima cosa sono venuto a una soluzione con arguments.callee , ma è stato terribile.
Mi aspettavo che si rompesse in modalità rigorosa globale, ma sembra che funzioni anche lì.

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

È stato un brutto modo per usare arguments.callee , passare il codice come una stringa e forzarne l’esecuzione in modalità non rigida. Ma l’idea di apply override è apparsa.

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

E il test, mostrando che sono in grado di eseguire questo come funzione in diversi modi:

 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" 

Versione con

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

infatti contiene funzionalità di bind :

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

Versione con

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

effettua call e apply su window incoerente:

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

quindi il controllo dovrebbe essere spostato in apply :

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

Questa è la soluzione che ho risolto e che soddisfa tutte le mie esigenze di estendere le funzioni e mi è servita abbastanza bene. I benefici di questa tecnica sono:

  • Quando si estende ExtensibleFunction , il codice è idiomatico di estendere qualsiasi class ES6 (no, ingannare con i costruttori o i proxy fittizi).
  • La catena di prototipi viene mantenuta attraverso tutte le sottoclassi e instanceof / .constructor restituisce i valori previsti.
  • .bind() .apply() e .call() tutte funzionano come previsto. Ciò avviene sovrascrivendo questi metodi per modificare il contesto della funzione “interiore” in opposizione all’istanza di ExtensibleFunction (o di sua sottoclass).
  • .bind() restituisce una nuova istanza del costruttore di funzioni (sia ExtensibleFunction o una sottoclass). Utilizza Object.assign() per garantire che le proprietà memorizzate sulla funzione associata siano coerenti con quelle della funzione di origine.
  • Le chiusure sono onorato e le funzioni delle frecce continuano a mantenere il contesto appropriato.
  • La funzione “interiore” è memorizzata tramite un Symbol , che può essere offuscato da moduli o un IIFE (o qualsiasi altra tecnica comune di riferimento di privatizzazione).

E senza ulteriori indugi, il codice:

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

C’è una soluzione semplice che sfrutta le funzionalità funzionali di JavaScript: passare la “logica” come argomento di funzione al costruttore della class, assegnare i metodi di quella class a quella funzione, quindi restituire quella funzione dal costruttore come risultato :

 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 

Quanto sopra è stato testato su Node.js 8.

Un difetto dell’esempio sopra riportato non supporta i metodi ereditati dalla catena della superclass. Per supportarlo, è sufficiente sostituire “Object. GetOwnPropertyNames (…)” con qualcosa che restituisce anche i nomi dei metodi ereditati. Come farlo credo sia spiegato in qualche altra domanda-risposta su Stack Overflow :-). BTW. Sarebbe bello se ES7 aggiungesse un metodo per produrre anche i nomi dei metodi ereditati ;-).

Se è necessario supportare metodi ereditati, una possibilità è l’aggiunta di un metodo statico alla class precedente che restituisce tutti i nomi dei metodi ereditati e locali. Quindi chiama questo dal costruttore. Se estendi il Funk di quella class, ottieni anche quel metodo statico ereditato.