Secretos de las funciones de JavaScript

Todo programador está familiarizado con las funciones. Las funciones en JavaScript tienen muchas posibilidades, lo que nos permite llamarlas "funciones de orden superior". Pero incluso si usa funciones de JavaScript todo el tiempo, es posible que tengan algo que lo sorprenda. En esta publicación, cubriré algunas de las características avanzadas de las funciones de JavaScript. Espero que les resulte útil lo que han aprendido hoy.











Funciones puras



Una función que cumple los dos requisitos siguientes se llama pura:



  • Siempre, cuando se llama con los mismos argumentos, devuelve el mismo resultado.
  • No se producen efectos secundarios cuando se ejecuta la función.


Consideremos un ejemplo:



function circleArea(radius){
  return radius * radius * 3.14
}


Si a esta función se le pasa el mismo valor radius, siempre devuelve el mismo resultado. Al mismo tiempo, durante la ejecución de la función, nada fuera de ella cambia, es decir, no tiene efectos secundarios. Todo esto significa que esta es una función pura.



Aquí hay otro ejemplo:



let counter = (function(){
  let initValue = 0
  return function(){
    initValue++;
    return initValue
  }
})()


Probemos esta función en la consola del navegador.





Probando la función en la consola del navegador



Como puede ver, la funcióncounterque implementa el contador devuelve resultados diferentes cada vez que se llama. Por tanto, no se puede llamar puro.



Y aquí hay otro ejemplo:



let femaleCounter = 0;
let maleCounter = 0;
function isMale(user){
  if(user.sex = 'man'){
    maleCounter++;
    return true
  }
  return false
}


Aquí se muestra una función isMaleque, cuando se le pasa el mismo argumento, siempre devuelve el mismo resultado. Pero tiene efectos secundarios. Es decir, estamos hablando de cambiar una variable global maleCounter. Como resultado, esta función no se puede llamar pura.



▍¿Por qué se necesitan funciones puras?



¿Por qué trazamos la línea divisoria entre funciones regulares y puras? El punto es que las funciones puras tienen muchos puntos fuertes. Su uso puede mejorar la calidad del código. Hablemos de lo que nos aporta el uso de funciones puras.



1. El código de funciones puras es más claro que el código de funciones ordinarias, es más fácil de leer



Cada función pura está dirigida a una tarea específica. Este, llamado con la misma entrada, siempre devuelve el mismo resultado. Esto mejora enormemente la legibilidad del código y facilita la documentación.



2. Las funciones puras se prestan mejor a la optimización al compilar su código



Suponga que tiene un fragmento de código como este:



for (int i = 0; i < 1000; i++){
    console.log(fun(10));
}


Si fun- esta es una función que no es pura, entonces durante la ejecución de este código, esta función tendrá que ser llamada fun(10)1000 veces.



Y si funes una función pura, el compilador puede optimizar el código. Podría verse algo como esto:



let result = fun(10)
for (int i = 0; i < 1000; i++){
    console.log(result);
}


3. Las funciones puras son más fáciles de probar



Las pruebas de función pura no deben ser sensibles al contexto. Al escribir pruebas unitarias para funciones puras, simplemente pasan algunos valores de entrada a dichas funciones y verifican lo que devuelven con ciertos requisitos.



He aquí un ejemplo sencillo. La función pura toma una matriz de números como argumento y agrega 1 a cada elemento de esa matriz, devolviendo una nueva matriz. Aquí hay una representación taquigráfica:



const incrementNumbers = function(numbers){
  // ...
}


Para probar dicha función, basta con escribir una prueba unitaria que se parezca a la siguiente:



let list = [1, 2, 3, 4, 5];
assert.equals(incrementNumbers(list), [2, 3, 4, 5, 6])


Si una función no está limpia, para probarla, debe tener en cuenta muchos factores externos que pueden afectar su comportamiento. Como resultado, probar dicha función será más difícil que probar una pura.



Funciones de orden superior.



Una función de orden superior es una función que tiene al menos una de las siguientes capacidades:



  • Es capaz de aceptar otras funciones como argumentos.
  • Puede devolver una función como resultado de su trabajo.


El uso de funciones de orden superior le permite aumentar la flexibilidad de su código, ayudándole a escribir programas más compactos y eficientes.



Digamos que hay una matriz de números enteros. Es necesario crear sobre su base una nueva matriz de la misma longitud, pero cada elemento representará el resultado de multiplicar el elemento correspondiente de la matriz original por dos.



Si no usa las capacidades de las funciones de orden superior, entonces la solución a este problema puede verse así:



const arr1 = [1, 2, 3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] * 2);
}


Si piensa en el problema, resulta que los objetos de tipo Arrayen JavaScript tienen un método map(). Este método se llama como map(callback). Crea una nueva matriz llena con los elementos de la matriz para la que se llama, procesada con la función que se le pasa callback.



Así es como se ve la solución a este problema usando el método map():



const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
  return item * 2;
});
console.log(arr2);


El método map()es un ejemplo de una función de orden superior.



El uso correcto de funciones de orden superior ayuda a mejorar la calidad de su código. En las siguientes secciones de este material, volveremos a estas funciones más de una vez.



Resultados de la función de almacenamiento en caché



Digamos que tiene una función pura que se ve así:



function computed(str) {    
    // ,       
    console.log('2000s have passed')
      
    // ,     
    return 'a result'
}


Para mejorar el rendimiento del código, no nos vendrá mal recurrir al caché de los resultados de los cálculos realizados en la función. Cuando llame a una función de este tipo con los mismos parámetros con los que ya se llamó, no tendrá que volver a realizar los mismos cálculos. En cambio, sus resultados, previamente almacenados en la caché, se devolverán de inmediato.



¿Cómo equipar una función con un caché? Para hacer esto, puede escribir una función especial que pueda usarse como envoltorio para la función de destino. Le daremos un nombre a esta función especial cached. Esta función toma una función objetivo como argumento y devuelve una nueva función. En una función, cachedpuede organizar el almacenamiento en caché de los resultados de una llamada a la función envuelta alrededor de ella usando un objeto regular ( Object) o usando un objeto que es una estructura de datosMap...



Así es como podría verse el código de función cached:



function cached(fn){
  //     ,      fn.
  const cache = Object.create(null);

  //   fn,    .
  return function cachedFn (str) {

    //       -   fn
    if ( !cache[str] ) {
        let result = fn(str);

        // ,   fn,   
        cache[str] = result;
    }

    return cache[str]
  }
}


Estos son los resultados de experimentar con esta función en la consola del navegador.





Experimentar con una función cuyos resultados se almacenan en caché



Funciones perezosas



En los cuerpos funcionales, generalmente hay algunas instrucciones para verificar algunas condiciones. A veces, las condiciones correspondientes a ellos deben verificarse solo una vez. No tiene sentido comprobarlos cada vez que se llama a la función.



En tales circunstancias, puede mejorar el rendimiento de la función "eliminando" estas instrucciones después de que se ejecuten por primera vez. Como resultado, resulta que la función, durante sus llamadas posteriores, no tendrá que realizar comprobaciones que ya no serán necesarias. Esta será la función "perezosa".



Supongamos que necesitamos escribir una función fooque siempre devuelva el objeto Datecreado la primera vez que se llama a esa función. Tenga en cuenta que necesitamos un objeto que se creó exactamente en la primera llamada de la función.



Su código podría verse así:



let fooFirstExecutedDate = null;
function foo() {
    if ( fooFirstExecutedDate != null) {
      return fooFirstExecutedDate;
    } else {
      fooFirstExecutedDate = new Date()
      return fooFirstExecutedDate;
    }
}


Cada vez que se llama a esta función, se debe comprobar la condición. Si esta condición es muy difícil, las llamadas a dicha función provocarán una caída en el rendimiento del programa. Aquí es donde podemos utilizar la técnica de crear funciones "perezosas" para optimizar el código.



Es decir, podemos reescribir la función de la siguiente manera:



var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
}


Después de la primera llamada a la función, reemplazamos la función original por la nueva. Esta nueva función devuelve el valor trepresentado por el objeto Datecreado la primera vez que se llamó a la función. Como resultado, no es necesario verificar ninguna condición al llamar a dicha función. Este enfoque puede mejorar el rendimiento de su código.



Este fue un ejemplo condicional muy simple. Veamos ahora algo más cercano a la realidad.



Al adjuntar controladores de eventos a elementos DOM, debe realizar comprobaciones para asegurarse de que la solución sea compatible con los navegadores modernos y con IE:



function addEvent (type, el, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
    }
}


Resulta que cada vez que llamamos a una función addEvent, se verifica una condición en ella, lo cual es suficiente para verificar solo una vez, la primera vez que se llama. Hagamos esta función "perezosa":



function addEvent (type, el, fn) {
  if (window.addEventListener) {
      addEvent = function (type, el, fn) {
          el.addEventListener(type, fn, false);
      }
  } else if(window.attachEvent){
      addEvent = function (type, el, fn) {
          el.attachEvent('on' + type, fn);
      }
  }
  addEvent(type, el, fn)
}


Como resultado, podemos decir que si se verifica una determinada condición en una función, que debe realizarse solo una vez, aplicando la técnica de crear funciones "perezosas", puede optimizar el código. Es decir, la optimización consiste en el hecho de que después de la primera verificación de la condición, la función original es reemplazada por una nueva, en la que no hay más verificaciones de condiciones.



Funciones de curry



Currying es una transformación de una función, después de aplicar la cual, una función que previamente tenía que ser llamada pasándole varios argumentos a la vez, se convierte en una función que se puede llamar pasando los argumentos requeridos uno por uno.



En otras palabras, estamos hablando del hecho de que una función curry, que requiere varios argumentos para funcionar correctamente, es capaz de aceptar el primero de ellos y devolver una función que es capaz de tomar un segundo argumento. Esta segunda función, a su vez, devuelve una nueva función que toma un tercer argumento y devuelve una nueva función. Esto continuará hasta que se pase a la función el número requerido de argumentos.



¿Para qué sirve esto?



  • Currying ayuda a evitar situaciones en las que es necesario llamar a una función pasando el mismo argumento una y otra vez.
  • Esta técnica ayuda a crear funciones de orden superior. Es extremadamente útil para manejar eventos.
  • Gracias al curry, puede organizar la preparación preliminar de funciones para realizar ciertas acciones y luego reutilizar convenientemente dichas funciones en su código.


Considere una función simple que suma los números que se le pasan. Vamos a llamarlo add. Toma tres operandos como argumentos y devuelve su suma:



function add(a,b,c){
 return a + b + c;
}


Se puede llamar a dicha función pasándole menos argumentos de los que necesita (aunque esto conducirá al hecho de que devuelve algo completamente diferente de lo que se espera de ella). También se puede llamar con más argumentos de los previstos cuando se creó. En tal situación, los argumentos "innecesarios" simplemente serán ignorados. Experimentar con una función similar podría verse así:



add(1,2,3) --> 6 
add(1,2) --> NaN
add(1,2,3,4) --> 6 //  .


¿Cómo curry tal función?



Aquí está el código de la función curryque está destinada a cursar otras funciones:



function curry(fn) {
    if (fn.length <= 1) return fn;
    const generator = (...args) => {
        if (fn.length === args.length) {

            return fn(...args)
        } else {
            return (...args2) => {

                return generator(...args, ...args2)
            }
        }
    }
    return generator
}


Estos son los resultados de experimentar con esta función en la consola del navegador.





Experimentar con curry en la consola del navegador



Composición de funciones



Suponga que necesita escribir una función que, tomando una cadena como entrada bitfish, devuelva una cadena HELLO, BITFISH.



Como puede ver, esta función tiene dos propósitos:



  • Concatenación de cadenas.
  • Conversión de los caracteres de la cadena resultante a mayúsculas.


Así es como podría verse el código para dicha función:



let toUpperCase = function(x) { return x.toUpperCase(); };
let hello = function(x) { return 'HELLO, ' + x; };
let greet = function(x){
    return hello(toUpperCase(x));
};


Experimentemos con eso.





Prueba de una función en la consola del navegador



Esta tarea incluye dos subtareas que están organizadas como funciones independientes. Como resultado, el código de funcióngreetes bastante simple. Si fuera necesario realizar más operaciones en cadenas, entonces la funcióngreetcontendría una construcción comofn3(fn2(fn1(fn0(x)))).



Simplifiquemos la solución del problema y escribamos una función que componga otras funciones. Vamos a llamarlocompose. Aquí está su código:



let compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};


Ahora la función greetse puede crear usando la función compose:



let greet = compose(hello, toUpperCase);
greet('kevin');


Usar una función composepara crear una nueva función basada en dos existentes implica crear una función que llame a esas funciones de izquierda a derecha. Como resultado, obtenemos un código compacto que es fácil de leer.



Ahora nuestra función composetoma solo dos parámetros. Y nos gustaría que pudiera aceptar cualquier número de parámetros.



Una función similar, capaz de aceptar cualquier número de parámetros, está disponible en la conocida biblioteca de subrayados de código abierto .



function compose() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        var result = args[start].apply(this, arguments);
        while (i--) result = args[i].call(this, result);
        return result;
    };
};


Al utilizar la composición de funciones, puede hacer que las relaciones lógicas entre funciones sean más comprensibles, mejorar la legibilidad de su código y sentar las bases para futuras extensiones y refactorizaciones.



¿Utiliza alguna forma especial de trabajar con funciones en sus proyectos de JavaScript?










All Articles