Funciones de orden superior en JS: Young Fighter Course

Este artículo está destinado a una persona que da sus primeros pasos tímidos en el camino espinoso de aprender JavaScript. A pesar del hecho de que en 2018, uso la sintaxis ES5 para que los jóvenes padawans que toman el curso JavaScript Nivel 1 en HTML Academy puedan entender el artículo.

Una de las características que distingue a JS de muchos otros lenguajes de programación es que en este lenguaje una función es un "objeto de primera clase". O, en ruso, una función es un significado. Igual que un número, cadena u objeto. Podemos escribir una función en una variable, podemos ponerla en una matriz o en una propiedad de objeto. Incluso podemos sumar dos funciones. De hecho, nada significativo saldrá de esto, pero de hecho, ¡podemos!

function hello(){}; function world(){}; console.log(hello + world); //  ,  ,   //   ,     

Lo más interesante es que podemos crear funciones que operen en otras funciones: aceptarlas como argumentos o devolverlas como un valor. Dichas funciones se denominan funciones de orden superior . Y hoy, niñas y niños, hablaremos sobre cómo adaptar esta oportunidad a las necesidades de la economía nacional. En el camino, aprenderá más sobre algunas características útiles de las funciones en JS.

Tubería


Digamos que tenemos algo con lo que necesitas hacer muchas piezas. Digamos que un usuario cargó un archivo de texto que almacena datos en formato JSON, y queremos procesar su contenido. Primero, necesitamos recortar los caracteres de espacio en blanco adicionales, que podrían "crecer" alrededor de los bordes como resultado de las acciones del usuario o del sistema operativo. Luego verifique que no haya código malicioso en el texto (quién sabe, estos usuarios). Luego cambie de texto a objeto utilizando el método JSON.parse . Luego, elimine los datos que necesitamos de este objeto. Y al final, envíe estos datos al servidor. Obtienes algo como esto:

 function trim(){/*  */}; function sanitize(){/*  */}; function parse(){/*  */}; function extractData(){/*  */}; function send(){/*  */}; var textFromFile = getTextFromFile(); send(extractData(parse(sanitize(trim(testFromFile)))); 

Se ve muy de acuerdo. Además, es probable que no haya notado que falta un soporte de cierre. Por supuesto, el IDE le diría esto, pero aún hay un problema. Para resolverlo, recientemente se propuso un nuevo operador |> . De hecho, no es nuevo, pero honestamente se tomó prestado de lenguajes funcionales, pero este no es el punto. Con este operador, la última línea podría reescribirse de la siguiente manera:

 textFromFile |> trim |> sanitize |> parse |> extractData |> send; 

El operador |> toma su operando izquierdo y lo pasa al operando derecho como argumento. Por ejemplo, "Hello" |> console.log equivalente a console.log("Hello") . Esto es muy conveniente precisamente para los casos en que se invocan varias funciones a lo largo de la cadena. Sin embargo, antes de la introducción de este operador, pasará mucho tiempo (si se acepta esta propuesta), pero debe vivir de alguna manera ahora. Por lo tanto, podemos escribirle a nuestra bicicleta una función que simule este comportamiento:

 function pipe(){ var args = Array.from(arguments); var result = args.shift(); while(args.length){ var f = args.shift(); result = f(result); } return result; } pipe( textFromFile, trim, sanitize, parse, extractData, send ); 

Si es un javascriptista novato (javascript? Javascript?), La primera línea de la función puede parecerle incomprensible. Es simple: dentro de la función, usamos la palabra clave argumentos para acceder a un objeto tipo matriz que contiene todos los argumentos pasados ​​a la función. Esto es muy conveniente cuando no sabemos de antemano cuántos argumentos tendrá. Un objeto masivo es como una matriz, pero no del todo. Por lo tanto, lo convertimos a una matriz normal usando el método Array.from . Espero que el código adicional ya sea bastante legible: comenzamos de izquierda a derecha para extraer elementos de la matriz y aplicarlos entre sí de la misma manera que lo haría el operador |>.

Registro


Aquí hay otro ejemplo cercano a la vida real. Supongamos que ya tenemos una función f que hace ... algo útil. Y en el proceso de probar nuestro código, queremos saber más sobre exactamente cómo lo hace f . En qué momentos se llama, qué argumentos se le pasan, qué valores se devuelven.

Por supuesto, con cada llamada de función, podemos escribir esto:

 var result = f(a, b); console.log(" f     " + a + "  " + b + "   " + result); console.log(" : " + Date.now()); 

Pero, en primer lugar, es bastante engorroso. Y en segundo lugar, es muy fácil olvidarlo. Un día simplemente escribiremos f(a, b) , y desde entonces la oscuridad de la ignorancia se asentará en nuestras mentes. Se expandirá con cada nuevo desafío f , del cual no sabemos nada.

Idealmente, me gustaría iniciar sesión automáticamente. Para que cada vez que llame a f , todo lo que necesitamos se escriba en la consola. Y, afortunadamente, tenemos una manera de hacer esto. ¡Conoce la nueva función de orden superior!

 function addLogger(f){ return function(){ var args = Array.from(arguments); var result = f.apply(null, args); console.log(" " + f.name + "     " + args.join() + "    " + result + "\n" + " : " + Date.now()); return result; } } function sum(a, b){ return a + b; } var sumWithLogging = addLogger(sum); sum(1, 2); //   sumWithLogging(1, 2) //  

Una función toma una función y devuelve una función que llama a la función pasada a la función cuando se creó la función. Lo siento, no pude dejar de escribir esto. Ahora en ruso: la función addLogger crea un addLogger alrededor de la función que se le pasa como argumento. La envoltura también es una función. Cuando se le llama, recopila una matriz de sus argumentos de la misma manera que lo hicimos en el ejemplo anterior. Luego, usando el método apply , llama a una función envuelta con los mismos argumentos y recuerda el resultado. Después de eso, el contenedor escribe todo en la consola.

Aquí tenemos el clásico caso de ataque de hombre en el medio. Si usa un contenedor en lugar de f , desde el punto de vista del código que lo usa, prácticamente no hay diferencia. El código puede suponer que se comunica con f directamente. Mientras tanto, el contenedor informa todo al camarada mayor.

Eins, zwei, drei, vier ...


Y una tarea más cerca de la práctica. Supongamos que necesitamos numerar algunas entidades. Cada vez que aparece una nueva entidad, obtenemos un nuevo número para ella, uno más que el anterior. Para hacer esto, comenzamos una función de la siguiente forma:

 var lastNumber = 0; function getNewNumber(){ return lastNumber++; } 

Y luego tenemos un nuevo tipo de entidad. Digamos, antes de eso, numeramos los conejitos, y ahora también hay conejos. Si usa una función para ambos y para otros, entonces cada número emitido a los conejos hará un "agujero" en la serie de números emitidos a los conejos. Entonces, necesitamos la segunda función, y con ella la segunda variable:

 var lastHareNumber = 0; function getNewHareNumber(){ return lastHareNumber++; } var lastRabbitNumber = 0; function getNewRabbitNumber(){ return lastRabbitNumber++; } 

¿Sientes que este código huele mal? Me gustaría tener algo mejor. En primer lugar, me gustaría poder declarar tales funciones menos detalladas, sin duplicar el código. Y en segundo lugar, me gustaría "empaquetar" de alguna manera la variable que la función utiliza en la función misma para no obstruir el espacio de nombres una vez más.

Y luego un hombre irrumpe, familiarizado con el concepto de OOP, y dice:
"Elemental, Watson". Es necesario hacer que los generadores de números no sean objetos, sino objetos. Los objetos están diseñados para almacenar funciones que funcionan con datos, junto con estos mismos datos. Entonces podríamos escribir algo como:

 var numberGenerator = new NumberGenerator(); var n = numberGenerator.get(); 

A lo que responderé:
- Para ser honesto, estoy completamente de acuerdo contigo. Y, en principio, este es un enfoque más correcto que el que ofreceré ahora. Pero aquí tenemos un artículo sobre funciones, no sobre OOP. ¿Podrías callarte un momento y dejarme terminar?

Aquí (¡sorpresa!) La función de orden superior nos ayudará nuevamente.

 function createNumberGenerator(){ var n = 0; return function(){ return n++; } } var getNewHareNumber = createNumberGenerator(); var getNewRabbitNumber = createNumberGenerator(); console.log( getNewHareNumber(), getNewHareNumber(), getNewHareNumber(), getNewRabbitNumber(), getNewRabbitNumber(), ); //    0, 1, 2, 0, 1 

Y aquí algunas personas pueden tener una pregunta, tal vez incluso en forma obscena: ¿qué demonios está pasando? ¿Por qué estamos creando una variable que no se usa en la función misma? ¿Cómo accede a ella una función interna si una función externa ha completado su ejecución hace mucho tiempo? ¿Por qué dos funciones creadas que se refieren a la misma variable obtienen resultados diferentes? Una respuesta a todas estas preguntas es el cierre .

Cada vez que se createNumberGenerator función createNumberGenerator , el intérprete JS crea una cosa mágica llamada "contexto de ejecución". En términos generales, este es un objeto en el que se almacenan todas las variables declaradas en esta función. No podemos acceder a él como un objeto javascript ordinario, pero sin embargo lo es.

Si la función era "simple" (digamos, agregando números), luego del final de su trabajo, el contexto de ejecución resulta inútil. ¿Sabes qué sucede con los datos innecesarios en JS? Son devorados por un demonio insaciable llamado Garbage Collector. Sin embargo, si la función era "complicada", puede suceder que alguien todavía necesite su contexto incluso después de que se haya ejecutado esta función. En este caso, el recolector de basura lo salva, y él permanece colgado en algún lugar de su memoria, para que aquellos que lo necesitan puedan tener acceso a él.

Por lo tanto, la función devuelta por createNumberGenerator siempre tendrá acceso a su propia copia de la variable n . Puedes pensarlo como una bolsa de tenencia de D&D. Pones tu mano en tu bolso y te encuentras en un "bolsillo" interdimensional personal donde puedes almacenar todo lo que quieras.

Debounce


Existe tal cosa como "eliminar el rebote". Esto es cuando no queremos que se llame a alguna función con demasiada frecuencia. Supongamos que hay un botón, al hacer clic en el cual se inicia un proceso "costoso" (largo, o que consume mucha memoria, o Internet, o sacrifica las vírgenes). Puede suceder que un usuario impaciente comience a hacer clic en este botón con una frecuencia de más de diez hertzios. Además, el proceso mencionado tiene una naturaleza tal que no tiene sentido ejecutarlo diez veces seguidas, porque el resultado final no cambiará. Es entonces cuando aplicamos la "eliminación de charla".

Su esencia es muy simple: realizamos la función no inmediatamente, sino después de un tiempo. Si antes de que este tiempo haya pasado, se volvió a llamar a la función, "reiniciamos el temporizador". Por lo tanto, el usuario puede hacer clic en el botón al menos mil veces, solo se necesita uno para el sacrificio. Sin embargo, menos palabras, más código:

 function debounce(f, delay){ var lastTimeout; return function(){ if(lastTimeout){ clearTimeout(lastTimeout); } var args = Array.from(arguments); lastTimeout = setTimeout(function(){ f.apply(null, args); }, delay); } } function sacrifice(name){ console.log(name + "     * *"); } function sacrificeDebounced = debounce(sacrifice, 500); sacrificeDebounced(""); sacrificeDebounced(""); sacrificeDebounced(""); 

En medio segundo, Lena será sacrificada, y Katya y Sveta sobrevivirán gracias a nuestra función mágica.

Si lee atentamente los ejemplos anteriores, debe comprender bien cómo funciona todo aquí. La función de contenedor creada por debounce desencadena la ejecución retrasada de la función original usando setTimeout . En este caso, el identificador de tiempo de espera se almacena en la variable lastTimeout, que es accesible para el reiniciador debido al cierre. Si el identificador de tiempo de espera ya está en esta variable, el reiniciador cancela este tiempo de espera con clearTimeout . Si el tiempo de espera anterior ya se ha completado, no pasa nada. Si no, mucho peor para él.

Sobre esto, tal vez, termine. Espero que hoy hayas aprendido muchas cosas nuevas y, lo más importante, hayas entendido todo lo que aprendiste. Nos vemos de nuevo.

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


All Articles