Todas las novedades de ECMAScript (2021 – 2025)

Todas las novedades de ECMAScript (2021 – 2025)

Si programas en JavaScript, sabes que el lenguaje no para de evolucionar. Desde 2021 hasta 2025, ECMAScript ha traído cambios muy interesantes: nuevas formas de escribir clases, métodos más potentes para arrays, mejoras en objetos, y propuestas que prometen hacer nuestro código más limpio y eficiente.

En este artículo voy a intentar resumir todo lo que necesitas saber, usando ejemplos claros y explicaciones que van al grano.


ECMAScript 2021 (ES12)

Operadores lógicos de asignación

Permiten asignar un valor a una variable si se cumple una condición lógica.

let a = null; 
a ||= 'valor por defecto'; // "valor por defecto"

Separadores numéricos

Mejoran la legibilidad de números grandes.

const billon = 1_000_000_000;

String.prototype.replaceAll()

Permite reemplazar todas las apariciones de una subcadena.

"foo bar foo".replaceAll("foo", "baz"); // "baz bar baz"

Promise.any()

Resuelve con la primera promesa cumplida.

Promise.any([
   Promise.reject("error"),
   Promise.resolve("éxito")
]); // "éxito"

WeakRef y FinalizationRegistry

Permiten la referencia débil a objetos y su manejo al ser recolectados, lo que resulta útil en tareas como cacheado o manejo de recursos sin evitar que el recolector de basura los libere.

  • WeakRef: crea una referencia «débil» a un objeto, que no impide que el GC (garbage collector) lo elimine.

  • FinalizationRegistry: te permite registrar una función que se ejecuta cuando el objeto asociado es recolectado, pero no garantiza cuándo se ejecutará la función, solo que lo hará en algún momento después de que el objeto se recolecte. No es recomendable para lógica crítica.

class Cache {
  constructor() {
    this.cache = new Map();
    this.finalizationRegistry = new FinalizationRegistry((key) => {
      console.log(`Objeto con clave "${key}" fue recolectado.`);
      this.cache.delete(key); // Limpia la entrada del mapa
    });
  }

  set(key, value) {
    this.cache.set(key, new WeakRef(value));
    this.finalizationRegistry.register(value, key);
  }

  get(key) {
    const ref = this.cache.get(key);
    return ref?.deref(); // Devuelve el objeto si aún no ha sido recolectado
  }
}

// Uso
let user = { name: "Sparrow" };
const cache = new Cache();

cache.set("user1", user);

console.log(cache.get("user1")?.name); // "Sparrow"

user = null; // El objeto ahora puede ser recolectado. Después de cierto tiempo, el GC lo elimina y FinalizationRegistry se activa

ECMAScript 2022 (ES13)

Campos y métodos privados en clases

Restricción de acceso mediante el prefijo #

class Persona {
   #nombre;
   constructor(nombre) {
      this.#nombre = nombre;
   }
   #saludar() {
      console.log(`Hola, soy ${this.#nombre}`);
   }
}

Array.prototype.at()

Accede a elementos por índices positivos o negativos.

const arr = [1, 2, 3]; 
arr.at(-1); // 3

Object.hasOwn()

Verifica si un objeto tiene una propiedad propia (sin herencia).

Object.hasOwn({ a: 1 }, 'a'); // true

Error.cause

Permite encadenar errores con contexto adicional.

try {
   throw new Error('Error bajo nivel');
} catch (err) {
   throw new Error('Error alto nivel', { cause: err });
}

RegExp Match Indices

Obtiene posiciones de coincidencias.

const m = /a(b)c/d.exec("abc");
m.indices; // [[0,3],[1,2]]

ECMAScript 2023 (ES14)

Array.prototype.findLast() y findLastIndex()

Buscan desde el final del array.

[1, 2, 3, 4].findLast(x => x % 2 === 0); // 4

Métodos de arrays inmutables

Retornan nuevas versiones del array:

const arr = [3, 1, 2];
arr.toSorted();      // [1, 2, 3]
arr.toReversed();    // [2, 1, 3]
arr.toSpliced(1, 1); // [3, 2]
arr.with(1, 99);     // [3, 99, 2]

Símbolos como claves en WeakMap

Ya se pueden usar símbolos como claves en un WeakMap, siempre que no sean símbolos registrados (es decir, creados con Symbol.for()).

Antes de esta mejora, solo se permitían objetos como claves, y usar símbolos directamente generaba error.

Esto permite usar símbolos únicos (no registrados globalmente) como identificadores privados o claves internas en estructuras como WeakMap, sin necesidad de envolverlos en objetos.

const wm = new WeakMap();

const sym = Symbol(); // símbolo no registrado
const obj = { secreto: "valor oculto" };

wm.set(sym, obj); // ¡Ahora permitido!

console.log(wm.get(sym)); // { secreto: "valor oculto" }

Hashbang grammar

A partir de ES2023, JavaScript admite oficialmente el uso de una línea hashbang (#!) al comienzo de los archivos, al estilo de los scripts de Unix.

Esta nueva característica nos permite que archivos JavaScript sean ejecutables directamente desde la terminal en sistemas como Linux o macOS, sin necesidad de invocar manualmente node.

Esto puede ser muy útil para crear nuestro propios CLI (Command Line Interface) en node.

#!/usr/bin/env node console.log("¡Hola desde un script ejecutable de Node.js!");

¿Qué hace #!/usr/bin/env node?

  • Es una directiva para el sistema operativo, no para JavaScript.

  • Le dice al sistema que use el ejecutable de node para correr el archivo.

  • El motor JavaScript ignora esa línea gracias a la nueva sintaxis soportada por el estándar

Chequeo ergonómico de campos privados

Permite verificar si un objeto tiene un campo privado:

if (#miCampo in obj) { ... }

ECMAScript 2024 (ES15)

Object.groupBy() y Map.groupBy()

Agrupan elementos según una función de agrupamiento.

const arr = [1, 2, 3, 4, 5];
Object.groupBy(arr, x => x % 2 === 0 ? 'pares' : 'impares');

Pipeline operator (|>)

Permite componer funciones de forma legible.

const payload = [{ name: 'A', active: true }, { name: 'X', active: true }, { name: 'H', active: false }, { name: 'N', active: true }, { name: 'J', active: false }];

// Forma normal concatenando funciones
const fnComplete = users => users
        .filter(user => user.active)
        .map(user => user.name.toUpperCase())
        .sort()
        .join(', ');
console.log(fnComplete(payload)); // A, N, X

// Usando |>
const getActiveUser = users => users.filter(user => user.active);
const getUserNameToUppercase = users.map(user => user.name.toUpperCase());
const sortUsers = users => users.sort();
const usersToString = users => users.join(', ');
const x = users |> getActiveUser|> getUserNameToUppercase |> sortUsers |> usersToString;
console.log(x); // A, N, X

Para usar esta característica es necesario instalar el plugin plugin-proposal-pipeline-operator.

npm install --save-dev @babel/plugin-proposal-pipeline-operator

También deberás añadir la configuración en tu archivo .babelrc

{
  "plugins": [["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }]]
}

Nuevos métodos en Set

Operaciones como en matemáticas:

set1.union(set2);
set1.intersection(set2);
set1.difference(set2);

ECMAScript 2025 (ES16)

Los detalles finales de ES2025 aún no están cerrados oficialmente, ya que el proceso del comité TC39 sigue en marcha. Sin embargo, es probable que la versión incluya propuestas que actualmente se encuentran en etapa 3 o 4, como:

  • Nuevas mejoras de ergonomía en clases y estructuras de datos.

  • Posibles avances en tipos integrados opcionales.

  • Funciones integradas más expresivas y reutilizables.

  • Mejoras de rendimiento y consistencia del lenguaje.

Puedes seguir el estado más actualizado de las propuestas activas y su progreso directamente en las siguientes web: https://tc39.es/https://tc39.es/ecma262/y https://github.com/tc39/proposals

A medida que estas propuestas avancen a Stage 4, pasarán a formar parte del borrador oficial de ECMAScript.

El estándar se publica normalmente cada junio, por lo que ES2025 se cerrará en la primera mitad del año.

Conclusión

ECMAScript evoluciona rápidamente para adaptarse a un ecosistema de desarrollo más potente, expresivo y seguro.

Estas mejoras no solo aportan sintaxis más limpia, sino también patrones más robustos que permiten desarrollar aplicaciones modernas con mayor confianza.

Como funciona JS

Como funciona Javascript – Parte 2

Como funciona Javascript

En la entrada anterior sobre como funciona Javascript aprendimos algunos conceptos sobre el funcionamiento interno de Javascript.

Todo estos conceptos son importante conocerlos ya que nos permite entender como funciona Javascript, y de esta forma crear un código óptimo.

Para ello continuaremos viendo nuevos conceptos que se apoyan en los aprendidos en la entrada anterior de Javascript.

El entorno léxico (Lexical environment)

En Javascript existe un termino llamado entorno léxico (lexical environment), y lo podemos definir como el entorno donde esta escrito nuestro código.

Como veremos a continuación, el contexto de ejecución (execution context) nos dice que entorno léxico (lexical environment) se esta ejecutando en ese momento.

El contexto de ejecución (Execution context)

El contexto de ejecución (Execution context), lo podemos definir como el entorno donde se ejecuta una función y el ámbito de una variable.

En base a la definición anterior, cada vez que se inicia nuestro motor (engine) de Javascript, este genera un contexto de ejecución global, y cada vez que ejecutamos una función se genera un nuevo contexto que es totalmente independiente entre si.

Esta acción nos ofrece en el contexto global los objetos window y this, que aunque en este punto son lo mismo, veremos que el objeto this tiene un comportamiento un tanto peculiar.

console.log('Contexto de ejecución global'); 
// Contexto de ejecución global

console.log('WINDOW:', window);
/* WINDOW: Window {0: global, 1: Window, window: Window, self: Window, document: document, name: "", location: Location, …} 
*/
console.log('THIS:', this);
/* THIS: Window {0: global, 1: Window, window: Window, self: Window, document: document, name: "", location: Location, …} 
*/

const day = 'Friday';
function suma(a, b) {
   console.log('Contexto de ejecución en la función "suma()"');
   console.log('Que día es: ', day);
   return a + b;
}

console.log(suma(10, 10));

/*
Contexto de ejecución en la función "suma()"
Que día es: Friday
20
*/

Como hemos observado en el código anterior, el contexto de ejecución tiene una fase que se realiza automáticamente y que se llama fase de creación.

En ese punto hemos visto que se crean los espacios de memoria, el objeto global window y el objeto this que tendrá el valor del contexto que se este ejecutando en ese momento, de allí el comportamiento peculiar que habíamos comentado anteriormente.

A continuación veremos que esa asignación de memoria que se esta produciendo en la fase de creación, tiene un termino conocido como hoisting.

¿Que es el hoisting?

Se puede definir como el movimiento de las variables y las funciones al inicio de nuestro código (siempre en su respectivo entorno), pero…. en realidad esto no es correcto.

Lo que esta sucediendo es que las variables y las funciones están siendo asignadas en memoria y por eso tenemos acceso a ellas, realmente el código continua en su sitio.

(() => {
    function suma(a, b) { 
        return a + b;
    }
    console.log(suma(5, 5));       // 10
    console.log(resta(100, 20));   // 80
    function resta(a, b) {
        return a - b; 
    }
})();

Otro ejemplo para entender como funciona el hoisting, lo podemos comprobar cuando realizamos la definición de una función, y es que no es lo mismo tener una función declarativa que una función expresiva.

En la función declarativa se aplica el hoisting de tal forma que se esta reservando espacio en la memoria y podemos invocarla.

En la función expresiva no se aplica el hoisting como es normal.

(() => {
    fnDeclaration(); // fnDeclaration
    fnExpresion();   // Uncaught TypeError: fnExpresion is not a function
    const fnExpresion = function() {
        console.log('fnExpresion');
    }
    function fnDeclaration() {
        console.log('fnDeclaration');
    }
})();

Aunque esto funcione, personalmente me parece muy mala practica, genera caos y un código poco legible.

La segunda fase llamada fase de ejecución, ejecutara el código que se ha definido en la fase de creación, en nuestro caso la función suma.

Continuando en la fase de ejecución podemos encontrar otro concepto llamado Scope y que hace referencia al contexto actual de ejecución.

¿Que es el Scope?

Lo podemos definir como el alcance o la capacidad que tenemos para acceder a los valores que son accesibles o referenciados.

Esto significa que si tenemos por ejemplo una variable y esta no existe en el contexto actual de ejecución (scope), entonces no estará disponible.

A continuación podemos ver algunos ejemplos de scope para entender como funciona

(() => {
    const global = 'Global';

    const fnScope1 = () => {
        const scope1 = '1';
        console.log(scope1);
        console.log(global);
    }

    const fnScope2 = () => {
        const scope2 = '2';
        console.log(scope2);
        console.log(global);
    }

    const fnScope3 = () => {
        const scope3 = '3';
        console.log(scope3);
        console.log(global);
    }

    fnScope1();  // 1, Global
    fnScope2();  // 2, Global
    fnScope3();  // 3, Global
})();

En el ejemplo anterior tenemos el scope (global) que es accesible desde cada función, y a su vez cada función tiene su propio scope (scope1, scope2, scope3) que solo es accesible desde su propio scope pero pueden acceder al scope superior.

Otro ejemplo interesante donde podemos ver el uso de los scopes son en las closures.

A continuación un pequeño ejemplo.

(() => {
    const data = a => {
        let scope1 = a;
        console.log('SCOPE 1', scope1); // SCOPE 1 1
        return b => {
            let scope2 = b;
            console.log('SCOPE 2', scope1, scope2); // SCOPE 2 1 2
            return c => {
                let scope3 = c;
                console.log('SCOPE 3', scope1, scope2, scope3); // SCOPE 3 1 2 3
                return `${scope1}-${scope2}-${scope3}`;
            }
        }
    }
    const a = data(1);
    const b = a(2);
    const c = b(3);
    console.log('RESULT:', c); // RESULT: 1-2-3

    // Forma abreviada
    const dataAbrev = a => b => c => `${a}-${b}-${c}`;
    const r = dataAbrev(10)(20)(30);
    console.log('RESULT 2:', r); // RESULT 2: 10-20-30

})();

En el ejemplo anterior podemos acceder desde un nivel inferior a un nivel superior sin problemas, pero si intentásemos acceder desde un nivel superior a un nivel inferior fallaría ya que cada función tiene su scope.

Ahora que sabemos que es el scope, ya podemos entender la diferencia entre el alcance en una función (function scope) y el alcance de bloque (block scope).

La diferencia entre function scope y block scope es la siguiente:

  • En el function scope cualquier variable declarada dentro de la misma es visible en cualquier sitio dentro de esa función.
  • En el block scope las variables definidas son visibles solo en el bloque encerrado entre las llaves.

A continuación un pequeño ejemplo.

(() => {
    // Function scope (IIFE Start)
    let data1 = 'DATA1';
    function fnScope() {
        // Function scope (Start)
        let data2 = 100;
        console.log('Function Scope', data2, data1);  // Function Scope 100 DATA1
        // Function scope (End);
    }

    {
        // Block scope (Start)
        let data3 = true;
        console.log('Other block scope', data3, data1); // Other block scope true DATA1
        // Block scope (End)    
    }
    console.log(data2);     // Uncaught ReferenceError: data2 is not defined
    console.log(data3);     // Uncaught ReferenceError: data3 is not defined
    fnScope();
    for (let cont = 0; cont < 10; cont++) {
       // Block scope (Start)
       console.log(cont); // 0 1 2 3 4 5 6 7 8 9
       // Block scope (End)
    }

    // Function scope (IIFE End)
})();

En el ejemplo anterior podemos ver que tenemos hasta 4 scopes diferentes:

  • El primero que es muestra IIFE y que esta limitado por la llaves que van de la línea 1 y la línea 27. (function scope).
  • El segundo que esta limitado por las llaves que van de la línea 4 a la línea 9. (function scope).
  • El tercero que esta limitado por las llaves que van de la línea 11 a línea 16. (block scope).
  • Y el cuarto y último scope que va desde la línea 20 a la línea 24. (block scope).

Debemos saber que esto es posible gracias al uso de let const cuando declaramos nuestras variables ya que respeta el scope donde fue declarada, cosa que no ocurre si usamos var.

Otro concepto que debemos entender y que esta muy relacionado con todo lo aprendido hasta ahora, es el famoso objeto this.

¿Que es this?

Al comienzo de esta entrada vimos que en la fase de creación, se creaban dos objetos muy importantes, window this.

Como dijimos, el objeto this es bastante peculiar ya que su contenido varía dependiendo de el lugar en el que se invoca, así que vamos a realizar algunos ejemplos para entender su comportamiento.

Vamos a crear una función llamada Suma desde la consola del navegador, por lo tanto el contexto actual es el contexto global y podemos decir que es window.

this1

this2

function Suma(a, b) {
   console.log('Suma', this);
   return a + b; 
}

console.log(Suma(1, 2));
// Window: { 0: global, ..... }
// 3

console.log(window.Suma(3, 3));
// Window: { 0: global, ..... }
// 6

Ahora vamos hacer un ejemplo para ver como cambia el valor del objeto this.

const obj = {
   status: true,
   getStatus: function() {
      console.log('Get Status', this);
      return this.status;
   },
   getStatus2: () => {
      console.log ('Get Status 2', this);
      return this.status;
   }
}

console.log(obj.getStatus());
// Ges Status {status: true, getStatus: ƒ, getStatus2: ƒ}
// true

console.log(obj.getStatus2());
// Get Status 2 Window {0: global, window: Window, self: Window, document: document, …}
// ""

Si observamos el resultado anterior podemos comprobar que cuando en el objeto obj usamos funciones flecha para definir un método, este toma para this el valor del entorno léxico y como el objeto obj forma parte de window, el valor de this sería window.

Esto es debido a que en la versión de ECMAScript 2015 (ES6) las arrow function no tienen su propio this, usando para el this el correspondiente al entorno léxico adjunto.

Como el método getStatus() esta definido como una función convencional podemos decir que el el entorno léxico no sería window, por lo tanto en este caso this tendría el valor del objeto obj.

En el siguiente punto veremos que existen otras formas de trabajar con this usando los métodos bind, call y apply para gestionar el valor de this.

Usando los métodos bind, call y apply

Bind, call y apply son unos métodos que nos permiten modificar el valor del objeto this, ya que como vimos al principio de la entrada dependiendo del contexto de ejecución (execution context), el valor de this puede cambiar de valor.

Ahora que sabemos que con estos métodos podemos manipular el valor de this, vamos a ver como hacerlo con algunos ejemplos.

  • bind: El método bind nos permite crear una función nueva con el mismo contenido que la función vinculada pero pudiendo asociar el valor que necesitemos al objeto this.
    • const externalData = {
          title: 'external',
          value: 100,
          active: true
      };
      
      const personalData = {
          title: 'internal',
          value: 0,
          active: false,
          getTitle: function() {
              return this.title;
          },
          getValue: function() {
              return this.value;
          },
          getActive: function() {
              return this.active;
          },
          showArguments: function() {
              console.log(...arguments, this);
          }
      };
      
      // function
      console.log('title:', personalData.getTitle());    // title: internal
      console.log('value:', personalData.getValue());    // value: 0
      console.log('active:', personalData.getActive());  // active: false
      
      // BIND
      const fnTitle = personalData.getTitle;
      const resultTitle = fnTitle.bind(externalData);
      
      console.log('bind data:', resultTitle());          // bind data: external
      
      const fnShow = personalData.showArguments.bind(externalData, 1, 2, 3);  
      fnShow('*', 5, 6, 7, 8, 9, '*'); 
      // 1 2 3 "*" 5 6 7 8 9 "*" {title: "external", value: 100, active: true}
      
      function showThis() {
         console.log('THIS:', this);
      }
      
      showThis(); // Window { window: Window, self: Window, document: document, ...}
      
      const fn = showThis.bind({ data: 100});
      fn();       // THIS: { data: 100 }
      
      showThis.bind({ data: 200 })();  // THIS: { data: 200 }
    • En el código anterior podemos observar en las líneas 31 32 como estamos haciendo el uso de bind y quizás ahora es mas sencillo entender su definición.
    • La sintaxis del método bind recibe como primer argumento el valor que tendrá el objeto this, y los siguientes argumentos se enviaran junto a los que se tenga la función vinculada (linea 36 y 37).
  • call: Con este método podemos llamar a una función indicando el valor para el objeto this y además enviar los argumentos individualmente.
    • function showThis() {
          console.log('THIS:', this, arguments);
      }
      
      showThis.call(null, 4, 5, 6); 
      // THIS: Window {0: global, …} Arguments(3) [4, 5, 6, callee: ƒ, Symbol(Symbol.iterator): ƒ]
      showThis.call({ data: 100 }, 7, 8, 9); 
      // THIS: {data: 100} Arguments(3) [7, 8, 9, callee: ƒ, Symbol(Symbol.iterator): ƒ]
  • apply: Funciona igual que el método call, pero en apply los argumentos se pasan mediante un array.
    • function showThis(...data) {
          console.log('THIS:', this, arguments, data);
      } 
      
      showThis.apply(null, [4, 5, 6]);             // THIS: Window {0: global, window: Window, …} Arguments(3) [4, 5, 6, callee: (...), Symbol(Symbol.iterator): ƒ] (3) [4, 5, 6]
      showThis.apply({ data: 100 }, [4, 5, 6]);    // THIS: {data: 100} Arguments(3) [4, 5, 6, callee: (...), Symbol(Symbol.iterator): ƒ] (3) [4, 5, 6]

Con esto terminamos la segunda parte de este pequeño curso sobre como funciona Javascript.

Como funciona JS

Como funciona Javascript – Parte 1

Como funciona Javascript

Javascript es un lenguaje de programación interpretado con un único hilo de ejecución y que se apoya en uno de los tantos motores (engines) que existen actualmente.

En esta entrada y las restantes siembre nos apoyaremos en el engine V8 ya que actualmente es el que mejores resultado tiene.

No obstante puedes encontrar en el siguiente enlace un amplio listado con múltiples motores.

Sabiendo esto, ahora veremos como es el proceso para convertir este código.

function f(param) {
  return param.name;
}

En este otro código.

;; full compiled call site
 ldr   r0, [fp, #+8]     ; load parameter "param" from stack
 ldr   r2, [pc, #+84]    ; load string "name" from constant pool
 ldr   ip, [pc, #+84]    ; load uninitialized stub from constant pool
 blx   ip                ; call the stub
 ...
 dd    0xabcdef01        ; address of stub loaded above
                         ; this gets replaced when the stub misses

Como funciona el V8

Lo primero que debemos saber es que el engine V8 esta escrito en C++, motivo por el cual lo hace sumamente rápido.

En el interior del engine V8 existen diferentes partes y cada una se encarga de una tarea en particular, todo esto para que desde un archivo de Javascript (texto plano), nuestro ordenador, movil, tablet, tv, etc… pueda ejecutarlo.

  1. El archivo JS es procesado usando un PARSER que realiza un análisis léxico, el cual hace pequeños trozos de código llamados tokens e intenta identificar el significado de cada token y que debe hacer ese código.
  2. Ahora con esos tokens generados se formara un arbol de sintaxis abstracta (AST – Abstract Sintax Tree), y que por cierto, si queréis ver como se forma ese árbol dentro del engine V8 podéis visitar la siguiente web.
  3. A continuación pasaríamos a la fase de interpretación (INTERPRETER), que es la encargada de pasar toda esa estructura a un código que comprenda cualquier máquina (bytecode). Esta última fase tiene algunos matices que veremos a continuación.

Diferentes caminos: intérprete y compilación

En este punto ya entendemos el motivo por el cual Javascript es un lenguaje interpretado.

Podemos observar que la última fase (casi la última) del engine V8, su finalidad es interpretar ese árbol y convertirlo a un código comprensible por la máquina.

Bien…, es necesario hacer un pequeño paréntesis para comprender las siguientes fases.

Debemos saber que no solo existen los lenguajes interpretados, también existen los lenguajes compilados como C, C++, C#, Go, Java (bytecode), Delphi, etc..

En estos y otros muchos lenguajes de programación compilados no se produce la «interpretación» al vuelo como sucede en Javascript, PHP o Python.

En el proceso de compilación se revisa el código he intenta comprender que hace ese código para compilarlo a un nuevo lenguaje que entienda la máquina (código máquina).

A continuación un pequeño ejemplo creado usando C.

Creamos nuestro código.

TruboC1

Compilamos nuestro código.

TurboC2

Finalmente construimos nuestro fichero ejecutable (.exe).

TurboC3

Ahora con el fichero .exe generado, vamos a obtener el código máquina usando la siguiente web.

asm1

Y finalmente obtenemos el código fuente que la maquina es capaz de entender (HELLO EXEes un fichero de texto plano).

Con este pequeño ejemplo, hemos aprendido la diferencia entre un lenguaje de programación interpretado como Javascript y otro compilado como C.

Ahora que ya sabemos lo que significa y que ventajas ofrece el proceso de compilación, podemos continuar.

Continuando con V8

Con lo aprendido hasta ahora mismo podríamos pensar que un lenguaje compilado es mejor que uno interpretado, pero vamos a ver que cada uno tiene puntos fuertes y débiles.

INTERPRETADO COMPILADO
Pros Contras Pros Contras
  • Se inicia muy rápido, ya que no es necesario compilar el código.
  • El engine recibe el fichero, lo interpreta y lo ejecuta.
  • Como es interpretado, según aumente la cantidad de código, este puede volverse muy lento es su ejecución.
  • En una compilación se ha optimizado para evitar esa comprobación constante.
  • El código generado esta muy optimizado, evitando la repetición de código con mismo resultados, ya que previamente lo analiza y optimiza.
  • Tiene que realizar un proceso previo, haciendo que su arranque e inicio sea mucho mas lento.

En este punto lo interesante sería tener lo mejor de ambos «mundos», pudiendo tener un arranque rápido con un código optimizado.

Bien, para ello la empresa Google en el año 2008 combino ambos mundos creando un compilador en tiempo de ejecución, JIT Compiler (Just In Time Compiler).

En la sección «como funciona V8»  en el paso 3 (INTERPRETER) dentro del engine V8 se esta generando nuestro bytecode, que todavía no es un código de bajo nivel como lo es el código máquina.

Aunque ese bytecode ya es comprensible y se puede ejecutar, no esta optimizado.

El siguiente paso es la revisión de el código generado (bytecode) en el paso 3 (INTERPRETER), por un nuevo elemento llamado PROFILER.

El PROFILER se encarga de revisar nuestro código mientras se ejecuta a través del INTERPRETER, y va tomando nota sobre las mejoras que se pueden realizar y como optimizar el código.

Si el PROFILER encuentra código que se puede mejorar entonces lo enviará al COMPILER (compilador).

A continuación un pequeño diagrama del proceso.

v8

Como esta compilación se produce en tiempo de ejecución, tomara ese código no optimizado y lo optimizara, para luego remplazar las partes que se pueden mejorar en el código final.

Debemos tener en cuenta que este proceso se esta realizando constantemente, de tal forma que siempre se debería tener la mejor versión del código máquina generado.

Un poco mas de V8

En este punto ya entendemos que es un lenguaje interpretado y compilado, y como Google con su engine V8 introdujo el JIT Compiler, mezclando ambos mundos para obtener la mejor versión del código.

Dentro del engine V8 nos encontramos otros elementos que son necesarios para que el engine funcione como debe, y los vamos a detallar a continuación:

  • Call stack
  • Callback queue
  • Memory Heap
  • Event loop
  • Web APIs

Call Stack y Memory Heap

Como habíamos explicado al principio, Javascript solo tienen un único hilo de ejecución y además tiene un contexto de ejecución global.

Esto significa que Javascript solo nos permite tener una pila de llamadas (call stack) y una pila de memoria (memory heap) donde almacenamos la información.

En la pila de memoria (memory heap) cada vez que definimos una variable esta se almacena, ya sea del tipo string, number, boolean, object, etc…

const text = 'Hello world';
const status = false;
const age = 100;
const data = {
   x: 100,
   y: 'test',
};

Todos los datos que se están almacenando en la memoria, cuando ya no son necesarios Javascript se encarga de eliminarlos por nosotros.

Aunque el proceso de eliminación es automático como hemos dicho antes, debemos tener en cuenta que Javascript bloquea las zonas de memoria que están en uso para evitar fugas de memoria (memory leaks).

Esta gestión automática que realiza el recolector de basura (garbage collector) por parte de Javascript es muy cómoda, pero puedes imaginar lo que implica esa gestión automática, y es que si no tenemos cuidado podemos aumentar la memoria en uso y que suceda un desbordamiento de la pila (stack overflow).

Si necesitáis información detallada sobre la gestión de memoria en Javascript, desde este enlace podéis acceder a toda la información sobre el flujo, técnica y algoritmos que usan para realizar esa gestión.

En la pila de llamadas (call stack) debemos saber que Javascript usa para la gestión de llamadas del call stack y su procesamiento, la técnica LIFO (Last Input – First Output).

A continuación un pequeño video para poner en practica la teoría.

Ahora que sabemos como funciona, debemos estar muy atentos con la pila de llamadas (call stack), ya que debemos controlar correctamente la ejecución de nuestro código para evitar un desbordamiento de la pila (stack overflow).

A continuación mediante recursividad vamos a reproducir el fallo, que aunque en este caso es muy evidente, nos sirve como ejemplo para cuando nos tengamos que enfrentar a ello.

 

Event Loop y Callback Queue

Otros dos elementos que nos encontramos dentro del engine V8, son el Event Loop y el Callback Queue.

El Callback Queue es una pila/cola donde se van añadiendo los procesos que requieren de un mayor tiempo de procesamiento o simplemente quedan a la espera de una respuesta, como puede ocurrir en la llamada a un servicio externo.

En este punto entra el Event Loop, que no es mas que un observador que controla todo lo que esta pendiente en el Callback Queue, y lo añade al Call Stack cuando este quede vacío.

A continuación un pequeño video explicando como funciona usando la siguiente herramienta.

Web APIs

Las Web APIs no son mas que interfaces y una serie de objetos que nos ofrece nuestro navegador para desarrollar aplicaciones.

Con ellas podemos acceder a decenas de interfaces que nos permiten desde el propio navegador por ejemplo, usar el audio de nuestro equipo, acceso a la cámara, nuestra posición, etc…

En el siguiente enlace podéis acceder a todas las APIs que existen actualmente, aunque algunas estén en fase experimental.

Optimizando nuestro Javascript

Ahora que conocemos las tripas del engine V8, lo interesante sería escribir código Javascript que sea mas óptimo y que nuestro engine pueda optimizar mucho mejor.

A continuación vamos a definir algunas pautas que parecen evidentes, pero que según crece el proyecto y pasa el tiempo suelen obviarse:

  • Tener el código actualizado es importante, ya que muchas veces se continua creando código que con el tiempo y diferentes mejoras queda desactualizado o en el peor de los casos cae en el olvido.
  • Las comparaciones se realizan de izquierda a derecha, esto nos permite hacer evaluaciones mucho mas óptimas ya que si no se cumpliera la primera condición el resto no se evaluaría.
    • const a = false;
      const b = true;
      const c = true;
      
      // Primera condición no se cumple 
      a && b && c && alert('To do');
      
  • Aprovechar la interface performance, para evaluar el rendimiento de nuestro código y saber que podemos mejorar.
    • (() => {
          // Rellenando un array con valores para comprobar el performance
          let arr = [];
          for (let cont = 0; cont < 1000000; cont++) {
              arr.push(cont);
          }
          const len = arr.length;
          
          const ta1 = performance.now();
          for (let cont = 0; cont < arr.length; cont++) {}
          const ta2 = performance.now();
      
          const tb1 = performance.now();
          for (let cont = 0; cont < len; cont++) {}    
          const tb2 = performance.now();
          
          function Resuts(t1, t2) {
              this.CALCULATED = t1;
              this.STORED = t2;
          }
          const result = new Resuts((ta2 - ta1), (tb2 - tb1));
          console.table(result);
      
      })();
  • Almacenar un valor que no va a cambiar, ya que evita la consulta y su correspondiente tiempo en obtener y calcular el valor. En el ejemplo anterior podemos ver un claro ejemplo de su uso en el ciclo for, calculando cada iteración frente a su asignación
  • El acceso a las propiedades de un objeto usando ‘.’ a usar ‘[]’, es un tema que me llamo la atención bastante, pero es cierto que los tiempos son menores cuando usamos ‘[]’ que cuando usamos ‘.’ para acceder a las propiedades.
    • (() => {
          const obj = {
              a: 100,
              b: true,
          };
          const ta1 = performance.now();
          for (let cont = 0; cont < 1000; cont++) {
              const a = obj.a;
              const b = obj.b;
          }
          const ta2 = performance.now();
      
          const tb1 = performance.now();
          for (let cont = 0; cont < 1000; cont++) {
              const a = obj['a'];
              const b = obj['b'];
          }    
          const tb2 = performance.now();
          
          function Resuts(t1, t2) {
              this.DOT = t1;
              this.SQUARE_BRACKET = t2;
          }
          const result = new Resuts((ta2 - ta1), (tb2 - tb1));
          console.table(result);
      })();
  • El proceso de iteración sobre algunos elementos, aunque se puede iterar de múltiples formas existen algunas que son mas óptimas que otras. También es cierto que en ocasiones, muchas de estas pruebas se hacen con grandes cantidades de información y quizás el código que ahorramos frente a el código mas óptimo no compense.
    • (() => {
          const newArray = (len = 100) => {
              let arr = [];
              for (let cont = 0; cont < len; cont++) {
                  arr.push(cont);
              }
              return arr;
          };
          const speedTest = cb => {
              const t1 = performance.now();
              cb();
              const t2 = performance.now();
              return t2 - t1;
          };
          
          const arr = newArray(10000);
      
          const t1 = speedTest(function() {
              const len = arr.length;
              for (let cont = 0; cont < len; cont++) {
                  arr[cont]
              }   
          });
          const t2 = speedTest(function() {
              arr.forEach(item => item);
          })
          const t3 = speedTest(function() {
              arr.map(item => item);
          })
          const t4 = speedTest(function() {
              let cont = 0;
              let len = arr.length;
              while (cont < len) {
                  arr[cont];
                  cont++;
              }
          });
          const t5 = speedTest(function() {
              for (let data of arr) {
                  data;
              }
          })
          console.table([['FOR',t1], ['FOREACH', t2], ['MAP', t3], ['WHILE', t4], ['FOR_OF', t5]]);
      })();

Aquí solo he puesto algunas pruebas de código para que entendáis que siempre se puede mejorar siguiendo buenas practicas, buscando información, investigando y practicando mucho.

Además existen muchas formas de optimizar nuestro código, ya no solamente a nivel de algoritmia y una buena codificación, también existen diferentes herramientas que nos permiten generar un código mas reducido y optimizado.

Bien con esto finalizamos la primera de una serie de entradas enfocadas exclusivamente a Javascript.

Javascript ES12 – ES2021

Javascript ES12 – ES2021

Mientras termino de redactar un nuevo curso, continuo ampliando el conocimiento sobre Javascript y para ellos vamos a ver las últimas features que se han incluido en la última versión de Javascript ES12 (ES2021).

A continuación veremos las features que se han incluido en esta nueva versión:

  • replaceAll()

    • Esta nueva feature nos permite remplazar todas las coincidencias de una cadena de texto.
    • const text = "Tu contraseña ha cambiado. Recuerda actualizar tu contraseña y guardar tu contraseña en un sitio seguro";
      
      text.replace('contraseña', 'PASSWORD')
      // "Tu PASSWORD ha cambiado. Recuerda actualizar tu contraseña y guardar tu contraseña en un sitio seguro"
      
      text.replaceAll('contraseña', 'PASSWORD')
      // "Tu PASSWORD ha cambiado. Recuerda actualizar tu PASSWORD y guardar tu PASSWORD en un sitio seguro"
  • Operador de asignación lógica

    • Este operador nos permite combinar los operadores lógicos ??, || && con una asignación.
    • &&=, la asignación solo se realiza si el valor es true.
    • ||=, la asignación solo se realiza si el valor es false.
    • ??=, la asignación solo se realiza si el valor es null undefined, siendo su comportamiento similar al operator nullish coalescing.
    • let status = true;
      let result = 'Hello';
      
      status &&= result;
      console.log('&&=', result, status); // Hello Hello
      
      status = false;
      status &&= result;
      console.log('&&=', result, status); // Hello false
      
      status = true;
      status ||= result;
      console.log('||=', result, status); // Hello true
      
      status = false;
      status ||= result;
      console.log('||=', result, status); // Hello Hello
      
      status = null;
      status ??= result;
      console.log('??=', result, status); // Hello Hello
      
      status = undefined;
      status ??= result;
      console.log('??=', result, status); // Hello Hello
      
      status = 0;
      status ??= result;
      console.log('??=', result, status); // Hello 0
  • Promise.any

    • Nos permite gestionar un array de promesas. capturando la primera que se resuelva satisfactoriamente.
    • En el caso de que ninguna se resuelva ninguna, nos devolverá un array de excepciones.
    • Existe otro método llamado .race() que funciona igual, solo que en este caso no importa si falla o resuelve.
    • (async () => {
          const fn = (status, time) => {
              return new Promise((resolve, reject) => {
                  setTimeout(() => {
                     status ? resolve('OK') : reject('FAIL');
                     resolve(status);
                  }, time);
              });
          };
          
          // Promise.any [OK ===> OK]
          try {
              const arr = [fn(true, 1000), fn(false, 100)]; // Se cumple antes el fallo, pero espera a la correcta
              const result = await Promise.any(arr);
              console.log('OK ===>', result);
          } catch(err) {
              console.log('ERR ===>', err);
          }
      
          // Promise.any (todas fallan) [ERR ===> AggregateError: All promises were rejected]
          try {
              const arr = [fn(false, 1000), fn(false, 100)]; // Se cumple antes el fallo, pero espera a la correcta
              const result = await Promise.any(arr);
              console.log('OK ===>', result);
          } catch(err) {
              console.log('ERR ===>', err);
          }
      
          // Promise.race [ERR ===> FAIL]
          try {
              const arr = [fn(true, 1000), fn(false, 100)]; // Se primera que se cumpla
              const result = await Promise.race(arr);
              console.log('OK ===>', result);
          } catch(err) {
              console.log('ERR ===>', err);
          }
          
      })();
  • Separadores numéricos

    • Con esta nueva feature podemos tener literales numéricos más legibles usando el carácter ‘_‘.
    • const numero = 1_000_000_000; // 1000000000
      
  • Intl.ListFormat

    • El objeto Intl se encuentra dentro del API de internacionalización de ECMAScript incluido en la versión anterior.
    • En esta nueva versión han incluido el nuevo constructor ListFormat, que permite formatear un array de strings en un string.
    • Puedes encontrar mas información aquí
    • const nombres = ['Iván', 'Gustavo', 'Juan Manuel'];
      const language = 'es';
      
      const formatA = new Intl.ListFormat(language, { style: 'long', type: 'conjunction' });
      console.log(formatA.format(nombres)); // Iván, Gustavo y Juan Manuel
      
      const formatB = new Intl.ListFormat(language, { style: 'long', type: 'disjunction' });
      console.log(formatB.format(nombres)); // Iván, Gustavo o Juan Manuel
  • Métodos de clase privados

    • En esta nueva revisión también han incluido métodos privados usando como prefijo a su definición la ‘#‘.
    • class Persona {
          #addPrivate(val) {
              console.log('PRIVATE...', val);
          }
          addPublic(val) {
              console.log('PUBLIC....', val);
          }
      }
      const P1 = new Persona();
      P1.addPublic(100);  // PUBLIC.... 100
      P1.addPrivate(200); // Uncaught TypeError: P1.addPrivate is not a function
      
      

Javascript y las mejoras en ES11 (ES2020) – Parte 7

Las nuevas features de Javascript ES11 (ES2020)

Con esta nueva entrada ampliamos el tutorial de Javascript y su mejoras, añadiendo las últimas features que se han incluido en la última versión de Javascript ES11 (ES2020).

Debemos recordar que estas features puede que no se encuentren disponibles en todos los navegadores, pero usando usando Babel en su versión 7.8 o superior (versión actual 7.11.0) podremos usarlas sin problemas.

A continuación veremos las nuevas features que se han incluido:

  • Operador Nullish Coalescing

    • El nuevo operador ?? lógico, es parecido al operador OR salvo que este solo comprueba verdaderos valores nulos (null y undefined).
    • console.log(undefined ?? 'resultado'); // resultado
      console.log(null ?? 'resultado');      // resultado
      console.log(NaN ?? 'resultado');       // NaN
      console.log('' ?? 'resultado');        // 
      console.log(false ?? 'resultado');     // false
      console.log(0 ?? 'resultado');         // 0
      
      console.log(undefined || 'resultado'); // resultado
      console.log(null || 'resultado');      // resultado
      console.log(NaN || 'resultado');       // resultado
      console.log('' || 'resultado');        // resultado
      console.log(false || 'resultado');     // resultado
      console.log(0 || 'resultado');         // resultado
  • Optional Chaining

    • Usando esta nueva sintaxis podemos acceder a cualquier propiedad y cualquier nivel de un objeto sin tener que preocuparnos, si existe o no.
    • En el caso de no existir nos devuelve undefined, ademas esta sintaxis nos permite también aplicarlas en funciones y arrays.
    • const data = {
          nombre: 'aaaa',
          estudios: [
          {
              curso: 'qqq',
              nota: 10
          },
          {
              curso: 'www',
              nota: 9
          }
          ],
          getNombre() {
              return this.nombre;
          },
          experiencia: ['pppp', 'oooo', 'iiii'],
      };
      
      console.log(data?.nombre);                   // aaaa
      console.log(data?.nombre2);                  // undefined
      console.log(data?.estudios?.[0]);            // { curso: "qqq", nota: 10 }
      console.log(data?.estudios?.[3]);            // undefined
      console.log(data?.estudios2?.[0]);           // undefined
      console.log(data?.estudios?.[0]?.curso);     // qqq
      console.log(data?.estudios?.[0]?.nota);      // 10
      console.log(data?.estudios?.[0]?.nota2);     // undefined
      console.log(data?.getNombre?.());            // aaaa
      console.log(data?.getNombre2?.());           // undefined
      console.log(data?.experiencia?.[0]);         // pppp
      console.log(data?.experiencia?.[10]);        // undefined
      console.log(data?.experiencia2?.[0]);        // undefined
  • globalThis

    • Cuando necesitamos acceder al objeto this nos podemos encontrar en la siguientes situaciones dependiendo del entorno:
      • Nnavegador: window es el objeto this global.
      • Nodejs: global es el objeto this global.
      • Web worker: self es el objeto this global.
    • Con globalThis ya tenemos una forma estandarizada de acceder al objeto this global.
    • console.log(globalThis === window); // true
  • String.prototype.matchAll

      • Este nuevo método nos devuelve un iterador con todos los resultados coincidentes en una cadena de texto usando una expresión regular.
      • console.log([...'patólogo, patóloga, pan, patógeno, pares'.matchAll(/pat/g)]);
        
        /*
        [Array(1), Array(1), Array(1)]
        [
           ["pat", index: 0, input: "patólogo, patóloga, pan, patógeno, pares", groups: undefined],
           ["pat", index: 10, input: "patólogo, patóloga, pan, patógeno, pares", groups: undefined],
           ["pat", index: 25, input: "patólogo, patóloga, pan, patógeno, pares", groups: undefined]
        ]
        */
  • BigInt

    • Esta nueva feature nos permite superar el limite que tiene actualmente Javascript para manejar valores enteros.
    • El límite actual de un número entero es 9007199254740991, pero ahora añadiendo una n al final del número entero podemos crear números enteros más grandes.
    • let nMax = Number.MAX_SAFE_INTEGER;
      console.log(nMax); // 9007199254740991
      nMax++;
      console.log(nMax); // 9007199254740991
      
      let nMore = 9007199254740991n;
      console.log(nMore); // 9007199254740991n
      nMore++;
      console.log(nMore); // 9007199254740992n
      nMore++;
      console.log(nMore); // 9007199254740993n
      nMore++;
      console.log(nMore); // 9007199254740994n
      nMore++;
      console.log(nMore); // 9007199254740995n
      nMore++;
      console.log(nMore); // 9007199254740996n
      nMore++;
      console.log(nMore); // 9007199254740997n
      
      
  • Promise.allSettled

    • Actualmente Javascript tiene el método Promise.all que funciona igual que Promise.allSettled pero con una pequeña diferencia:
      • Promise.all: Si falla alguna de las promesas, la ejecución fallaría inmediatamente con el valor de la promesa que fallo descartando el resto de las demás promesas hayan sido o no cumplidas.
      • Promise.allSettled: Con este método todas las promesas se ejecutaran independientemente de si su estado es resolved o rejected, devolviendo en un array con el resultado de cada una de las promesas.
    • // Promise.all
      const promise1 = Promise.resolve(1);
      const promise2 = Promise.resolve(2);
      const promise3 = Promise.reject(3);
      const arrayPromise = [promise1, promise2, promise3];
      
      Promise.all(arrayPromise).then(result => console.log('OK', result)).catch(err => console.log('ERROR', err));
      /*
          ERROR 3
          Script snippet %2340:1 Promise {<fulfilled>: undefined}
      */
      
      Promise.allSettled(arrayPromise).then(result => console.log('OK', result)).catch(err => console.log('ERROR', err));
      /*
          OK
          0: {status: "fulfilled", value: 1}
          1: {status: "fulfilled", value: 2}
          2: {status: "rejected", reason: 3}
      */
  • Imports dinámicos

    • Los imports dinámicos nos permiten importar módulos dinámicamente de forma nativa, tal como hace Webpack o Babel.
    • import('./tools.js').then(module => {
          module.capitalize();
      });
      
      (async () => {
          const module = await('./tools.js');
          module.capitalize();
      })();
  • Import.meta

    • El objeto import.meta nos permite acceder a los metadatos de un módulo.
    • <script src="./tools.js" type="module"></script>
      console.log(import.meta);  // { url: "file:///www/libs/tools.js" }

       

Esta nueva revisión nos ha traído algunas mejoras que facilitan el trabajo y que la comunidad frontend echaba en falta.

Tutorial de Javascript y las mejoras desde ES6 (ES2015) hasta ES10 (ES2019) – Parte 6 (Features)

Las nuevas features de Javascript desde ES6 hasta ES10

En esta nueva entrada veremos todas (o casi todas) las nuevas características que esta ofreciendo Javascript.

Comencemos con la versión que mas cambios ha traido al Front End, ES6 (2015).

  • let
    • Podemos decir que se parece a la forma de usar var pero con alguna diferencia.
    • Esta nueva característica se define un alcance de bloque (block scope), de tal forma que la variable solo sera accesible desde el bloque donde se defina.
    • // Uso de VAR
      (() => {
          console.log('--- VAR ---');
          var numero = 100;
          console.log('OUT:', numero);
          {
              var numero = 1000;
              console.log('IN:', numero);
          }
          console.log('OUT 2:', numero);
      })();
      --- VAR ---
      OUT: 100
      IN: 1000
      OUT 2: 1000
      
      /* 
         Esto es debido al "hoisting", que es un comportamiento por defecto que tiene Javascript 
         moviendo todas las declaraciones al principio de la función o hasta el inicio del objeto window.
      */
      (() => {
          console.log('--- LET ---');
          let numero = 100;
          console.log('OUT:', numero);
          {
              let numero = 1000;
              console.log('IN:', numero);
          }
          console.log('OUT 2:', numero);
      })();
      --- LET ---
      OUT: 100
      IN: 1000
      OUT 2: 100

       

  • const
    • Como ocurre con let, también tiene un alcance de bloque (block scope).
    • Ahora podemos definir un valor constante, hasta cierto punto ya que no se permite reasignar un nuevo contenido.
    • Esto significa que:
      • Si el valor es de tipo primitivo no podríamos reasignar ya que en los tipos primitivos la asignación se hace por valor.
      • Si el valor es de tipo objeto entonces si podríamos reasignar ya que en los tipos objetos la asignación se hace por referencia.
    • const number = 10;
      const data = { number: 10 };
      data.number = 100;
      number = 100; // Script snippet %2321:4 Uncaught TypeError: Assignment to constant variable.
      
      
  • block scoped (alcance de bloque)
    • En versiones anteriores de Javascript se podía emular el alcance de un bloque envolviendo la función
    • En las nuevas versiones ya no es necesario
    • {
          function bool() {
              return true;
          }
          console.log(bool());
          // true
          {
              function bool() {
                  return false;
              }
              console.log(bool());
              // false
          }
          console.log(bool());
          // true
      }
  • arrow function (función flecha)
    • Algunas características
      • Tiene una sintaxis mas corta.
      • No tiene this propio.
      • No tiene arguments.
      • Siempre es anónima.
      • No se pueden usar como constructor.
      • Los paréntesis son opcionales si solo tiene un argumento, para el resto son obligatorios.
      • Si no tiene un bloque definido el return esta implícito, en caso contrario debería incluir un return de forma explícita.
      • Se pueden asignar parámetros por defecto.
      • const formaAbreviada = (a = 1, b = 2) => a + b;
        console.log(formaAbreviada());                       // 3
        console.log(formaAbreviada(5));                      // 7
        console.log(formaAbreviada(10, 10));                 // 20
        
        const contexto = msn => `${this}, ${msn}`;
        console.log(contexto('test'));                       // [object Window], test
        
        const bool = () => true;
        console.log(bool());                                 // true
        
        const multiple = (a = 1, b = 2, c = 3) => {
            let aa = a * 2;
            let bb = b * 3;
            let cc = c * 4;
            return aa + bb + cc;
        }
        console.log(multiple());                             // 20
        
        const objetoDefault = (nombre = '---', edad = '0') => ({
            nombre,
            edad,
        });
        console.log(objetoDefault());                        // { nombre: "---", edad: "0" }
        console.log(objetoDefault('aaaaa', 100));            // { nombre: "aaaaa", edad: 100 }
  • extend parameter handling (manejo extendido de parámetros)
    • Esta nueva característica incluye:
      • Default parameter value (valor de parámetros por defecto)
        • Permite añadir valores por defecto en las funciones
        • console.log('--- Default parameter value ---');
          const suma = (a = 10, b = 20) => a + b;
          // Valores por defecto
          console.log(suma());       // 30
          // Si incluye argumento sobreescribe el valor por defecto
          console.log(suma(2));      // 22
          // Si incluye argumento sobreescribe el valor por defecto
          console.log(suma(2, 5));   // 7
          
      • Rest parameter (resto de parametros)
        • Nos permite reunir los parámetros definidos en una función mediante el uso de ‘‘ seguidos del nombre de la matriz.
        • Muy importante incluir al final de la definición.
        • const fnSuma_1 = (a, b, ...resto) => {
              console.log('a', a);
              console.log('b', b);
              console.log('resto', resto);
              return a + b + resto.reduce((acc, curr) => acc + curr);
          }
          console.log(fnSuma_1(1,2,3,4,5,6,7,8));
          /*
          a 1
          b 2
          resto (6) [3, 4, 5, 6, 7, 8]
          36
          */
          
          const fnSuma_2 = (a, ...resto) => {
              console.log('a', a);
              console.log('resto', resto);
              return a + resto.reduce((acc, curr) => acc + curr);
          }
          console.log(fnSuma_2(1,2,3,4,5,6,7,8));
          /*
          a 1
          resto (7) [2, 3, 4, 5, 6, 7, 8]
          36
          */
      • Spread operator (operador de propagación)
        • Podemos usarlo en una función para agrupar múltiples argumentos (rest operator)
        • Podemos usarlo para generar una lista de valores a partir de una array o un string.
        • // ARRAY
          let arr1 = [1, 2, 3];               
          let arr2 = [4, 5, 6];               
          let arr3 = [...arr1, ...arr2];      
          console.log(arr1);                  // [1, 2, 3]
          console.log(arr2);                  // [4, 5, 6]
          console.log(arr3);                  // [1, 2, 3, 4, 5, 6]
          console.log(...arr1);               // 1 2 3
          console.log(...arr2);               // 4 5 6
          console.log(...arr3);               // 1 2 3 4 5 6
          
          // STRING
          let text1 = 'Prueba';
          console.log([...text1]);            // ["P", "r", "u", "e", "b", "a"]
  • Template literals (plantillas de texto)
    • Son literales que permiten usar expresiones incrustadas y para crearlas se usa la tilde invertida (`.).
    • Para incrustar las expresiones usaremos la siguiente sintaxis: ${expresión}
    • const datos = {
          a: 1,
          b: 2,
          c: true,
          d: 'saludo',
      };
      const msn = `El valor de a es ${datos.a}
                   El valor de b es ${datos.b}
                   ${datos.c ? datos.d : 'false'}`;
       
      console.log(msn);
      
      /*
      El valor de a es 1
                   El valor de b es 2
                   saludo
      */
  • Enhanced Object Properties (Propiedades de objeto mejoradas)
    • Esta nueva característica nos permite de una forma simple crear objetos usando notación literal, y como consecuencia una codificación mas clara.
      • Property shorthand (Propiedades «abreviadas»): Sintaxis más corta para la de definición de propiedades de un objeto.
      • let texto = 'texto';
        let estado = false;
        
        const ES6 = {
           texto,
           estado,
        };
        
      • Computed property names (Nombres de propiedades calculadas): Nombres calculados en la definición de las propiedades de un objeto.
      • const m = 'methods';
        
        const ES6_1 = {
            a: true,
            [m]: 100,
        };
        /*
        {
           a: true
           methods: 100
        }
        */
        
        const ES6_2 = {
            aa: true,
        };
        for (let cont = 0; cont < 10; cont++) {
            ES6_2[`${m}_${cont}`] = cont;
        }
        /*
          aa: true
          methods_0: 0
          methods_1: 1
          methods_2: 2
          methods_3: 3
          methods_4: 4
          methods_5: 5
          methods_6: 6
          methods_7: 7
          methods_8: 8
          methods_9: 9
        */
        
      • Method Properties (): Notación de métodos en la definición de las propiedades de un objetos.
      • const ES6_3 = {
            suna (a, b) {
                return a + b;
            },
            resta (a, b) {
                return a - b;
            }
        };
        
        ES6_3.suna(2,3);
        // 5
        ES6_3.resta(2,3);
        // -1
  • Destructuring Assignment (Asignación de Desestructuración)
    • Es una nueva característica que nos permite extraer de un array u objeto sus variables.
    • A continuación veremos las múltiples formas de extraer esa información
      • Array matching (Parear arrays): Desestructuración de arrays en variables durante la asignación.
      • const semana = ['Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes', 'Sabado', 'Domingo'];
        const [l, m, x, j, v, s, d] = semana;
        
        console.log(semana);
        // (7) ["Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"]
        
        console.log(l, m, x, j, v, s, d);
        // Lunes Martes Miercoles Jueves Viernes Sabado Domingo
      • Object Matching (Parear objetos): Desestructuración de objetos en variables durante la asignación.
      • const semana = {
            l: 'Lunes', 
            m: 'Martes', 
            x: 'Miercoles', 
            j: 'Jueves', 
            v: 'Viernes', 
            s: 'Sabado', 
            d: 'Domingo',
        };
        const {l, m, x, j, v, s, d} = semana;
        
        console.log(semana);
        /*
        {
           l: "Lunes"
           m: "Martes"
           x: "Miercoles"
           j: "Jueves"
           v: "Viernes"
           s: "Sabado"
           d: "Domingo"
        */
        
        console.log(l, m, x, j, v, s, d);
        // Lunes Martes Miercoles Jueves Viernes Sabado Domingo
      • const datos = {
            nombre: 'nombre',
            edad: 20,
            direccion: {
                nombre: 'Calle Pino',
                numero: 5,
                planta: 3,
                puerta: 'A',
                poblacion: 'Población',
                Provincia: 'Provincia',
                cp: '12122',
            },
            estudios: {
                reglados: {
                    nombre: 'X',
                    duracion: 4,
                },
            },
        };
        
        const {
            direccion: {
                nombre,
                numero,
                poblacion,
                provincia,
            },
            estudios: {
                reglados,
            },
        } = datos;
        
        console.log(nombre, numero, poblacion, provincia, reglados);
        // Calle Pino 5 Población undefined {nombre: "X", duracion: 4}
        
      • Default values in object and array (Valores defecto en objetos y arrays): Añadir valores defecto a la desestructuración de arrays y objetos
      • const infoObject = {
            a: 1,
            b: false,
        };
        const infoObjectTemp = {
            at: 1,
        };
        const infoArray = [1, 2, 3];
        
        const {a, b = true } = infoObject;
        console.log(a,b);
        // 1 false
        
        const {at, bt = true } = infoObjectTemp;
        console.log(at, bt);
        // 1 true
        
        const [ar = 0, br = 0, cr = 0, dr = 0] = infoArray;
        console.log(ar, br, cr, dr);
        // 1 2 3 0
      • Parameter Context Matching (Parear los parámetro del contexto): Añadir el pareo y valores por defecto en las llamadas a las funciones de objetos y arrays
      • const sumaArr = ([param1, param2]) => param1 + param2;
        console.log(sumaArr([1, 9]));
        // 10
        
        const objTest_1 = ({param1 = 'default', param2 = 'default'}) => ({ param1, param2});
        console.log(objTest_1({}));
        // {param1: "default", param2: "default"}
        
        console.log(objTest_1({param1: 1}));
        // {param1: 1, param2: "default"}
        
        console.log(objTest_1({param1: 1, param2: 9}));
        // {param1: 1, param2: 9}
        
        const objAlias_1 = ({param1: p1 = 'default', param2: p2 = 'default'}) => ({ p1, p2});
        console.log(objAlias_1({}));
        // {p1: "default", p2: "default"}
        
        console.log(objAlias_1({param1: 1}));
        // {p1: 1, p2: "default"}
        
        console.log(objAlias_1({param1: 1, param2: 9}));
        // {p1: 1, p2: 9}
  • Modules (Módulos)
    • Es una forma de compartir y encapsular nuestro código.
      • Export / import: Podemos exportar e importar módulos
      • // string.js
        export upper = (msn = '') => msn.toUpperCase();
        export lower = (msn = '') => msn.toLowerCase();
        
        // script.js
        import * as str from 'string.js';
        
        str.upper('Hola');
        // HOLA
        
        str.lower('Hola');
        // hola
        
        // newScript.js
        import {upper, lower} from 'string.js';
        
        str.upper('Hola');
        // HOLA
        
        str.lower('Hola');
        // hola
      • Default: Valor por defecto
      • // string.js
        export default (msn = '') => msn.toUpperCase();
        
        // script.js
        import upper from 'string.js';
        upper('Hola');
        // HOLA
  • Classes (Clases)
    • Con esta nueva característica podemos definir clases usando el estilo de programación orienta a objetos clásica.
      • Class definition (Definición de clases): Definición de clases.
      • class Persona {
            constructor(name, age) {
                this.name = name;
                this.age = age;
            }
        }
        
        const p1 = new Persona('a', 1);
        console.log(p1);
        // Persona {name: "a", age: 1}
        
        const p2 = new Persona('b', 2);
        console.log(p2);
        // Persona {name: "b", age: 2}
        
      • Inheritance (Herencia): Usando herencia
      • class Persona {
            constructor(name, age) {
                this.name = name;
                this.age = age;
            }
        }
        class Profession extends Persona {
            constructor(name, age, jobPosition, salary) {
                super(name, age);
                this.jobPosition = jobPosition;
                this.salary = salary;
            }
        }
        
        const p1 = new Profession('a', 1, 'developer', 40000);
        console.log(p1);
        // Profession {name: "a", age: 1, jobPosition: "developer", salary: 40000}
      • Static class members (Miembros de clase estática): Acceso sencillo a los miembros estáticos de una clase.
      • class Persona {
            constructor(name, age) {
                this.name = name;
                this.age = age;
            }
            saludo() {
                return `Hola ${this.name} y tienes ${this.age} años`;
            }
            static welcome() {
                return 'Bienvenidos, método estático';
            }
        }
        
        console.log(Persona.welcome());
        // "Bienvenidos, método estático"
        
        const p1 = new Persona('a', 1);
        console.log(p1.saludo());
        // "Hola a y tienes 1 años"
      • Getters / Setters: Getters y setters dentro de las clases.
      • class Persona {
            constructor(name, age) {
                this._name = name;
                this._age = age;
            }
            saludo() {
                return `Hola ${this._name} y tienes ${this._age} años`;
            }
            static welcome() {
                return 'Bienvenidos, método estático';
            }
            get name() {
                return this._name;
            }
            set name(value) {
                this._name = value;
            }
            get age() {
                return this._age;
            }
            set age(value) {
                this._age = value;
            }
        }
        
        console.log(Persona.welcome());
        // "Bienvenidos, método estático"
        
        const p1 = new Persona('a', 1);
        console.log(p1);
        // Persona {_name: "a", _age: 1}
        
        console.log(p1.name);
        // "a"
        
        console.log(p1.age);
        // 1
        
        p1.name = 'aaaaaa'
        console.log(p1);
        // Persona {_name: "aaaaaa", _age: 1}
        
        p1.age = 1000
        console.log(p1);
        // Persona {_name: "aaaaaa", _age: 1000}
  • Symbol
    • Es un tipo de dato primitivo, único e inmutable.
      • Se le puede pasar un parámetro pero es opcional.
      • Aunque tengan la misma descripción, siguen siendo únicos.
      • const COL_NAME_USERS = Symbol('name');
        const COL_NAME_USERS_2 = Symbol('name');
        const COL_SURNAME_USERS = Symbol('surname');
        const COL_SURNAME_USERS_2 = Symbol('surname');
        
        console.log(COL_NAME_USERS === COL_NAME_USERS_2);
        // false
        console.log(COL_SURNAME_USERS === COL_SURNAME_USERS_2);
        // false
  • Iterators (Iteradores)
    • Es un objeto que nos permite recorrer una colección mediante el método next(), para devolvernos al final un objeto con dos propiedades.
      • value: Corresponde al siguiente valor de la iteración
      • done: Cuando esta propiedad esta a true entonces significa que termino la iteración, mientras tanto nos devolverá false y la propiedad value.
      • const iterar = arr => {
            let index = 0;
            return {
                next: () => {
                    return index < arr.length ? { value: arr[index++], done: false} : {done:true}
                }
            }
        }
        
        const iterador = iterar(['a','b','c',1,2,3]);
        console.log(iterador.next());
        console.log('-------');
        console.log('Primer valor del array', iterador.next().value);
        console.log('Podemos hacer lo que necesitemos ahora')
        for (let cont = 0; cont < 10; cont++) {
            console.log(cont);
        }
        console.log('Siguiente valor del array', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        console.log('Otro', iterador.next().value);
        
        /*
        {value: "a", done: false}
        -------
        Segundo valor del array b
        Podemos hacer lo que necesitemos ahora
        0
        1
        2
        3
        4
        5
        6
        7
        8
        9
        Siguiente valor del array c
        Otro 1
        Otro 2
        Otro 3
        Otro undefined
        Otro undefined
        Otro undefined
        Otro undefined
        Otro undefined
        Otro undefined
        Otro undefined
        */
  • Generators (Generadores)
    • Se puede definir como un iterador especial donde podemos parar y reanudar el flujo de control.
      • function* generate1to10() {
            yield 1;
            yield 2;
            yield 3;
            yield 4;
            yield 5;
            yield 6;
            yield 7;
            yield 8;
            yield 9;
            yield 10;
        }
        const generate = generate1to10();
        
        generate.next()
        // {value: 1, done: false}
        generate.next()
        // {value: 2, done: false}
        generate.next()
        // {value: 3, done: false}
        generate.next()
        // {value: 4, done: false}
        generate.next()
        // {value: 5, done: false}
        generate.next()
        // {value: 6, done: false}
        generate.next()
        // {value: 7, done: false}
        generate.next()
        // {value: 8, done: false}
        generate.next()
        // {value: 9, done: false}
        generate.next()
        // {value: 10, done: false}
        generate.next()
        // {value: undefined, done: true}
      • // Simulate range in Python
        function* range(start = 0, stop = 1, step = 1) {
            let init = start;
            while(init < stop) {
                yield init+=step;     
            }
        }
        
        for (i of range(0, 20, 4)) {
            console.log(i);
        }
        
        const r = range(0, 20, 4);
        console.log(r.next());
        console.log(r.next());
        console.log(r.next());
        console.log(r.next());
        console.log(r.next());
        console.log(r.next());
        
        /*
        4
        8
        12
        16
        20
        {value: 4, done: false}
        {value: 8, done: false}
        {value: 12, done: false}
        {value: 16, done: false}
        {value: 20, done: false}
        {value: undefined, done: true}
        */
  • Map
    • El objeto Map nos permite almacenar pares de clave/valor de cualquier tipo, tanto objetos como primitivos.
      • El objeto Map tiene múltiples métodos y propiedades que nos permiten manipular su contenido
      • const mapTest = new Map();
        const obj = {
            nombre: 'aaa',
            edad: 1,
            estado: false,
        };
        
        mapTest.set('obj', obj);
        mapTest.set('nombre', 'bbb');
        console.log(mapTest);
        // Map(2) {"obj" => {…}, "nombre" => "bbb"}
        
        console.log(mapTest.size)
        // 2
        
        console.log(mapTest.get('nombre'));
        // bbb
        
        console.log(mapTest.get('obj'));
        // {nombre: "aaa", edad: 1, estado: false}
        
        mapTest.delete('obj');
        console.log(mapTest);
        // {"nombre" => "bbb"}
  • Set
    • El objeto Set consta de una colección de valores y solo puede estar una vez el valor de esa colección.
      • Como ocurre con el objeto Map, este nos ofrece también múltiples métodos y propiedades para su manipulación.
      • const setTest = new Set();
        
        setTest.add('a');
        setTest.add(true);
        setTest.add({a:1, status: true});
        
        console.log(setTest);
        // {"a", true, {…}}
        
        console.log(setTest.has(true));
        // true
        
        console.log(setTest.has('a'));
        // true
        
        console.log(setTest.has('b'));
        // false
        
        setTest.delete('a');
        console.log(setTest);
        // {true, {…}}[[Entries]]0: true1: Object2: "a"size: (...)__proto__: Set
        
        setTest.add('a');
        setTest.add('a');
        setTest.add('a');
        setTest.add('a');
        setTest.add('a');
        console.log(setTest);
        // {true, {…}, "a"}
  • Object.assign
    • Con el método Object.assign() podemos copiar los valores de todas las propiedades enumerables de uno o múltiples objetos a un destino. Esta operación nos devuelve el objeto destino.
      • const a = { a: 1 };
        const b = { b: 1 };
        const c = { c: 1 };
        const d = { d: 1 };
        
        const result_1 = { destino: true };
        console.log(result_1);
        // {destino: true}
        
        Object.assign(result_1, a, b, c, d);
        console.log(result_1);
        // {destino: true, a: 1, b: 1, c: 1, d: 1}
        
        const result_2 = Object.assign(result_1, a, b, c, d);
        console.log(result_2);
        // {destino: true, a: 1, b: 1, c: 1, d: 1}
        
        const result_3 = Object.assign({}, result_1, a, b, c, d);
        console.log(result_3);
        // {destino: true, a: 1, b: 1, c: 1, d: 1}
  • Array
    • Array.find(): Nos devuelve el valor de la primera coincidencia que cumpla el criterio.
    • Array.findIndex(): Nos devuelve el indice de la primera coincidencia que cumpla el criterio.
      • [1,2,3,4,5,6,7,8,9].find(v => v > 5)
        // 6
        [1,2,3,4,5,6,7,8,9].findIndex(v => v > 5)
        // 5
        
    • Array.from(): Crea un array a partir de un objeto iterable
      • const arr = Array.from('Prueba de texto');
        console.log(arr);
        // ["P", "r", "u", "e", "b", "a", " ", "d", "e", " ", "t", "e", "x", "t", "o"]
        
      • const cesarCypher = 'Mensaje usando cifrado Cesar';
        const offset = 4;
        const cypher = Array.from(cesarCypher, letter => String.fromCharCode(letter.charCodeAt(0) + offset));
        console.log(cypher);
        // ["Q", "i", "r", "w", "e", "n", "i", "$", "y", "w", "e", "r", "h", "s", "$", "g", "m", "j", "v", "e", "h", "s", "$", "G", "i", "w", "e", "v"]
        console.log(Array.from(cesarCypher));
        // ["M", "e", "n", "s", "a", "j", "e", " ", "u", "s", "a", "n", "d", "o", " ", "c", "i", "f", "r", "a", "d", "o", " ", "C", "e", "s", "a", "r"]
      • const obj = {
            length: 4,
            0: 'a',
            1: 'b',
            2: 1,
            3: false,
        };
        console.log(Array.from(obj));
        // ["a", "b", 1, false]
    • Array.fill(): Cambia todos los elementos de un array por un valor estático, devolviendo un array modificado.
      • const arr = [1,2,3,4,5,6,7,8,9,10];
        console.log(arr.fill(0));
        // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
        console.log(arr.fill('a', 2));
        // [0, 0, "a", "a", "a", "a", "a", "a", "a", "a"]
        console.log(arr.fill(true, 3, 5));
        // [0, 0, "a", true, true, "a", "a", "a", "a", "a"]

         

  • String.repeat(), String.startsWith(), String.endsWith(), String.includes()
    • String.repeat(): Devuelve una cadena nueva que contiene el número especificado de copias de la cadena original (concatenado).
    • String.startsWith(): Devuelve un valor boleano que nos permite saber si una cadena comienza o no con otra cadena (Distingue entre mayúsculas y minúsculas).
    • String.endsWith(): Devuelve un valor boleano que nos permite saber si una cadena termina o no con otra cadena (Distingue entre mayúsculas y minúsculas).
    • String.includes(): Devuelve un valor boleano que nos permite determinar si una cadena de texto se encuentra incluida dentro de la otra.
      • const saludo = "Hola";
        console.log(saludo.repeat(10));
        // HolaHolaHolaHolaHolaHolaHolaHolaHolaHola
        
        const texto = "En un lugar de la Mancha";
        
        console.log(texto.startsWith('Mancha', 19)); // false
        console.log(texto.startsWith('En un'));      // true
        console.log(texto.startsWith('En '));        // true
        
        console.log(texto.endsWith('Mancha'));       // true
        console.log(texto.endsWith('En un', 10));    // false
        console.log(texto.endsWith('En '));          // false
        
        console.log(texto.includes('Mancha'));       // true
        console.log(texto.includes('Avión'));        // false
        console.log(texto.includes('un', 3));        // true
        
  • Promises (Promesas)
    • Es un objeto que representa la resolución o el fallo eventual de una operación asíncrona.
      • const asyncTest = () => {
            return new Promise((resolve, reject) => {
               setTimeout(() => {
                   const rnd = Math.floor(Math.random() * 100);
                   if (rnd < 50) {
                       resolve('OK');
                   } else {
                       reject('FAIL');
                   }
               }, 1000) 
            });
        };
        
        asyncTest().then(data => console.log(data)).catch(err => console.log(err));
        // Promise {<pending>} FAIL
        
        asyncTest().then(data => console.log(data)).catch(err => console.log(err));
        // Promise {<pending>} OK
        
        asyncTest().then(data => console.log(data)).catch(err => console.log(err));
        // Promise {<pending>} FAIL
        
        asyncTest().then(data => console.log(data)).catch(err => console.log(err));
        // Promise {<pending>} OK
      • const asyncTest = () => {
            return new Promise((resolve, reject) => {
               setTimeout(() => {
                   const rnd = Math.floor(Math.random() * 100);
                   if (rnd < 50) {
                       resolve('OK');
                   } else {
                       reject('FAIL');
                   }
               }, 1000) 
            });
        };
        
        Promise.all([asyncTest(),asyncTest()]).then(data => console.log(data)).catch(err => console.log(err))
        // Promise {<pending>} ["OK", "OK"]
        
        Promise.all([asyncTest(),asyncTest()]).then(data => console.log(data)).catch(err => console.log(err))
        // Promise {<pending>} FAIL
        
        Promise.all([asyncTest(),asyncTest()]).then(data => console.log(data)).catch(err => console.log(err))
        // Promise {<pending>} ["OK", "OK"]
      • // API de Star Wars
        
        const baseURL = 'https://swapi.co/api/people/';
        const stack = [];
        for (let cont = 1; cont < 10; cont++) {
            let promesa = fetch(`${baseURL}${cont}/`);
            stack.push(promesa);
        }
        Promise.all(stack)
        .then(response => {
           response.forEach(async res => {
               let result = await res.json();
               console.log(result);
           })
        })
        .catch(err => {
            console.log(err);
        });
        
        /* 
        {name: "Luke Skywalker", height: "172", mass: "77", hair_color: "blond", skin_color: "fair", …}
        {name: "R2-D2", height: "96", mass: "32", hair_color: "n/a", skin_color: "white, blue", …}
        {name: "Darth Vader", height: "202", mass: "136", hair_color: "none", skin_color: "white", …}
        {name: "Leia Organa", height: "150", mass: "49", hair_color: "brown", skin_color: "light", …}
        {name: "Owen Lars", height: "178", mass: "120", hair_color: "brown, grey", skin_color: "light", …}
        {name: "Beru Whitesun lars", height: "165", mass: "75", hair_color: "brown", skin_color: "light", …}
        {name: "R5-D4", height: "97", mass: "32", hair_color: "n/a", skin_color: "white, red", …}
        {name: "Biggs Darklighter", height: "183", mass: "84", hair_color: "black", skin_color: "light", …}
        {name: "C-3PO", height: "167", mass: "75", hair_color: "n/a", skin_color: "gold", …}
        */
  • Proxy, Reflect (Metaprogramación)
    • Proxy
      • Es un objeto que nos permite capturar operaciones de lectura y escritura sobre las propiedades de un objeto.
      • Debemos usar la palabra reservada new ya que es un Constructor.
      • const datos = {
            nombre: '',
            edad: 0,
            estado: false,
        };
        let proxyExample = new Proxy(datos, {
            get: (target, propertyKey, receiver) => {
                if (!target.nombre || target.edad === 0) {
                    throw new Error('Es necesario completar el nombre y/o la edad')
                }
                return target[propertyKey];
            },
            set: (target, prop, value, receiver) => {
                console.log(prop)
                if (prop === 'edad') {
                    if (isNaN(value)) {
                        throw new Error('La edad tiene que ser un valor numérico');
                    }    
                }
                
                // Se puede usar ambas opciones, pero la documentación oficial recomienda hacer uso de del objecto Reflect
                // target[prop] = value;
                Reflect(target, prop, value);
                return true;
            },
        });
        
        
        proxyExample
        // Proxy {nombre: "", edad: 0, estado: false}
        
        proxyExample.nombre
        // Proxy:9 Uncaught Error: Es necesario completar el nombre y/o la edad at Object.get (Proxy:9) at <anonymous>:1:14 get @ Proxy:9
        
        proxyExample.edad
        // Proxy:9 Uncaught Error: Es necesario completar el nombre y/o la edad at Object.get (Proxy:9) at <anonymous>:1:14 get @ Proxy:9
        
        proxyExample.nombre = 'aaa'
        // Proxy:14 nombre "aaa"
        
        proxyExample.edad = 'aaa'
        /*
        edad
        Proxy:17 Uncaught Error: La edad tiene que ser un valor numérico at Object.set (Proxy:17)at <anonymous>:1:19 set @ Proxy:17
        */
        
        proxyExample.edad = 100
        // edad 100
    • Reflect
      • Reflect  es un objecto que nos proporciona métodos para interceptar operaciones de Javascript y con los mismos que el objeto Proxy.
      • A diferencia de otros objetos globales, este no es un constructor, por lo tanto no se puede instanciar usando new invocarse como una función.
      • Todas sus propiedades y métodos son estáticos.
      • const datos = {};
        Reflect.set(datos, 'nombre', 'Texto');
        console.log(datos);
        // {nombre: "Texto"}
        
        datos.apellidos = 'aaa';
        console.log(datos);
        // {nombre: "Texto", apellidos: "aaa"}
        
        const ok = Reflect.set(datos, 'edad', 10)
        console.log(ok);
        // true
        
        console.log(Reflect.get(datos, 'edad'));
        // 10
        
      • function method(msn) { console.log(`Mensaje: ${msn}`, this); }
        
        Reflect.apply(method, undefined, ['Hola 1']);
        // Mensaje: Hola 1 Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
        
        Reflect.apply(method, this, ['Hola 2']);
        // Mensaje: Hola 2 Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
        
        Reflect.apply(method, {a: 1}, ['Hola 3']);
        // Mensaje: Hola 3 {a: 1}
  • Internationalization
    • Intl
      • Este objeto global es el espacio de nombres para el API de Internacionalización.
      • Nos ofrece comparación de cadenas, formato de números, fechas y tiempos.
      • // Números 
        const EN = new Intl.NumberFormat("en-US")
        const DE = new Intl.NumberFormat("de-DE")
        const ES = new Intl.NumberFormat("es-ES")
        
        console.log(EN.format(1234567.89));  // 1,234,567.89
        console.log(DE.format(1234567.89));  // 1.234.567,89
        console.log(ES.format(1234567.89));  // 1.234.567,89
        
        // Moneda
        const USD = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })
        const GBP = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" })
        const EUR = new Intl.NumberFormat("es-ES", { style: "currency", currency: "EUR" })
        
        console.log(USD.format(100200300.40));  // $100,200,300.40
        console.log(GBP.format(100200300.40));  // £100,200,300.40
        console.log(EUR.format(100200300.40));  // 100.200.300,40 €
        
        // Fechas
        const EN = new Intl.DateTimeFormat("en-US");
        const DE = new Intl.DateTimeFormat("de-DE");
        const ES = new Intl.DateTimeFormat("es-ES");
        console.log(EN.format(new Date("2020-12-31")));  // 12/31/2020
        console.log(DE.format(new Date("2020-12-31")));  // 31.12.2020
        console.log(ES.format(new Date("2020-12-31")));  // 31/12/2020
        

Continuamos con la versión ES7 (2016).

  • Array.includes()
    • Comprueba si existe un determinado valor dentro del array, devolviendo un valor boleano a true si existe o false si no existe.
      • console.log([1, 2, 3].includes(4))
        // false
        console.log([1, 2, 3].includes(2))
        // true
        console.log(['a', 'b', 'c'].includes('z'))
        // false
        console.log(['a', 'b', 'c'].includes('c'))
        // true
  • Operador exponencial (**)
    • Eleva el primer operando a la potencia del segundo operando.
      • const operA = 2;
        const operB = 3;
        const operC = operA ** operB;
        console.log(operC);
        // 8

Continuamos con la versión ES8 (2017).

  • String.padStart()
    • Rellena la cadena actual con los valores indicados devolviendo una cadena resultante hasta alcanzar la longitud dada.
    • El relleno se realiza desde el inicio de la cadena.
      • console.log('texto'.padStart(10));
        //      texto
        console.log('texto'.padStart(10, '1'));
        // 11111texto
        
  • String.padEnd()
    • Rellena la cadena actual con los valores indicados devolviendo una cadena resultante hasta alcanzar la longitud dada.
    • El relleno se realiza desde el final de la cadena.
      console.log('texto'.padEnd(10));
      // 'texto     '
      console.log('texto'.padEnd(10, '1'));
      // texto11111
  • Object.values()
    • Devuelve un array que contiene los valores de las propiedades enumerables de un objeto.
      • const obj = {
            nombre: 'a',
            apellidos: 'b',
            edad: 1,
            estado: true,
        };
        const arr = [1,2,3,4,5];
        console.log(Object.keys(obj));
        // ["nombre", "apellidos", "edad", "estado"]
        
        console.log(Object.keys(arr));
        // ['0', '1', '2', '3', '4']
  • Object.entries()
    • Devuelve un array que contiene los valores de las propiedades enumerables de un objeto, como un array [clave, valor].
      • const obj = {
            nombre: 'a',
            apellidos: 'b',
            edad: 1,
            estado: true,
        };
        const arr = [1, 2, 3, 4, 5];
        
        console.log(Object.entries(obj));
        /*
           0: (2) ["nombre", "a"]
           1: (2) ["apellidos", "b"]
           2: (2) ["edad", 1]
           3: (2) ["estado", true]
        */
        console.log(Object.entries(arr));
        /*
           0: (2) ["0", 1]
           1: (2) ["1", 2]
           2: (2) ["2", 3]
           3: (2) ["3", 4]
           4: (2) ["4", 5]
        */
  • Object.getOwnPropertyDescriptors()
    • Devuelve todos los descriptores de las propiedades propias de un objeto.
      • const obj = {
            nombre: 'a',
            apellidos: 'b',
            edad: 1,
            estado: true,
        };
        console.log(Object.getOwnPropertyDescriptors(obj));
        /*
        nombre:
           value: "a"
           writable: true
           enumerable: true
           configurable: true
           __proto__: Object
        apellidos:
           value: "b"
           writable: true
           enumerable: true
           configurable: true
           __proto__: Object
        edad:
           value: 1
           writable: true
           enumerable: true
           configurable: true
           __proto__: Object
        estado:
           value: true
           writable: true
           enumerable: true
           configurable: true
           __proto__: Object
        */
  • Async / Await
    • Es una forma de simplificar el uso síncrono de promesas, sin  tener que hacer uso de los bloque then catch.
      • const test1 = async () => {
            return true;
        };
        console.log(test1());
        // Promise {<resolved>: true}
        
        
        // ------------------------------
        const api = () => {
            return new Promise((res, rej) => {
                setTimeout(() => {
                    res('OK');
                }, 2000);
            });
        };
        
        const test = async () => await api();
        
        test().then(data => console.log(data));
        // OK
        
        
        // -------------------------------
        const t = async msn => `Mensaje..... ${msn}`;
        console.log(t('a'));
        // Promise {<resolved>: "Mensaje..... a"}

Continuamos con la versión ES9 (2018).

  • Asynchronous Iteration (for await…of)
    • Nos permite crear un bucle iterando sobre objetos iterables asincrónicos y sincrónicos.
    • // Generador asincrono
      async function* gen() {
          let cont = 0;
          while (cont < 10) {
              yield cont++;
          }
      }
      (async () => {
          for await (let item of gen()) {
              console.log(item);
          }    
      })();
      
      // Iterando promesas 
      const createPromise = () => {
          return new Promise((res, rej) => {
              setTimeout(() => {
                  res(true);
              }, 500);
          })
      }
      async function* gen() {
          debugger;
          let cont = 0;
          while (cont < 10) {
              cont++;
              yield await createPromise();
          }
      }
      (async () => {
          for await (let item of gen()) {
              console.log('Promise', item);
          }    
      })();
      
      // Iterando array de promesas
      
      const createPromise = () => {
          return new Prmise((res, rej) => {
              setTimeout(() => {
                  res(true);
              }, 500);
          });
      };
      
      (async () => {
          const arr = [];
          for (let cont = 0; con < 10; cont++) {
              arr.push(createPromise());
          }
          for await (let item of arr) {
              console.log('Promise', item);
          }
      })();
  • Promise.finally()
    • Este método devuelve una Promise cuando se resuelve allá tenido éxito o no.
    • Con este comportamiento podemos tener código que siempre se ejecuta.
    • const createPromise = () => {
          return new Promise((res, rej) => {
              setTimeout(() => {
                  const v = Math.floor(Math.random() * 100);
                  v <=50 ? res(true) : rej(false);
              }, 500);
          })
      }
      
      createPromise().then(r => console.log('OK', r)).catch(e => console.log('FAIL', e)).finally(() => console.log('ALWAYS'));
      /*
      Promise {<pending>}
      FAIL false
      ALWAYS
      */
      
      createPromise().then(r => console.log('OK', r)).catch(e => console.log('FAIL', e)).finally(() => console.log('ALWAYS'));
      /*
      Promise {<pending>}
      OK true
      ALWAYS
      */
      
      createPromise().then(r => console.log('OK', r)).catch(e => console.log('FAIL', e)).finally(() => console.log('ALWAYS'));
      /*
      Promise {<pending>}
      FAIL false
      ALWAYS
      */

Continuamos con la versión ES10 (2019).

  • Array.flat(depth)
    • Crea un nuevo array con todos los elementos concatenados recursivamente hasta la profundidad especificada.
    • const arr = [1, 2, 3, [4, 5, 6, [7, 8, 9, [10], 11], 12, 13], 14, 15];
      
      console.log(arr.flat());
      // (11) [1, 2, 3, 4, 5, 6, Array(5), 12, 13, 14, 15]
      
      console.log(arr.flat(1));
      // (11) [1, 2, 3, 4, 5, 6, Array(5), 12, 13, 14, 15]
      
      console.log(arr.flat(2));
      // (15) [1, 2, 3, 4, 5, 6, 7, 8, 9, Array(1), 11, 12, 13, 14, 15]
      
      console.log(arr.flat(3));
      // (15) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
      
      console.log(arr.flat(4));
      // (15) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
  • Array.flatMap()
    • Es similar a Array.flat(), salvo que este método primero mapea cada elemento y luego aplana el resultado en un nuevo array (profundidad de 1).
    • const arr = [1, 2, 3, [4, 5, 6, [7, 8, 9, [10], 11], 12, 13], 14, 15];
      
      console.log(arr.flatMap(item => item - 1))
      // (6) [0, 1, 2, NaN, 13, 14]
      
      console.log(arr.flatMap(item => item * 10))
      // (6) [10, 20, 30, NaN, 140, 150]
      
      console.log(arr.flatMap(item => item > 2 ? item * 10 : false))
      // (6) [false, false, 30, false, 140, 150]
      
      console.log(arr.flatMap(item => item > 2 ? item * 10 : null))
      // (6) [null, null, 30, null, 140, 150]
      
      console.log(arr.flatMap(item => item > 2 ? item * 10 : null).filter(item => item))
      // (3) [30, 140, 150]
      
  • Object.fromEntries()
    • Este método nos transforma una lista de pares [clave, valor] en un objeto.
    • const obj = {
          a: 1,
          b: 2,
          c: 3
      };
      
      console.log(Object.fromEntries(Object.entries(obj)));
      // {length: 3, a: 1, b: 2, c: 3}
      
      console.log(Object.fromEntries(new Map([['nombre', 'aaaa'], ['apellidos', 'bbbbb']])));
      // {nombre: "aaaa", apellidos: "bbbbb"}
      
      console.log(Object.fromEntries([['nombre', 'aaaa'], ['apellidos', 'bbbbb']]));
      // {nombre: "aaaa", apellidos: "bbbbb"}
  • String.trimStart(), String.trimEnd()
    • trimStart(): Elimina los espacios en blanco al principio de una cadena de texto.
    • trimEndt(): Elimina los espacios en blanco al final de una cadena de texto.
    • let text = '    Hola mundo!!!      ';
      
      console.log(text.trimEnd())
      // '    Hola mundo!!!'
      
      console.log(text.trimStart())
      // 'Hola mundo!!!    '
      
  • Optional Catch Binding
    • Nos permite usar en un bloque try/catch el bloque catch sin parámetro
    • try {
          throw new Error('Error')
      } catch {
          console.log('Error en el código');
      }
      
      // Error en el código
  • String.matchAll()
    • Nos devuelve un iterador con todos los resultados de ocurrencia en una cadena de texto contra una expresión regular.
    • const expresion = 't';
      const texto = 'Todos los dias tomo una tostada';
      const arr = [...texto.matchAll(expresion)];
      console.log(arr);
      /*
      0: ["t", index: 15, input: "Todos los dias tomo una tostada", groups: undefined]
      1: ["t", index: 24, input: "Todos los dias tomo una tostada", groups: undefined]
      2: ["t", index: 27, input: "Todos los dias tomo una tostada", groups: undefined]
      */
      
      
  • Dynamic import
    • La expresión import carga un módulo y este devuelve una promesa que se resuelve en un objeto de módulo que contiene todas sus exportaciones.
    • En la nueva revisión podemos importar dinámicamente esos módulos.
    • // externo.js
      export const suma = (a, b) => a + b;
      export const resta = (a, b) => a - b;
      
      // index.js
      const { suma, resta } = await import('./externo.js');
  • globalThis
    • Es una nueva forma con la que podemos acceder al this global
    • console.log(window);
      // Window {parent: Window, opener: null, top: Window, length: 2, frames: Window, …}
      
      console.log(globalThis);
      // Window {parent: Window, opener: null, top: Window, length: 2, frames: Window, …}

Bien, con esto terminamos esta guía donde he intentado estructurar todo (o casi todo) el contenido de Javascript ademas de todas las features de cada una de las revisión de ECMAScript.

Tutorial de Javascript y las mejoras desde ES6 (ES2015) hasta ES10 (ES2019) – Parte 5 (Gestión de errores)

Gestión de errores (Error Handling)

La gestión de errores es una técnica que nos permite controlar los errores ocasionados durante la ejecución de nuestro código.

Si queremos realizar esa gestión de errores correctamente debemos conocer los tipos de error que existen.

  • Syntax Errors: Los errores de sintaxis ocurren en tiempo de interpretación.
    • function ()
      Uncaught SyntaxError: Function statements require a function name
    • Cuando ocurre este tipo de error solo el código contenido en el mismo hilo que el error de sintaxis se ve afectado, el resto se ejecuta asumiendo que nada de ellos depende del código que contiene el error.
  • Runtime Errors: Los errores de tiempo de ejecución (excepciones) ocurren durante la ejecución (después de la interpretación).
    • (() => {
        window.saludo();
      })();
      Uncaught TypeError: window.saludo is not a function
    • Las excepciones afectan en el hilo donde ocurren permitiendo que otros hilos continúen la ejecución normal.
  • Logical Errors: Este tipo de errores son muy complicados de localizar, ya que son el resultado de un error en la lógica del código no obteniendo el resultado esperado.

La sentencia try…catch…finally

Como ocurre en otros lenguajes de programación, Javascript también ofrece las sentencias try…catch…finally para la gestión de errores que junto al operador throw permite manejar las excepciones.

Para entender mejor como usar estas sentencias vamos a crear un sencillo ejemplo.

try {
  console.log('Ejecutamos TRY');
} catch (err) {
  console.log('Ejecutamos CATCH:', err);
} finally {
  console.log('Ejecutamos FINALLY');
}

// Ejecutamos TRY
// Ejecutamos FINALLY

try {
  console.log('Ejecutamos TRY');
  throw new Error('Lanzamos un error');
} catch (err) {
  console.log('Ejecutamos CATCH:', err);
} finally {
  console.log('Ejecutamos FINALLY');
}

// Ejecutamos TRY 
// Ejecutamos CATCH: Error: Lanzamos un error 
// Ejecutamos FINALLY

try {
  console.log('Ejecutamos TRY');
  window.saludo();
} catch (err) {
  console.log('Ejecutamos CATCH:', err);
} finally {
  console.log('Ejecutamos FINALLY');
}

// Ejecutamos TRY
// Ejecutamos CATCH: TypeError: window.saludo is not a function
// Ejecutamos FINALLY

Como hemos visto en el código anterior, la sentencia try debe ir seguido de catch finally.

Cuando ocurre una excepción en el bloque try  la excepción se pasa en err y se ejecuta el bloque catch.

El bloque (opcional) finally se ejecuta siempre después de try…catch.

Ahora que conocemos los tipos de errores y como se gestionan, vamos a realizar algunos ejemplos.

Tipos de errores en Javascript

Como vimos al principio de la entrada, los errores podían ser de un grupo u otro (Syntax, Runtime y Logical) dependiendo de varios factores.

Pero cuando necesitamos saber mas sobre el error ocurrido, Javascript ofrece múltiples constructores para tener mas detalle y poder realizar correctamente su gestión.

A continuación un listado de los constructores.

  • EvalError: Error asociado a la ejecución de la función eval().
  • try {
        throw new EvalError('Mensaje de prueba');
    } catch(err) {
        console.log('ERROR', err);
        // ERROR EvalError: Mensaje de prueba
    }
    
  • RangeError: Error asociado a un valor fuera del rango permitido para una variable o un parámetro.
  • try {
        var data = 1234.56;
        data.toFixed(-1)
    } catch(err) {
        console.log('ERROR', err);
        // ERROR RangeError: toFixed() digits argument must be between 0 and 100
    }
    
    try {
        throw new RangeError('Mensaje');
    } catch(err) {
        console.log('ERROR', err);
        // ERROR RangeError: Mensaje
    }
    
    
    const validarEdad = edad => {
       if (edad < 0 || edad >= 110) {
           throw new RangeError('La edad tiene que estar entre 0 y 110 años');
       }
    }
    
    try {
       validarEdad(200);
    } catch (err) {
       console.log('ERROR', err);
       // ERROR RangeError: El rango de edad es desde los 0 años hasta los 110 años
    }
    
    
    
    
  • ReferenceError: Error asociado a la invocación de una función u objeto y no existe en ese ámbito.
  • try {
        sumar();
    } catch(err) {
        console.log('ERROR', err);
        // ERROR ReferenceError: sumar is not defined
    }
    
    try {
        const total = 10 * iva;
    } catch(err) {
        console.log('ERROR', err);
        // ERROR ReferenceError: iva is not defined
    }
    
    try {
        throw new ReferenceError('Mensaje referencia');
    } catch(err) {
        console.log('ERROR', err);
        // ERROR ReferenceError: Mensaje referencia
    }
  • SyntaxError: Error en la sintaxis del código que se intenta ejecutar.
  • try {
        JSON.parse('{prueba de json invalido}')
    } catch(err) {
        console.log('ERROR', err);
        // ERROR SyntaxError: Unexpected token p in JSON at position 1
    }
    
    try {
        throw new SyntaxError('Mensaje syntax error');
    } catch(err) {
        console.log('ERROR', err);
        // ERROR SyntaxError: Mensaje syntax error
    }
    
    
  • TypeError: Error que sucede cuando una variable o parámetro no tienen un tipo válido.
  • try {
        throw new TypeError('Mensaje type');
    } catch(err) {
        console.log('ERROR', err);
        // ERROR TypeError: Mensaje type
    }
    
    const info = {
        nombre: 'text',
        estado: false,
        hello: () => {
            return 'Hello';
        },
    };
    try {
        console.log(info);
        console.log(info.nombre);
        console.log(info.hello());
        console.log(info.bye());
    } catch(err) {
        console.log('ERROR', err);
        /*
           {
               nombre: "text",
               estado: false,
               hello: () => { return 'Hello'; }
           }
           text
           Hello
           ERROR TypeError: info.bye is not a function
        */
    }
    
    
  • URIError: Error que ocurre cuando se pasan parámetros no válidos a las funciones encodeURI() ó decodeURl().
  • try {
        throw new URIError('URI error');
    } catch(err) {
        console.log('ERROR', err);
        // ERROR URIError: URI error
    }
    
    try {
        decodeURI('\\%%');
    } catch(err) {
        console.log('ERROR', err);
       // ERROR URIError: URI malformed
    }

Ahora, conociendo los constructores que nos ofrece Javascript podemos ser muchos mas específicos a la hora de gestionar y mostrar los errores.

Todo lo anterior esta fenomenal, pero…¿si necesito tener mi propio error o errores?.

A continuación te muestro como crear nuestros errores personalizados.

Creando nuestros errores personalizados

Cuando necesitamos lanzar un error personalizado de una forma simple podemos usar throw.

const nombre = '';
try {
   if (nombre.length === 0) {
      throw 'El campo nombre es obligatorio';
   }
} catch(err) {
   console.log(err);
   // El campo nombre es obligatorio
}

try { 
   if (nombre.length === 0) { 
      throw {code: 400, status: 'fail'}; 
   } 
} catch(err) { 
   console.log(err); 
   // {code: 400, status: "fail"}
}

El problema del ejemplo anterior, es que los errores que estamos mostrando no ofrecen ninguna información adicional.

Para tener nuestros errores personalizados y con toda la información necesaria usaremos el constructor Error().

try { 
   if (nombre.length === 0) { 
      throw new Error('El campo nombre es obligatorio'); 
   } 
} catch(err) { 
   console.log('ERROR NAME:', err.name); 
   // ERROR NAME: Error 
   console.log('ERROR MESSAGE:', err.message); 
   // ERROR MESSAGE: El campo nombre es obligatorio 
   console.log('ERROR STACK:', err.stack); 
   // ERROR STACK: Error: El campo nombre es obligatorio at snippet:///Script%20snippet%20%2321:3:7 
}

Como hemos podido observar la diferencia entre una u otra forma es bastante evidente.

Con esta entrada finalizamos lo que podríamos llamar el core de Javascript (fundamentos, operadores, control de flujo, array, etc…).

Para las siguientes entradas comenzaremos a revisar las nuevas características que esta ofreciendo Javascript, y que van desde ES6 (2015) hasta ES10 (2019).