JavaScript entretenido: sin llaves

imagen


JavaScript siempre me sorprendió, en primer lugar, porque probablemente como ningún otro lenguaje extendido admite ambos paradigmas al mismo tiempo: programación normal y anormal. Y si se ha leído casi todo sobre las mejores prácticas y plantillas adecuadas, entonces el maravilloso mundo de cómo no se debe escribir código pero se puede, queda solo un poco entornado.


En este artículo, analizaremos otra tarea artificial que requiere un abuso inexcusable de una solución normal.


Tarea anterior:



Redacción


Implemente una función de decorador que cuente el número de llamadas a la función pasada y brinde la capacidad de obtener este número a pedido. La solución no utiliza llaves y variables globales.

El contador de llamadas es solo una excusa, porque hay console.count () . La conclusión es que nuestra función acumula algunos datos cuando se llama a una función empaquetada y proporciona una determinada interfaz para acceder a ellos. Puede guardar todos los resultados de una llamada y recopilar registros y algún tipo de memorización. Solo un contra-primitivo y comprensible para todos.


Toda la complejidad está en una restricción anormal. No puede usar llaves, lo que significa que debe reconsiderar las prácticas cotidianas y la sintaxis ordinaria.


Solución habitual


Primero debes elegir un punto de partida. Por lo general, si el idioma o su extensión no proporciona la función de decoración necesaria, implementaremos un contenedor por nuestra cuenta: una función envuelta, datos acumulados y una interfaz de acceso a ellos. Esto es a menudo una clase:


class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.calls; // 2 

Esto no nos conviene de inmediato, ya que:


  1. En JavaScript, no puede implementar una propiedad privada de esta manera: podemos leer las llamadas de la instancia (que necesitamos) y escribir el valor desde el exterior (que NO necesitamos). Por supuesto, podemos usar el cierre en el constructor , pero ¿cuál es el significado de la clase? Y todavía tendría miedo de usar campos privados frescos sin babel 7 .
  2. El lenguaje admite un paradigma funcional, y crear una instancia a través de nuevo parece no ser la mejor solución aquí. Es mejor escribir una función que devuelve otra función. Si!
  3. Finalmente, la sintaxis de ClassDeclaration y MethodDefinition no nos permitirá deshacernos de todas las llaves.

Pero tenemos un patrón de módulo maravilloso que implementa la privacidad mediante el cierre:


 function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.getCalls(); // 2 

Ya puedes trabajar con esto.


Decisión entretenida


¿Por qué se usan llaves aquí? Estos son 4 casos diferentes:


  1. Definir el cuerpo de una función de conteo ( FunctionDeclaration )
  2. Inicialización del objeto devuelto.
  3. La definición del cuerpo de la función de invocación ( FunctionExpression ) con dos expresiones
  4. Definir el cuerpo de una función getCalls ( FunctionExpression ) con una sola expresión

Comencemos con el segundo párrafo. De hecho, no necesitamos devolver un nuevo objeto, al tiempo que complicamos la invocación de la función final a través de la invocación . Podemos aprovechar el hecho de que una función en JavaScript es un objeto, lo que significa que puede contener sus propios campos y métodos. Creemos nuestra función return df y agreguemos el método getCalls , que a través del cierre tendrá acceso a las llamadas como antes:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; } 

Es más agradable trabajar con esto:


 const csum = count((x, y) => x + y); csum(3, 7); // 10 csum(9, 6); // 15 csum.getCalls(); // 2 

El cuarto punto es claro: simplemente reemplazamos FunctionExpression con ArrowFunction . La ausencia de llaves nos proporcionará un breve registro de la función de flecha en el caso de una sola expresión en su cuerpo:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; } 

Con el tercero , todo es más complicado. Recuerde que lo primero que hicimos fue reemplazar FunctionExpression con invocar funciones con FunctionDeclaration df . Para volver a escribir esto en ArrowFunction, habrá que resolver dos problemas: no perder el acceso a los argumentos (ahora es una pseudo-matriz de argumentos ) y evitar el cuerpo de la función de dos expresiones.


El primer problema nos ayudará a hacer frente a los argumentos explícitamente especificados para el parámetro de función con el operador spread . Y para combinar dos expresiones en una, puede usar AND lógico . A diferencia del operador de conjunción lógica clásica que devuelve Boolean, calcula operandos de izquierda a derecha al primer "falso" y lo devuelve, y si todos son "verdaderos", entonces el último valor. El primer incremento del contador nos dará 1, lo que significa que esta subexpresión siempre se convertirá en verdadera. La reducibilidad a la "verdad" del resultado de la llamada a la función en la segunda sub-expresión no nos interesa: en cualquier caso, la calculadora se detiene en ella. Ahora podemos usar la función de flecha :


 function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; } 

Puede decorar un registro un poco usando el incremento de prefijo:


 function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; } 

La solución al primer y más difícil punto comenzará reemplazando FunctionDeclaration con ArrowFunction . Pero todavía tenemos el cuerpo entre llaves:


 const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }; 

Si queremos deshacernos de las llaves que enmarcan el cuerpo de la función, tendremos que evitar declarar e inicializar variables a través de let . Y tenemos dos variables enteras: llamadas y df .


Primero, tratemos con el mostrador. Podemos crear una variable local definiéndola en la lista de parámetros de función, y transferir el valor inicial llamándolo usando IIFE (Expresión de función invocada inmediatamente):


 const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0); 

Queda por concatenar las tres expresiones en una. Como tenemos las tres expresiones que representan funciones que siempre se pueden reducir a verdadero, también podemos usar AND lógico :


 const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0); 

Pero hay otra opción para concatenar expresiones: usar el operador de coma . Es preferible, ya que no se ocupa de transformaciones lógicas innecesarias y requiere menos corchetes. Los operandos se evalúan de izquierda a derecha, y el resultado es el valor de este último:


 const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

¿Supongo que logré engañarte? Audazmente nos deshicimos de la declaración de la variable df y dejamos solo la asignación de nuestra función de flecha. En este caso, esta variable se declarará globalmente, ¡lo cual es inaceptable! Para df , repetimos la inicialización de la variable local en los parámetros de nuestra función IIFE, pero no pasaremos ningún valor inicial:


 const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

Así se logra el objetivo.


Variaciones sobre un tema


Curiosamente, pudimos evitar crear e inicializar variables locales, varias expresiones en bloques de funciones y crear un objeto literal. Al mismo tiempo, la solución original se mantuvo limpia: la ausencia de variables globales, la privacidad del contador, el acceso a los argumentos de la función que se está envolviendo.


En general, puede tomar cualquier implementación e intentar hacer algo similar. Por ejemplo, el polyfill para la función de vinculación a este respecto es bastante simple:


 const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args)); 

Sin embargo, si el argumento f no es una función, deberíamos lanzar una excepción en el buen sentido. Y la excepción de lanzamiento no se puede lanzar en el contexto de la expresión. Puede esperar las expresiones de lanzamiento (etapa 2) e intentar nuevamente. ¿O alguien ya tiene pensamientos ahora?


O considere una clase que describe las coordenadas de un punto:


 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } } 

Que puede ser representado por una función:


 const point = (x, y) => (p => (px = x, py = y, p.toString = () => ['(', x, ', ', y, ')'].join(''), p))(new Object); 

Solo aquí hemos perdido la herencia del prototipo: toString es una propiedad del objeto prototipo Point , no un objeto creado por separado. ¿Se puede evitar esto si te esfuerzas?


En los resultados de las transformaciones, obtenemos una mezcla poco saludable de programación funcional con hacks imperativos y algunas características del lenguaje en sí. Si lo piensa, esto puede resultar ser un ofuscador interesante (pero no práctico) del código fuente. Puede crear su propia versión de la tarea de "ofuscador de horquillado" y entretener a colegas y amigos de JavaScript en su tiempo libre del trabajo útil.


Conclusión


La pregunta es, ¿a quién es útil y por qué se necesita? Esto es completamente dañino para los principiantes, ya que forma una falsa idea de la excesiva complejidad y desviación del lenguaje. Pero puede ser útil para los profesionales, ya que le permite observar las características del lenguaje desde el otro lado: la llamada no es para evitar, y la llamada es para tratar de evitar en el futuro.

Source: https://habr.com/ru/post/es428337/


All Articles