Saltar al contenido
schedule 12 min JavaScript

JavaScript moderno (ES6+)

JavaScript no dejó de evolucionar en 2015. Cada año se añaden funcionalidades que hacen tu código más corto, más seguro y más expresivo. El problema es que muchos tutoriales enseñan JavaScript de hace 10 años. Esta lección es lo contrario: aquí solo verás las herramientas modernas que usarás en tu día a día como desarrollador profesional. Si dominas lo que viene a continuación, tu código se distinguirá del de un principiante a primera vista.

ES Modules: import y export

Cuando tu archivo app.js tiene 2000 líneas, es imposible de mantener. Los módulos te permiten dividir tu código en archivos separados, cada uno con una responsabilidad clara:

utils/math.js
// Exportaciones con nombre (named exports)
export const sumar = (a, b) => a + b;
export const restar = (a, b) => a - b;
export const multiplicar = (a, b) => a * b;

// También puedes exportar al final del archivo
const dividir = (a, b) => {
    if (b === 0) throw new Error('No se puede dividir entre 0');
    return a / b;
};

const PI = 3.14159265359;

export { dividir, PI };
utils/format.js
// Exportación por defecto (solo una por archivo)
// Úsala cuando el archivo tiene UNA cosa principal
const formatearMoneda = (cantidad, moneda = 'EUR') => {
    return new Intl.NumberFormat('es-ES', {
        style: 'currency',
        currency: moneda,
    }).format(cantidad);
};

export default formatearMoneda;

// También puedes mezclar default y named exports
export const formatearFecha = (fecha) => {
    return new Intl.DateTimeFormat('es-ES', {
        day: 'numeric',
        month: 'long',
        year: 'numeric',
    }).format(new Date(fecha));
};
app.js
// Importar named exports: con llaves {}
import { sumar, multiplicar, PI } from './utils/math.js';

console.log(sumar(5, 3));       // 8
console.log(multiplicar(4, PI)); // 12.566...

// Importar default export: sin llaves, el nombre lo eliges tú
import formatearMoneda from './utils/format.js';

console.log(formatearMoneda(1299.99)); // "1.299,99 €"

// Importar default y named juntos
import formatearMoneda, { formatearFecha } from './utils/format.js';

console.log(formatearFecha('2026-03-15')); // "15 de marzo de 2026"

// Importar todo como un objeto
import * as MathUtils from './utils/math.js';

console.log(MathUtils.sumar(10, 5));  // 15
console.log(MathUtils.PI);            // 3.14159...

Para usar módulos en el navegador, tu script necesita type="module":

index.html
<!-- ❌ Script normal — no soporta import/export -->
<script src="app.js"></script>

<!-- ✅ Módulo ES — soporta import/export -->
<script type="module" src="app.js"></script>

<!-- Los módulos tienen defer automático (no bloquean el HTML)
     y strict mode activado por defecto -->

En producción, herramientas como Vite (que usa Laravel) empaquetan todos tus módulos en archivos optimizados. No te preocupes por tener muchos archivos pequeños: Vite los combina automáticamente para que la carga sea rápida.

Template literals avanzados: tagged templates

Ya conoces los template literals con backticks para interpolar variables. Pero hay una funcionalidad avanzada: los tagged templates. Son funciones que procesan el template antes de generar el string:

app.js
// Un tagged template recibe las partes del string y los valores interpolados
const sanitizar = (strings, ...valores) => {
    return strings.reduce((resultado, str, i) => {
        let valor = valores[i] ?? '';

        // Escapar caracteres peligrosos para prevenir XSS
        if (typeof valor === 'string') {
            valor = valor
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;');
        }

        return resultado + str + valor;
    }, '');
};

// Uso: el tag "sanitizar" limpia los valores antes de insertarlos
const nombreUsuario = '<script>alert("hackeado")</script>';

// ❌ Sin sanitizar — inyección de código
const htmlPeligroso = `<h1>Hola, ${nombreUsuario}</h1>`;

// ✅ Con tagged template — seguro
const htmlSeguro = sanitizar`<h1>Hola, ${nombreUsuario}</h1>`;
// "<h1>Hola, &lt;script&gt;alert(&quot;hackeado&quot;)&lt;/script&gt;</h1>"

Optional chaining: acceso seguro a propiedades

El error TypeError: Cannot read properties of undefined es probablemente el error más común de JavaScript. Optional chaining (?.) lo previene:

app.js
const usuario = {
    nombre: 'Carlos',
    direccion: {
        calle: 'Gran Vía 42',
        ciudad: 'Madrid',
    },
    // No tiene "empresa" definida
};

// ❌ Sin optional chaining — explota si algo es undefined
// console.log(usuario.empresa.nombre); // TypeError!

// ✅ Con optional chaining — devuelve undefined en vez de explotar
console.log(usuario.empresa?.nombre);          // undefined (no error)
console.log(usuario.direccion?.calle);         // "Gran Vía 42"
console.log(usuario.direccion?.codigoPostal);  // undefined

// Funciona con métodos también
const resultado = usuario.calcularDescuento?.(); // undefined si el método no existe

// Y con arrays
const usuarios = [{ nombre: 'Ana' }, { nombre: 'Luis' }];
console.log(usuarios[5]?.nombre); // undefined (el índice 5 no existe)

// Encadenar múltiples niveles
const config = {};
const puerto = config?.servidor?.red?.puerto; // undefined, sin errores

Nullish coalescing: ?? vs ||

El operador ?? proporciona un valor por defecto cuando algo es null o undefined. Parece igual que ||, pero la diferencia es crucial:

app.js
// || devuelve el segundo valor si el primero es "falsy"
// Valores falsy: false, 0, "", null, undefined, NaN

const precio = 0;
const cantidad = '';

console.log(precio || 10);    // 10 — ¡INCORRECTO! El precio sí es 0, es un valor válido
console.log(cantidad || 'N/A'); // "N/A" — ¡INCORRECTO! La cantidad vacía puede ser intencional

// ?? solo devuelve el segundo valor si el primero es null o undefined
console.log(precio ?? 10);    // 0 — ✅ Correcto, 0 es un valor válido
console.log(cantidad ?? 'N/A'); // "" — ✅ Correcto, string vacío es un valor válido
console.log(null ?? 'por defecto');      // "por defecto"
console.log(undefined ?? 'por defecto'); // "por defecto"

// Caso real: configuración de una app
const configuracion = {
    itemsPorPagina: 0,  // El usuario quiere 0 (mostrar todos)
    tema: '',           // El usuario borró el tema (usar el del sistema)
    idioma: null,       // No se ha configurado
};

// Con || obtienes valores incorrectos:
const items = configuracion.itemsPorPagina || 20; // 20 — mal, el usuario quería 0

// Con ?? obtienes valores correctos:
const itemsCorrecto = configuracion.itemsPorPagina ?? 20; // 0 — correcto

// Combinado con optional chaining es muy potente:
const idiomaUsuario = configuracion?.idioma ?? 'es'; // "es"

Regla simple: usa ?? cuando quieras un valor por defecto solo si algo es null o undefined. Usa || cuando cualquier valor falsy deba activar el valor por defecto.

for...of: iterar de forma limpia

El bucle for...of itera sobre los valores de cualquier iterable (arrays, strings, Maps, Sets). Es más legible que for clásico y no tiene los problemas de for...in:

app.js
const lenguajes = ['JavaScript', 'Python', 'Rust', 'Go'];

// ❌ for...in itera sobre los ÍNDICES (y también propiedades heredadas)
for (const i in lenguajes) {
    console.log(i); // "0", "1", "2", "3" — son strings, no números
}

// ✅ for...of itera sobre los VALORES
for (const lenguaje of lenguajes) {
    console.log(lenguaje); // "JavaScript", "Python", "Rust", "Go"
}

// Con strings
for (const letra of 'Hola') {
    console.log(letra); // "H", "o", "l", "a"
}

// Si necesitas el índice también, usa entries()
for (const [indice, lenguaje] of lenguajes.entries()) {
    console.log(`${indice + 1}. ${lenguaje}`);
}
// "1. JavaScript"
// "2. Python"
// "3. Rust"
// "4. Go"

// for...in es para objetos (iterar propiedades)
const persona = { nombre: 'Elena', edad: 30, ciudad: 'Sevilla' };

for (const clave in persona) {
    console.log(`${clave}: ${persona[clave]}`);
}
// "nombre: Elena"
// "edad: 30"
// "ciudad: Sevilla"

Map y Set: más allá de objetos y arrays

Map es como un objeto, pero mejor para datos dinámicos. Set es un array que solo permite valores únicos:

app.js
// === MAP ===
// Un Map puede usar cualquier cosa como clave (no solo strings)
const carrito = new Map();

const producto1 = { id: 1, nombre: 'Camiseta' };
const producto2 = { id: 2, nombre: 'Pantalón' };

carrito.set(producto1, 3);  // 3 unidades de camiseta
carrito.set(producto2, 1);  // 1 unidad de pantalón

console.log(carrito.get(producto1)); // 3
console.log(carrito.size);           // 2
console.log(carrito.has(producto1));  // true

// Iterar un Map
for (const [producto, cantidad] of carrito) {
    console.log(`${producto.nombre}: ${cantidad} uds.`);
}

// ¿Cuándo Map en vez de objeto?
// - Cuando las claves no son strings (objetos, números...)
// - Cuando necesitas saber el tamaño (.size)
// - Cuando añades/eliminas entradas frecuentemente
// - Cuando el orden de inserción importa (Map lo garantiza)

// === SET ===
// Un Set es un array donde cada valor es único
const etiquetas = new Set();

etiquetas.add('javascript');
etiquetas.add('frontend');
etiquetas.add('javascript'); // Ignorado — ya existe

console.log(etiquetas.size); // 2 (no 3)
console.log(etiquetas.has('javascript')); // true

// Caso práctico: eliminar duplicados de un array
const numerosConRepetidos = [1, 3, 5, 3, 7, 1, 9, 5];
const unicos = [...new Set(numerosConRepetidos)];
console.log(unicos); // [1, 3, 5, 7, 9]

// Contar visitantes únicos
const visitasHoy = new Set();

const registrarVisita = (userId) => {
    visitasHoy.add(userId);
    console.log(`Visitantes únicos hoy: ${visitasHoy.size}`);
};

registrarVisita('user_42');  // 1
registrarVisita('user_87');  // 2
registrarVisita('user_42');  // 2 — no se incrementa, ya estaba

Operadores de asignación lógica: ??=, ||=, &&=

Son atajos que combinan asignación con lógica. Ahorran código y son muy expresivos:

app.js
// ??= asigna solo si el valor actual es null o undefined
let configuracion = {};

configuracion.tema ??= 'oscuro';     // Era undefined → ahora es "oscuro"
configuracion.tema ??= 'claro';      // Ya tiene valor → no cambia
console.log(configuracion.tema);      // "oscuro"

// Es equivalente a:
// configuracion.tema = configuracion.tema ?? 'oscuro';

// ||= asigna si el valor actual es falsy (0, "", false, null, undefined)
let intentos = 0;
intentos ||= 3; // 0 es falsy → ahora es 3

// &&= asigna solo si el valor actual es truthy
let usuario = { nombre: 'Ana', activo: true };
usuario.activo &&= verificarSesion(); // Solo llama a verificarSesion() si activo es true

// Caso práctico: inicializar un cache
const cache = {};

const obtenerDatos = (clave) => {
    // Si no está en cache, calcularlo y guardarlo
    cache[clave] ??= calcularDatosCostosos(clave);
    return cache[clave];
};

at() para arrays y strings: índices negativos

El método at() te permite acceder a elementos con índices negativos, contando desde el final:

app.js
const colores = ['rojo', 'verde', 'azul', 'amarillo', 'morado'];

// Acceder al último elemento
// ❌ La forma clásica (fea)
console.log(colores[colores.length - 1]); // "morado"

// ✅ Con at() (limpio)
console.log(colores.at(-1));  // "morado"
console.log(colores.at(-2));  // "amarillo"
console.log(colores.at(0));   // "rojo" — también funciona con positivos

// Funciona con strings
const mensaje = '¡Hola mundo!';
console.log(mensaje.at(-1));  // "!"
console.log(mensaje.at(0));   // "¡"

// Caso práctico: obtener la extensión de un archivo
const archivo = 'proyecto.backup.2026.zip';
const extension = archivo.split('.').at(-1);
console.log(extension); // "zip"

Object.hasOwn(): la forma moderna de comprobar propiedades

app.js
const configuracion = { tema: 'oscuro', idioma: 'es' };

// ❌ La forma antigua — puede fallar con objetos creados con Object.create(null)
console.log(configuracion.hasOwnProperty('tema')); // true

// ✅ La forma moderna — funciona siempre
console.log(Object.hasOwn(configuracion, 'tema'));   // true
console.log(Object.hasOwn(configuracion, 'color'));  // false

// ¿Por qué importa?
const objSinPrototipo = Object.create(null);
objSinPrototipo.clave = 'valor';

// objSinPrototipo.hasOwnProperty('clave'); // TypeError — no tiene el método
Object.hasOwn(objSinPrototipo, 'clave');    // true — siempre funciona

Patrón: early returns para código más limpio

No es una funcionalidad nueva del lenguaje, pero es un patrón que distingue al código profesional del código de principiante. En vez de anidar if dentro de if, valida las condiciones de error al principio y sal pronto:

app.js
// ❌ Sin early returns — "pirámide de la perdición"
const procesarPedido = (pedido) => {
    if (pedido) {
        if (pedido.productos.length > 0) {
            if (pedido.usuario) {
                if (pedido.usuario.verificado) {
                    // ...finalmente el código real, con 4 niveles de indentación
                    const total = pedido.productos.reduce((sum, p) => sum + p.precio, 0);
                    return { exito: true, total };
                } else {
                    return { exito: false, error: 'Usuario no verificado' };
                }
            } else {
                return { exito: false, error: 'Sin usuario' };
            }
        } else {
            return { exito: false, error: 'Sin productos' };
        }
    } else {
        return { exito: false, error: 'Pedido inválido' };
    }
};

// ✅ Con early returns — plano, legible, profesional
const procesarPedidoMejor = (pedido) => {
    if (!pedido) {
        return { exito: false, error: 'Pedido inválido' };
    }

    if (pedido.productos.length === 0) {
        return { exito: false, error: 'Sin productos' };
    }

    if (!pedido.usuario) {
        return { exito: false, error: 'Sin usuario' };
    }

    if (!pedido.usuario.verificado) {
        return { exito: false, error: 'Usuario no verificado' };
    }

    // Si llegamos aquí, todo está bien
    const total = pedido.productos.reduce((sum, p) => sum + p.precio, 0);
    return { exito: true, total };
};

Ejemplo completo: sistema de módulos utilitarios

Vamos a construir un mini sistema de utilidades organizado con módulos, usando todo lo que hemos aprendido:

utils/validators.js
// Funciones de validación reutilizables
export const esEmailValido = (email) => {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email ?? '');
};

export const esPasswordSeguro = (password) => {
    if (!password) return false;
    return password.length >= 8
        && /[A-Z]/.test(password)
        && /[0-9]/.test(password);
};

export const esNombreValido = (nombre) => {
    return (nombre?.trim().length ?? 0) >= 2;
};

// Validar un formulario completo
export const validarFormulario = (datos) => {
    const errores = new Map();

    if (!esNombreValido(datos.nombre)) {
        errores.set('nombre', 'El nombre debe tener al menos 2 caracteres');
    }

    if (!esEmailValido(datos.email)) {
        errores.set('email', 'Introduce un email válido');
    }

    if (!esPasswordSeguro(datos.password)) {
        errores.set('password', 'Mínimo 8 caracteres, 1 mayúscula y 1 número');
    }

    return {
        valido: errores.size === 0,
        errores,
    };
};
utils/format.js
// Funciones de formato
export const formatearMoneda = (cantidad, moneda = 'EUR') => {
    return new Intl.NumberFormat('es-ES', {
        style: 'currency',
        currency: moneda,
    }).format(cantidad ?? 0);
};

export const formatearFecha = (fecha) => {
    return new Intl.DateTimeFormat('es-ES', {
        day: 'numeric',
        month: 'long',
        year: 'numeric',
    }).format(new Date(fecha));
};

export const truncarTexto = (texto, maxLength = 100) => {
    if (!texto || texto.length <= maxLength) return texto ?? '';
    return texto.slice(0, maxLength).trimEnd() + '...';
};

// Default export: un formateador completo
export default {
    moneda: formatearMoneda,
    fecha: formatearFecha,
    texto: truncarTexto,
};
utils/collections.js
// Utilidades para trabajar con colecciones usando Map y Set

export const agruparPor = (array, clave) => {
    const grupos = new Map();

    for (const item of array) {
        const valor = item[clave];
        const grupo = grupos.get(valor) ?? [];
        grupo.push(item);
        grupos.set(valor, grupo);
    }

    return grupos;
};

export const unicos = (array, clave) => {
    if (!clave) return [...new Set(array)];

    const vistos = new Set();
    return array.filter((item) => {
        const valor = item[clave];
        if (vistos.has(valor)) return false;
        vistos.add(valor);
        return true;
    });
};

export const contarOcurrencias = (array) => {
    const conteo = new Map();

    for (const item of array) {
        conteo.set(item, (conteo.get(item) ?? 0) + 1);
    }

    return conteo;
};
app.js
// Importar desde nuestros módulos
import { validarFormulario, esEmailValido } from './utils/validators.js';
import formato, { formatearMoneda } from './utils/format.js';
import { agruparPor, unicos, contarOcurrencias } from './utils/collections.js';

// Usar las utilidades de validación
const datosRegistro = {
    nombre: 'Laura',
    email: '[email protected]',
    password: 'MiClave123',
};

const resultado = validarFormulario(datosRegistro);
console.log(resultado.valido); // true

// Usar las utilidades de formato
console.log(formatearMoneda(49.99));            // "49,99 €"
console.log(formato.fecha('2026-03-15'));       // "15 de marzo de 2026"
console.log(formato.texto('Un texto muy largo que necesita ser cortado', 20));
// "Un texto muy largo..."

// Usar las utilidades de colecciones
const pedidos = [
    { producto: 'Camiseta', talla: 'M', precio: 25 },
    { producto: 'Pantalón', talla: 'L', precio: 45 },
    { producto: 'Camiseta', talla: 'S', precio: 25 },
    { producto: 'Gorra', talla: 'M', precio: 15 },
    { producto: 'Pantalón', talla: 'M', precio: 45 },
];

// Agrupar pedidos por producto
const porProducto = agruparPor(pedidos, 'producto');
console.log(porProducto.get('Camiseta')); // [{ producto: 'Camiseta', ... }, ...]

// Productos únicos
const productosUnicos = unicos(pedidos, 'producto');
console.log(productosUnicos.length); // 3

// Contar tallas
const tallas = pedidos.map((p) => p.talla);
const conteoTallas = contarOcurrencias(tallas);
console.log(conteoTallas.get('M')); // 3

Resumen de cuándo usar cada herramienta

  • ??: valor por defecto solo para null/undefined (no para 0, "", false).
  • ?.: acceder a propiedades sin miedo a que sean undefined.
  • ??=: inicializar una variable solo si no tiene valor.
  • Map: cuando las claves no son strings, necesitas .size, o el orden importa.
  • Set: cuando necesitas valores únicos o comprobar existencia rápida.
  • for...of: iterar valores de arrays, strings, Maps, Sets.
  • at(): acceder a elementos desde el final con índices negativos.
  • import/export: siempre. Divide tu código en módulos.
code

Crea un sistema de utilidades con módulos

Medio schedule 20 min

Organiza un mini proyecto con ES modules. Crea estos archivos:

  • utils/strings.js: exporta funciones capitalizar (primera letra mayúscula), slugificar (convierte "Hola Mundo" en "hola-mundo") y contarPalabras
  • utils/arrays.js: exporta funciones barajar (mezclar un array aleatoriamente), chunk (dividir array en grupos de N elementos) y ultimo (usando at(-1))
  • app.js: importa todo y demuestra su uso con datos reales (nombres de productos, lista de colores, etc.)
  • Usa ?? y ?. donde tenga sentido para manejar valores nulos
  • Usa Map o Set en al menos una función
  • Aplica el patrón de early returns en tus funciones
  • Crea un index.html con <script type="module" src="app.js"> para probarlo en el navegador
lightbulb Pistas

Para slugificar, usa .toLowerCase().trim().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''). Para barajar, busca el algoritmo Fisher-Yates. Para chunk, un bucle for que avance de N en N con slice(). Para probar en el navegador, necesitas un servidor local (por ejemplo npx serve) porque los módulos no funcionan con file://.

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.