Saltar a contenido

WA1.1 JavaScript moderno: datos, funciones y módulos

Cómo se han comprobado los ejemplos

Cada ejemplo con salida se ha ejecutado de verdad con Node al construir esta web: lo que ves es lo que imprime. Los navegadores y Node comparten el lenguaje, pero no todo el entorno (por ejemplo, document y window solo existen en el navegador), y esta página lo señala cuando importa.

Desestructurar: sacar piezas de un objeto o una lista

Desestructurar es declarar varias variables de golpe a partir de un objeto o un array. Ahorra mucho código repetitivo (const nombre = usuario.nombre; const ciudad = ...).

const usuario = { nombre: "Ana", dir: { ciudad: "Lugo" }, tags: ["web", "js", "css"] };

const { nombre, dir: { ciudad }, edad = 18 } = usuario;   // edad no existe: toma el valor por defecto
console.log(nombre, ciudad, edad);

const [primero, ...resto] = usuario.tags;                 // «resto» recoge lo que sobra
console.log(primero, resto);

function presentar({ nombre, edad = 0 }) {                // también en los parámetros
  return `${nombre} (${edad})`;
}
console.log(presentar(usuario), presentar({ nombre: "Luis", edad: 30 }));

let a = 1, b = 2;
[a, b] = [b, a];                                          // intercambiar sin variable auxiliar
console.log(a, b);

Salida (ejecutado con Node):

Ana Lugo 18
web [ 'js', 'css' ]
Ana (0) Luis (30)
2 1
Forma Qué hace
const { x, y } = obj Variables x e y con las propiedades del mismo nombre
const { x: nuevo } = obj Renombrar: la propiedad x va a la variable nuevo
const { x = 5 } = obj Valor por defecto si x es undefined
const [a, b] = lista Por posición
const [a, ...resto] = lista resto es un array con lo demás

Spread y rest: los tres puntos

Los tres puntos ... hacen dos cosas según dónde estén: esparcir (spread) un contenido, o recoger (rest) lo que sobra.

const base = { color: "rojo", talla: "M" };
const pedido = { ...base, talla: "L", unidades: 2 };      // copia base y sobrescribe talla
console.log(pedido);

const a = [1, 2, 3], b = [4, 5];
console.log([...a, ...b, 6]);                              // unir arrays
console.log(Math.max(...a));                               // pasar una lista como argumentos

function sumar(...numeros) {                               // rest: recoge todos los argumentos
  return numeros.reduce((t, n) => t + n, 0);
}
console.log(sumar(1, 2, 3, 4));

// OJO: la copia es SUPERFICIAL
const original = { nombre: "Ana", dir: { ciudad: "Lugo" } };
const copia = { ...original };
copia.dir.ciudad = "Vigo";
console.log(original.dir.ciudad, copia.dir === original.dir);

// Para una copia profunda
const profunda = structuredClone(original);
profunda.dir.ciudad = "León";
console.log(original.dir.ciudad, profunda.dir.ciudad);

Salida (ejecutado con Node):

{ color: 'rojo', talla: 'L', unidades: 2 }
[ 1, 2, 3, 4, 5, 6 ]
3
10
Vigo true
Vigo León

La copia con ... solo duplica el primer nivel: los objetos anidados se comparten, y cambiar uno cambia el otro. Para copiar de verdad todo el árbol, structuredClone.

Encadenamiento opcional y fusión de nulos

Leer una propiedad de algo que puede no existir lanza un error. ?. corta la cadena en cuanto encuentra null o undefined y devuelve undefined; ?? da un valor por defecto solo si lo de la izquierda es null o undefined.

const usuario = { nombre: "Ana", contacto: null, config: { volumen: 0, tema: "" } };

console.log(usuario.contacto?.email);                      // undefined, sin error
console.log(usuario.amigos?.[0]);                          // también con índices
console.log(usuario.saludar?.());                          // y con métodos que pueden no existir

// «||» trata como «falso» el 0, la cadena vacía y false. «??» solo null y undefined.
console.log(usuario.config.volumen || 50);                 // 50: ¡pisa el 0 válido!
console.log(usuario.config.volumen ?? 50);                 // 0: respeta el 0
console.log(usuario.config.tema || "claro", "|", usuario.config.tema ?? "claro", "|");

let opciones = { ruido: null };
opciones.ruido ??= 10;                                     // asigna solo si es null o undefined
opciones.paso ||= 1;                                       // asigna si es «falso»
console.log(opciones);

Salida (ejecutado con Node):

undefined
undefined
undefined
50
0
claro |  |
{ ruido: 10, paso: 1 }

La diferencia entre || y ?? es una fuente clásica de fallos: con ||, un volumen de 0 o un texto vacío se consideran «no hay valor» y se sustituyen. Cuando 0, "" o false son valores válidos, usa ??.

Cierres (closures)

Una función recuerda las variables del lugar donde se creó, aunque ese lugar ya haya terminado. Es lo que permite tener estado privado:

function crearContador(paso = 1) {
  let cuenta = 0;                         // privada: nadie de fuera puede tocarla
  return {
    subir() { cuenta += paso; return cuenta; },
    ver() { return cuenta; }
  };
}

const a = crearContador();
const b = crearContador(10);
a.subir(); a.subir();
b.subir();
console.log(a.ver(), b.ver());            // cada contador tiene SU cuenta
console.log(a.cuenta);                    // undefined: la variable no es accesible

// El clásico de los bucles
const con_var = [];
for (var i = 0; i < 3; i++) con_var.push(() => i);
const con_let = [];
for (let j = 0; j < 3; j++) con_let.push(() => j);
console.log(con_var.map(f => f()), con_let.map(f => f()));

Salida (ejecutado con Node):

2 10
undefined
[ 3, 3, 3 ] [ 0, 1, 2 ]

El último ejemplo es el error más famoso de JavaScript: con var, las tres funciones comparten la misma i (que al terminar el bucle vale 3). Con let, cada vuelta tiene su propia j. Por eso hoy se usa let y const, y casi nunca var.

this: quién llama

this dentro de una función depende de cómo se llama, no de dónde se escribió. Se pierde con facilidad al pasar un método como si fuera una función suelta. Las funciones flecha no tienen this propio: usan el del sitio donde se escribieron.

"use strict";
const contador = {
  n: 0,
  subir() { this.n++; return this.n; },
  subirFlecha: () => typeof this,        // una flecha NO tiene this propio
  programar() {
    return [1, 2].map(() => this.n);     // la flecha de dentro usa el this de «programar»: el objeto
  }
};

console.log(contador.subir());           // this = contador
const suelta = contador.subir;
try { suelta(); } catch (e) { console.log("método suelto ->", e.name); }

const atada = contador.subir.bind(contador);       // fija this para siempre
console.log(atada());
console.log(contador.subir.call({ n: 100 }));      // this = el objeto que le pasas

console.log(contador.programar());

Salida (ejecutado con Node):

1
método suelto -> TypeError
2
101
[ 2, 2 ]
Cómo se llama Valor de this
objeto.metodo() objeto
funcion() suelta (modo estricto) undefined (sin modo estricto, el objeto global)
f.call(x) / f.apply(x) / f.bind(x) x
Una función flecha El this de donde se escribió
new Clase() El objeto nuevo

Regla práctica

Para métodos de un objeto o clase, la forma normal metodo() { ... }. Para callbacks dentro de un método (map, setTimeout, un evento), una flecha, que conserva el this del método.

Colecciones y utilidades modernas

const palabras = ["pera", "manzana", "pera", "kiwi", "manzana", "pera"];

const unicas = new Set(palabras);                          // sin repetidos
console.log(unicas.size, [...unicas]);

const cuenta = new Map();                                  // claves de CUALQUIER tipo, con orden
for (const p of palabras) cuenta.set(p, (cuenta.get(p) ?? 0) + 1);
console.log(cuenta, Object.fromEntries(cuenta));

console.log(palabras.at(-1), palabras.includes("kiwi"));   // at(-1): el último
const ordenadas = palabras.toSorted();                     // NO modifica el original (ES2023)
console.log(ordenadas.slice(0, 3), palabras[0]);

const personas = [{ n: "Ana", edad: 17 }, { n: "Luis", edad: 25 }, { n: "Marta", edad: 17 }];
console.log(Object.groupBy(personas, p => p.edad));        // agrupar por clave

Salida (ejecutado con Node):

3 [ 'pera', 'manzana', 'kiwi' ]
Map(3) { 'pera' => 3, 'manzana' => 2, 'kiwi' => 1 } { pera: 3, manzana: 2, kiwi: 1 }
pera true
[ 'kiwi', 'manzana', 'manzana' ] pera
[Object: null prototype] {
  '17': [ { n: 'Ana', edad: 17 }, { n: 'Marta', edad: 17 } ],
  '25': [ { n: 'Luis', edad: 25 } ]
}

Funciones nuevas: comprueba la compatibilidad

toSorted, Object.groupBy y similares son recientes. Funcionan en los navegadores actuales, pero conviene comprobarlo en Can I use si tu proyecto debe funcionar en navegadores antiguos.

Módulos: repartir el código en archivos

Un proyecto real no cabe en un solo script.js. Los módulos ES permiten que cada archivo exporte lo que quiere compartir y importe lo que necesita. Lo que no se exporta queda privado al archivo.

// matematicas.mjs
const IVA = 0.21;                         // privado: no se exporta

export function conIva(precio) {
  return Math.round(precio * (1 + IVA) * 100) / 100;
}

export const REDONDEO = 2;

export default function descuento(precio, porcentaje) {   // exportación por defecto: una por archivo
  return precio * (1 - porcentaje / 100);
}
// main.mjs
import descuento, { conIva, REDONDEO } from "./matematicas.mjs";
import * as mates from "./matematicas.mjs";           // todo junto bajo un nombre

console.log(conIva(100), REDONDEO);
console.log(descuento(50, 10));
console.log(Object.keys(mates));                       // IVA no aparece: es privado

// Importación DINÁMICA: se carga cuando hace falta, y devuelve una promesa
const { conIva: iva } = await import("./matematicas.mjs");
console.log(iva(10));

Salida de node main.mjs:

121 2
45
[ 'REDONDEO', 'conIva', 'default' ]
12.1

En una página HTML, un módulo se enlaza con <script type="module" src="main.js"></script>. Tres diferencias con un script normal:

  • Se carga con defer automático: se ejecuta cuando el HTML ya está leído, así que puede ir en el <head>.
  • Tiene ámbito propio: sus variables no se filtran al resto (ni a window).
  • Va en modo estricto siempre.
  • Hay que servir la página desde un servidor (no abriendo el archivo con doble clic): los módulos no se cargan desde file://.

Ese servidor puede ser el más sencillo

Basta con npx serve o la extensión Live Server de VS Code. No hace falta nada más para probar módulos en local.

Errores frecuentes

Error Cómo evitarlo
Usar || para valores por defecto cuando 0 o "" son válidos ??
Creer que { ...obj } copia todo Es superficial: structuredClone para copiar en profundidad
Perder el this al pasar objeto.metodo como callback bind, una flecha, o llamarlo dentro de una flecha
var en bucles con funciones dentro let / const
import en un .js normal sin type="module" <script type="module"> y servir por HTTP
Una exportación por defecto cuyo nombre cambia en cada importación Preferir exportaciones con nombre: el editor las autocompleta y no se confunden

Para practicar

Los ejercicios WA1.1 a WA1.3 de WA1 · Ejercicios practican desestructurar, ?./?? y cierres.