learning-front

Nivel 3 · JavaScript moderno y asíncrono

Acceso seguro: ?. y ??

Leer datos que pueden no estar sin reventar (optional chaining ?.) y dar valores por defecto que distinguen "ausente" de "cero o vacío" (nullish coalescing ??).

Los datos reales vienen sucios. Un héroe recién creado no tiene stats; a otro le falta el rango; un novato tiene 0 partidas, que es un dato real, no un hueco. Acceder a esos datos a lo bruto —heroe.stats.rango— revienta en cuanto algo falta, y poner valores por defecto con || te estropea los ceros. JavaScript moderno tiene dos operadores para esto, y los usarás en cada línea de React: ?. para leer sin romper y ?? para dar defaults sin pisar valores legítimos.

Seguimos con el Overwatch Team Builder, pero esta vez con un roster a propósito incompleto: héroes sin stats, sin rango, y un novato con 0 partidas. El terreno perfecto para ?. y ??.

El problema: leer algo que puede no estar#

Si stats puede faltar, esto es una bomba:

javascript
// este héroe no trae stats
const heroe = { nombre: "Mercy" };

// stats es undefined → acceder a .rango sobre undefined LANZA un error y corta el programa.
// TypeError: Cannot read properties of undefined
console.log(heroe.stats.rango);

Durante años, la defensa fue encadenar comprobaciones con &&: si lo de la izquierda existe, sigue.

javascript
// "Si stats existe, dame stats.rango; si no, no sigas (da undefined)."
// && devuelve el primer operando que sea falsy — aquí undefined — no false.
const rango = heroe.stats && heroe.stats.rango;
// undefined (no revienta, pero el código se llena de stats &&)
console.log(rango);

Funciona, pero es ruidoso, sobre todo si bajas varios niveles. La sintaxis moderna lo resuelve en un operador.

Optional chaining: ?.#

El optional chaining ?. lee una propiedad solo si lo de la izquierda no es null ni undefined. Si lo es, corta la cadena y devuelve undefined, sin lanzar error.

javascript
// sin stats
const heroe = { nombre: "Mercy" };

// Demuestra el caso crítico: stats no existe; ?. corta la cadena sin lanzar TypeError.
// Si stats es null/undefined, la cadena corta aquí y devuelve undefined.
// undefined (no revienta)
console.log(heroe?.stats?.rango);

// Demuestra que con datos completos ?. no interfiere: funciona igual que el acceso normal.
const tracer = { nombre: "Tracer", stats: { rango: "Diamante" } };
// Diamante
console.log(tracer?.stats?.rango);

El ?. también protege llamadas y accesos por índice:

javascript
const equipo = { capitan: null };

// Demuestra la llamada opcional: no lanza "toUpperCase is not a function" cuando capitan es null.
// Llamada opcional: solo invoca toUpperCase() si capitan no es null/undefined.
// undefined (no "no es una función")
console.log(equipo.capitan?.toUpperCase());

const lista = null;
// Demuestra el acceso por índice opcional: no lanza error aunque lista sea null.
// Índice opcional: solo accede a [0] si lista existe.
// undefined (no revienta)
console.log(lista?.[0]);

Lectura mental: ?. es “accede si hay algo; si no, devuelve undefined y para”.

Nullish coalescing: ??#

?. evita el error, pero te deja un undefined. Para poner un valor por defecto está el nullish coalescing ??: devuelve lo de la derecha solo si lo de la izquierda es null o undefined.

javascript
// sin stats
const heroe = { nombre: "Mercy" };

// Si heroe.stats.rango no existe, usa "Sin clasificar".
const rango = heroe?.stats?.rango ?? "Sin clasificar";
// Sin clasificar
console.log(rango);

// Si SÍ existe, gana el valor real; el defecto se ignora.
const tracer = { stats: { rango: "Diamante" } };
// Diamante
console.log(tracer?.stats?.rango ?? "Sin clasificar");

?. y ?? son pareja: uno accede sin romper, el otro rellena el hueco. heroe?.stats?.rango ?? "Sin clasificar" es el patrón que repetirás mil veces.

?? frente a ||: cuidado con el cero#

Quizá pienses: “esto ya lo hacía con ||”. Casi. La diferencia es crucial y es un bug clásico. || usa el valor por defecto ante cualquier valor falsy: 0, "", false, además de null y undefined. ?? solo lo usa ante null/undefined. Mira qué pasa con un novato que tiene 0 partidas de verdad:

javascript
// 0 es un dato real, no un hueco
const novato = { partidas: 0 };

// || trata el 0 como "vacío" y lo pisa: PIERDES el dato.
// "sin datos"  (mal)
console.log(novato.partidas || "sin datos");

// ?? respeta el 0, porque no es null ni undefined.
// 0  (correcto)
console.log(novato.partidas ?? "sin datos");

La regla: para valores por defecto sobre datos donde 0, "" o false son legítimos, usa ??. Reserva || para cuando de verdad quieras tratar cualquier valor falsy como “vacío”.

Asignación lógica: ??=#

Una vuelta de tuerca cómoda: ??= asigna un valor por defecto solo si la variable está null/undefined. Es el atajo de if (x == null) x = ....

javascript
// falta el tema
const config = { tema: null };

// como tema es null, le pone "oscuro"
config.tema ??= "oscuro";
// oscuro
console.log(config.tema);

// ahora tema ya vale "oscuro": NO se toca
config.tema ??= "claro";
// oscuro
console.log(config.tema);

Tiene dos hermanos con la misma idea: ||= asigna si la variable es falsy (con la misma trampa del 0 que ||), y &&= asigna si es truthy. En la práctica, ??= es el más útil y seguro de los tres.

Pruébalo tú#

Edita el código de abajo y pulsa «Ejecutar»: el editor corre ?., ?? y ??= de forma nativa. Juega con él —añade un héroe sin nombre y muestra 'Anónimo' con ?? (no con ||, para que un nombre vacío '' no se confunda con ausente)— y mira la consola.

Comprueba lo que sabes#

Pregunta 1 de 5

Si heroe.stats es undefined, ¿qué hace heroe?.stats?.rango?

Tu turno#

Leerlo no es lo mismo que saber escribirlo. Resuélvelo aquí mismo: edita el código de partida y complétalo para imprimir con console.log una ficha por héroe a partir de un roster sucio —accediendo con ?. y dando defaults con ??, cuidado con el novato de 0 partidas— y ejecútalo. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.

Ejercicio · en esta página

Pinta fichas de datos sucios sin romper

A partir de un roster con datos sucios (héroes sin stats, sin rango, y un novato con 0 partidas legítimas), imprime con console.log una ficha por héroe (nombre, rango, partidas y winrate) sin que nada reviente. Da 'Sin clasificar' si falta el rango y no confundas el 0 con un dato ausente.

Paso 1: Que funcione

  • Imprimes una ficha por héroe con console.log y ninguna revienta cuando faltan datos.
  • Si falta el rango, muestras 'Sin clasificar'.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Acceso defensivo a la antigua: comprueba stats con && antes de leer dentro, así
// no revienta con Mercy (que no trae stats). Funciona y pinta todas las fichas.
//
// Su trampa: usa || para los valores por defecto. Y || trata el 0 como "vacío",
// así que Echo (un novato con 0 partidas DE VERDAD) sale como "sin datos partidas".
// Es un dato real perdido: justo el bug que ?? viene a arreglar.
// ════════════════════════════════════════════════════════════════════════════

const roster = [
  {
    nombre: "Tracer",
    rol: "Daño",
    stats: { rango: "Diamante", partidas: 120, victorias: 78 },
  },
  // sin stats
  { nombre: "Mercy", rol: "Apoyo" },
  // sin rango
  { nombre: "Genji", rol: "Daño", stats: { partidas: 150, victorias: 72 } },
  // ceros reales
  {
    nombre: "Echo",
    rol: "Daño",
    stats: { rango: "Sin clasificar", partidas: 0, victorias: 0 },
  },
];

function ficha(heroe) {
  // Rango: solo se lee si stats existe; si no, "Sin clasificar".
  let rango = "Sin clasificar";
  if (heroe.stats && heroe.stats.rango) {
    rango = heroe.stats.rango;
  }

  // Partidas con ||: OJO, el 0 de Echo se trata como vacío y sale "sin datos" (mal).
  let partidas = "sin datos";
  if (heroe.stats) {
    // 0 || "sin datos" → "sin datos"
    partidas = heroe.stats.partidas || "sin datos";
  }

  // Winrate: solo tiene sentido si jugó alguna partida (evita dividir por cero).
  let winrate = "";
  if (heroe.stats && heroe.stats.partidas > 0) {
    winrate =
      ((heroe.stats.victorias / heroe.stats.partidas) * 100).toFixed(1) + "%";
  }

  return (
    heroe.nombre + "" + rango + " · " + partidas + " partidas · " + winrate
  );
}

// Imprime una ficha por héroe, una línea por cada uno.
for (const heroe of roster) {
  // una línea por héroe
  console.log(ficha(heroe));
}

Por qué este nivel

  • Acceso defensivo con && a mano: no revienta con Mercy (sin stats), pero es verboso.
  • Usa || para los defaults: trata el 0 de Echo como vacío y lo muestra como 'sin datos' (un dato real perdido).
  • Funciona, pero ese bug del 0 es justo lo que el capítulo viene a arreglar.