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