Seleccionar página
Hay una máxima ampliamente difundida que dice “si algo funciona, no lo toques”. Como todas las máximas escode algo de verdad, pero también es una simplificación extrema que debemos plantearnos con espíritu crítico. En nuestro caso vamos a repasar las lecciones aprendidas después de un par de años utilizando una función de comparación universal que comprueba si dos elementos de Javascript son equivalentes entre sí: equal().

Cuando construimos la función equal() publicamos una serie de artículos donde íbamos explicando los diferentes retos que nos encontramos en su desarrollo:

En el tiempo transcurrido desde entonces hemos utilizado esta función en una buena cantidad de proyectos y hemos podido hacer una retrospectiva profunda de cómo la estamos utilizando, analizando cada caso de uso, que opciones estadísticamente hemos llamado más y cuales menos, que es intuitivo de su uso y que resulta más confuso, etc. Ahora, al hacer esta revisión, queremos compartir con todos vosotros algunas conclusiones que, quizás, puedan ser de utilidad.

Nombre de la función: confusa

El paquete se llama esequal (contracción de EcmaScript Equal) y la función equal(), lo cual ya ha sido bastante confuso. Los paquetes, si contienen una única función, deben llamarse igual que su contenido. Queríamos publicar equal en NPM pero ya estaba utilizado. Al final prácticamente nadie uso nuestra versión publicada en NPM y podríamos haber elegido el nombre equal en nuestro repositorio NPM privado (basado en Verdaccio) sin ningún problema.

El problema es más profundo, el nombre equal() no refleja bien lo que hace la función. Realmente lo que se está haciendo es ver si dos elementos son equivalentes, no si son iguales. Este error a la hora de elegir el nombre de la función ha causado más de una discusión en el equipo.

Elegir bien el nombre de las funciones, clases, paquetes, etc. es muy importante para que luego su uso sea intuitivo. Si te equivocas, es mejor cambiarlo. Nosotros finalmente hemos decidido llamar a la función equivalent() y todos hemos entendido mucho mejor lo que estamos haciendo.

Parámetros opcionales y retornos detallados: difíciles de recordar y muy poco utilizados

La función equal() tiene tres parámetros, valor1, valor2 y un tercer parámetro opcional con configuración que permiten definir el comportamiento de la comparación. En la práctica esas opciones de configuración se han usado muy pocas veces, prácticamente ninguna. Lo cierto es que seleccionamos nombres bastante largos y, aunque estaba perfectamente documentados, ha sido bastante difícil recordarlos.

Algo parecido ocurre con el retorno de la función. Es 0 (falsy) cuando no es igual y un valor diferente de 0 (truely) cuando los valores son equivalentes. Los valores por encima de 0 podían utilizarse para saber qué es exactamente lo que hemos comparado, pero en la práctica no lo hemos usado nunca.

Este es un ejemplo de sobre especificación, es decir, hemos creado una especificación para la función sin tener en cuenta realmente el uso de esta. Aunque es un parámetro opcional, lo cierto es que la implementación es bastante más compleja, la documentación más extensa y aumenta la dificultad para comprender el uso de la función.

Patrón módulo: a partir de ahora mejor el nativo de ES

La función se ha distribuido dentro de un patrón módulo que funciona indistintamente en Node y browser. Con el paso del tiempo import nativo de ES ha ido posicionándose como el sistema de módulos por excelencia. Quizás no esté todavía del todo extendido en Node (donde para usando todavía es necesario un parámetro --experimental-modules), pero si queremos que nuestro código esté preparado para el futuro, debemos pensar en utilizar el sistema de módulos de ES.

Con el tiempo las cosas van cambiando y lo que era novedoso y extraño se va convirtiendo, poco a poco, en lo más utilizado. No debemos tener miedo a actualizar nuestro código a los nuevos estándares en cuanto estos se van difundiendo por la comunidad. Mantener el código en lo usos y maneras originales hacen que, al final, queden obsoletos.

Soporte para navegadores muy antiguos: no tan necesario

En el código original tomamos muchas precauciones para que pudiera funcionar en navegadores bastante antiguos, por ejemplo, sin soporte a Map o Set. Lo cierto es que IE11 resiste, sobre todo en los entornos corporativos, pero ya da soporte a este tipo de objetos, por lo que la protección realizada resulta, por lo general, excesiva.

Es importante establecer de forma clara cual es nuestro soporte para navegadores antiguos. No siempre podemos confiar que Babel u otro transpiler pueda ofrecer un polyfill o alguna opción de transformación de nuestro código. Nos podemos llegar a sorprender del soporte que tiene IE11 a muchas características que solemos considerar bastante modernas. Es cierto que está bastante limitado en algunos aspectos, pero -en general- menos de lo que podemos pensar.

Comparación no estricta: para eso no necesitamos una función

Cuando queremos comparar dos valores aceptando una conversión de tipos no hemos utilizado nuestra función equal(), parecía en general demasiado “complicado” hacer una llamada a una función que hace muchas cosas para simplemente evitar el uso de ==.

Cuando el propio lenguaje da una solución sencilla, no intentemos reescribirla. En general los programadores conocen la solución aportada por el lenguaje y no van a recordar que existe una función en un determinado paquete para hacer esta misma operación, quizás de una forma más elegante, pero desde luego siempre más indirecta.

Comparar funciones: todavía no se nos ha producido el caso

Cuando implementamos equal() nos plateamos si comparar el código fuente de dos funciones tenía sentido o no. Parecía que cuando quisiéramos comprobar si dos clases son equivalentes comparar el código fuente de sus métodos podría llegar a ser útil. Lo cierto es que esta necesidad todavía no se ha presentado y no hemos hecho uso de esta funcionalidad en todo este tiempo.

Es bastante útil plantear alguna funcionalidad como “experimental” y comprobar con el tiempo si realmente responde a un caso de uso general que pueda ser de utilidad o, por el contrario, responde a una casuística muy poco realista, aunque se desde un punto de vista conceptual pudiera llegar a plantearse.

En los casos de undefined, null y NaN y para comparar fechas y objetos: muy útil

No todo han sido “fracasos”, siempre que hemos tenido casos complejos, sobre todo con undefined, null o NaN la función equal() ha sido de gran utilidad para evitar confusiones o errores no controlados. Estos casos excepcionales son, por lo general, bastante confusos y es difícil recordar cómo funcionan.

La comparación de fechas y objetos equivalentes ha sido otro de los casos de uso más habituales del uso que hemos dado a equal(). Cuando comparamos con === este tipo de elemento es comportamiento del lenguaje es muy específico y solo comprueba que los dos valores corresponden exactamente al mismo objeto, lo cual resulta poco intuitivo en muchas ocasiones. Es aquí donde una función como equal() cobra todo el sentido.

Cuando el comportamiento del lenguaje es limitado, confuso o poco intuitivo, una función personalizada es de gran utilidad y nos ayuda a dar respuesta de forma homogénea, completa y controlada a estas situaciones. El esfuerzo de aprender su comportamiento, importar la función y escribir su llamada compensa con creces la necesidad de escribir varias, e incluso muchas, líneas de código en cada ocurrencia y la reutilización es muy bienvenida.

Nueva versión de equivalent()

El código fuente de equal() se puede ver en Github. Aquí está la evolución a equivalent() donde hemos eliminado todas las funcionalidades que el tiempo nos ha confirmado que no son de utilidad práctica (también lo puedes ver en Github).

const NOT_EQUAL = false;                            // Return values
const EQUAL     = true;

export default function equivalent (a, b) {
  const aStack = [],                                // Stack array
        bStack = [];
  return (function check (a, b) {
    let aValue, bValue, aKeys, bKeys, key, i,       // Define variables
        aDescriptor, bDescriptor,
        aType = typeof a,                           // Get value types
        bType = typeof b;
    if (a === b) {                                  // Strict comparison
      return EQUAL;                                 // Equal value and type
    }
    if (aType === 'undefined' || bType === 'undefined' ||
        a === null || b === null)
    {                                               // undefined and null are always different
      return NOT_EQUAL;
    }
    if (aType === 'number' && bType === 'number' &&
        isNaN (a) && isNaN (b))
    {                                               // Special case: Not is a Number (NaN !== NaN)
      return EQUAL;
    }
    if (typeof a.valueOf === 'function' &&          // valueOf() is a function in both values
        typeof b.valueOf === 'function')
    {
      aValue = a.valueOf ();                        // Get valueOf()
      bValue = b.valueOf ();
      if (aValue !== a || bValue !== b) {           // Is different that the base value
        if (aValue === bValue) {                    // Is the same for both values
          if (a.constructor === b.constructor) {    // Same constructor, is the same type
            return EQUAL;
          }
          return NOT_EQUAL;                         // Strict comparison
        }
        return NOT_EQUAL;                           // Not equal
      }
    }
    if (aType !== bType) {                          // Different type is a not equal value
      return NOT_EQUAL;
    }
    if (aType === 'object') {                       // Objects
      if (aStack.indexOf (a) > -1 &&
          bStack.indexOf (b) > -1)
      {                                              // Has been previously processed
        return EQUAL;
      }
      if ((a instanceof RegExp && b instanceof RegExp) ||
          (a instanceof Error && b instanceof Error))
      {                                             // RegExp and Error family objects
        if (a.toString () !== b.toString ()) {
          return NOT_EQUAL;
        }
      } else if (
        (a instanceof Map && b instanceof Map) ||   // Map
        (a instanceof Set && b instanceof Set))     // Set
      {
        if (a.size !== b.size) {                    // Check size
          return NOT_EQUAL;
        }
        i = a.size;
        if (i > 0) {
          if (a instanceof Map && b instanceof Map) {
            aKeys = Array.from (a.keys ());
            bKeys = Array.from (b.keys ());
            while (i--) {
              if (bKeys.indexOf (aKeys[ i ]) === -1 ||
                  !check (a.get (aKeys[ i ]), b.get (aKeys[ i ])))
              {
                return NOT_EQUAL;
              }
            }
            return EQUAL;
          }
          if (check (
                Array.from (a.values ()).sort (), 
                Array.from (b.values ()).sort ())) 
          {
            return EQUAL;
          }
          return NOT_EQUAL;
        }
      } else if (                                   // ArrayBuffer
        (a instanceof ArrayBuffer || a instanceof DataView) &&
        (b instanceof ArrayBuffer || b instanceof DataView))
      {
        aValue = a instanceof ArrayBuffer ? new DataView (a) : a;
        bValue = b instanceof ArrayBuffer ? new DataView (b) : b;
        if (aValue.byteLength !== bValue.byteLength) {  // Check size
          return NOT_EQUAL;
        }
        i = bValue.byteLength;                      // Check content
        while (i--) {
          if (aValue.getInt8 (i) !== bValue.getInt8 (i)) {
            return NOT_EQUAL;
          }
        }
      } else {                                      // Compare properties
        aKeys = Object.getOwnPropertyNames (a);     // Get properties keys
        bKeys = Object.getOwnPropertyNames (b);
        if (aKeys.length !== bKeys.length) {        // Check number of properties keys
          return NOT_EQUAL;
        }
        if (aKeys.length > 0) {
          aStack.push (a);                          // Storage into stacks for recursive reference
          bStack.push (b);
          i = aKeys.length;
          while (i--) {                             // Check each property value (recursive call)
            key = aKeys[ i ];
            if (!check (a[ key ], b[ key ])) {
              return NOT_EQUAL;
            }
          }
        }
      }
      if (a.constructor === b.constructor) {        // It's the same constructor
        return EQUAL;                               // and as result is the equivalent object
      }
    }
    return NOT_EQUAL;                               // Not equal
  }) (a, b);
}

 

Optimización: quitar código sigue haciendo que mejore la velocidad de ejecución

La función equal() original tenía 250 líneas de código. La nueva función equivalent() tiene 120 líneas de código al eliminar muchas de las funcionalidades originales que se ha comprobado que no tienen un uso práctico. Esta reducción de funcionalidad y su consiguiente eliminación de código a mejorado un 20% el rendimiento de la función. Seguimos confirmando que eliminar código mejora el rendimiento y cuanto menos haga una función, mejor en cuanto a su velocidad de ejecución. No es una mejora lineal, hemos eliminado algo más de 50% de las líneas de código y la mejora de rendimiento es del 20%.

Conclusión

La lección más importante de todo esto es: no tengamos miedo a revisar con espíritu crítico nuestro trabajo pasado un tiempo. La experiencia nos ha da un conocimiento del que carecíamos cuando hicimos mucho de nuestro código y retomarlo, hacer una retrospectiva de su uso y cambiar lo que no funciona, quitar lo que no usamos y mejorar lo que es más útil siempre es interesante.

Novedades

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)

En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.

En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.

Observables en Javascript con Proxies

Observables en Javascript con Proxies

En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

Aplicaciones JAMStack, SEO friendly y escalables con NextJS

En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera

En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.