nodejs example es6 ejemplos ecmascript clases javascript function inheritance ecmascript-6 javascript-inheritance

javascript - example - ¿Cómo extender la función con clases de ES6?



prototype javascript ejemplos (8)

ES6 permite extender objetos especiales. Entonces es posible heredar de la función. Tal objeto se puede llamar como una función, pero ¿cómo puedo implementar la lógica para dicha llamada?

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

Cualquier método de clase obtiene referencia a la instancia de clase a través de this . Pero cuando se llama como una función, this refiere a la window . ¿Cómo puedo obtener la referencia a la instancia de clase cuando se llama como una función?

PD: misma pregunta en ruso.


Actualizar:

Desafortunadamente, esto no funciona porque ahora devuelve un objeto de función en lugar de una clase, por lo que parece que esto realmente no se puede hacer sin modificar el prototipo. Cojo.

Básicamente, el problema es que no hay forma de establecer this valor para el constructor de Function . La única forma de hacer esto realmente sería usar el método .bind después, sin embargo, esto no es muy amigable para la clase.

Podríamos hacer esto en una clase base auxiliar, sin embargo, this no estará disponible hasta después de la super llamada inicial, por lo que es un poco complicado.

Ejemplo de trabajo

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

(El ejemplo requiere un navegador moderno o node --harmony ).

Básicamente, la función base ClassFunction extiende envolverá la llamada al constructor de la Function con una función personalizada que es similar a .bind , pero que permite la vinculación posterior, en la primera llamada. Luego, en el ClassFunction constructor ClassFunction , llama a la función devuelta desde super que ahora es la función enlazada, pasando this para terminar de configurar la función de enlace personalizada.

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

Todo esto es bastante complicado, pero evita la mutación del prototipo, que se considera de mala forma por razones de optimización y puede generar advertencias en las consolas del navegador.


En primer lugar, llegué a una solución con arguments.callee , pero fue horrible.
Esperaba que se rompiera en modo estricto global, pero parece que funciona incluso allí.

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

Fue una mala manera debido al uso arguments.callee , pasando el código como una cadena y forzando su ejecución en modo no estricto. Pero apareció la idea de anular 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; } }

Y la prueba, que muestra que puedo ejecutar esto como función de diferentes maneras:

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"

Versión con

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

de hecho contiene funcionalidad de bind :

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

Versión con

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

hace que la call y la apply en la window inconsistentes:

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

entonces el cheque debe moverse a apply :

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


Esta es la solución que he desarrollado que satisface todas mis necesidades de ampliar funciones y me ha servido bastante bien. Los beneficios de esta técnica son:

  • Al extender ExtensibleFunction , el código es idiomático de extender cualquier clase ES6 (no, burlándose de constructores o servidores proxy).
  • La cadena del prototipo se retiene a través de todas las subclases, y la instanceof / .constructor devuelve los valores esperados.
  • .bind() .apply() y .call() funcionan como se esperaba. Esto se hace anulando estos métodos para alterar el contexto de la función "interna" en lugar de la instancia de ExtensibleFunction (o su subclase).
  • .bind() devuelve una nueva instancia del constructor de funciones (ya sea ExtensibleFunction o una subclase). Utiliza Object.assign() para garantizar que las propiedades almacenadas en la función enlazada sean coherentes con las de la función de origen.
  • Se cumplen los cierres y las funciones de flecha continúan manteniendo el contexto adecuado.
  • La función "interna" se almacena a través de un Symbol , que puede ofuscarse mediante módulos o un IIFE (o cualquier otra técnica común de privatización de referencias).

Y sin más preámbulos, el código:

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

Editar

Como estaba de humor, pensé que publicaría un paquete para esto en npm.


Este es mi enfoque para crear objetos invocables que hagan referencia correctamente a los miembros de sus objetos y mantengan la herencia correcta, sin meterse con prototipos.

Simplemente:

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

Extienda esta clase y agregue un método __call__ , más abajo ...

Una explicación en código y comentarios:

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

Ver en repl.it

Explicación adicional de bind :

function.bind() funciona de manera muy similar a function.call() , y comparten una firma de método similar:

fn.call(this, arg1, arg2, arg3, ...); más en mdn

fn.bind(this, arg1, arg2, arg3, ...); más en mdn

En ambos, el primer argumento redefine el contexto this dentro de la función. Los argumentos adicionales también pueden vincularse a un valor. Pero donde call llama inmediatamente a la función con los valores enlazados, bind devuelve un objeto de función "exótico" que envuelve de forma transparente el original, con this y cualquier argumento preestablecido.

Entonces, cuando define una función, bind algunos de sus argumentos:

var foo = function(a, b) { console.log(this); return a * b; } foo = foo.bind([''hello''], 2);

Llama a la función enlazada solo con los argumentos restantes, su contexto está preestablecido, en este caso a [''hello''] .

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


Hay una solución simple que aprovecha las capacidades funcionales de JavaScript: Pase la "lógica" como argumento de función al constructor de su clase, asigne los métodos de esa clase a esa función, luego devuelva esa función desde el constructor como resultado :

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

Lo anterior se probó en Node.js 8.

Un inconveniente del ejemplo anterior es que no admite métodos heredados de la cadena de superclase. Para admitir eso, simplemente reemplace "Object. GetOwnPropertyNames (...)" con algo que devuelva también los nombres de métodos heredados. Creo que cómo hacerlo se explica en alguna otra pregunta-respuesta en :-). Por cierto. Sería bueno si ES7 agregara un método para producir nombres de métodos heredados también ;-).

Si necesita admitir métodos heredados, una posibilidad es agregar un método estático a la clase anterior que devuelve todos los nombres de métodos heredados y locales. Entonces llama eso desde el constructor. Si luego extiende esa clase Funk, también obtendrá ese método estático heredado.


La super llamada invocará el constructor de Function , que espera una cadena de código. Si desea acceder a los datos de su instancia, simplemente puede codificarlos:

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

Pero eso no es realmente satisfactorio. Queremos usar un cierre.

Es posible que la función devuelta sea un cierre que puede acceder a las variables de su instancia , pero no es fácil. Lo bueno es que no tiene que llamar a super si no lo desea, aún puede return objetos arbitrarios de sus constructores de clase ES6. En este caso, haríamos

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

Pero podemos hacerlo aún mejor, y abstraer esto de 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; } }

Es cierto que esto crea un nivel adicional de indirección en la cadena de herencia, pero eso no es necesariamente algo malo (puede extenderlo en lugar de la Function nativa). Si quieres evitarlo, usa

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

pero observe que Smth no heredará dinámicamente las propiedades estáticas de la Function .


Puede envolver la instancia de Smth en un Proxy con una trampa de apply (y tal vez construct ):

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


Tomé el consejo de la respuesta de Bergi y lo envolví en un módulo 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 ]);