ES2020 y ES2021 introdujeron un conjunto de operadores que simplifican patrones muy frecuentes en JavaScript: el manejo de valores nulos o indefinidos y la asignación condicional. El nullish coalescing (??), optional chaining (?.) y los operadores de asignación lógica (??=, ||=, &&=) son hoy herramientas cotidianas en cualquier código moderno.
Nullish coalescing (??): el problema de ||
El operador || devuelve el segundo operando cuando el primero es falsy (false, 0, '', null, undefined, NaN). Esto causa bugs cuando 0 o '' son valores válidos. El operador ?? solo considera null y undefined como "sin valor":
// El bug clásico con ||
function configurarTimeout(opciones) {
// Si opciones.timeout es 0 (desactivado), || lo trata como falsy
const timeout = opciones.timeout || 5000;
// timeout === 5000 incluso si se pasó 0 explícitamente ? BUG
}
// Con ??: 0 es un valor válido
function configurarTimeoutBien(opciones) {
const timeout = opciones.timeout ?? 5000;
// Si opciones.timeout === 0, timeout === 0 ?
// Si opciones.timeout === undefined o null, timeout === 5000 ?
}
// Ejemplos prácticos
const config = { debug: false, pagina: 0, nombre: '' };
config.debug || true; // true ¡bug! false es falsy
config.debug ?? true; // false correcto
config.pagina || 1; // 1 ¡bug! 0 es falsy
config.pagina ?? 1; // 0 correcto
config.nombre || 'anon'; // 'anon' ¡bug! '' es falsy
config.nombre ?? 'anon'; // '' correcto
Optional chaining (?.) : acceso seguro a propiedades
?. evalúa la expresión de la izquierda; si es null o undefined, devuelve undefined en lugar de lanzar un TypeError. Encadena sin problemas con otros ?.:
const usuario = {
nombre: 'Ana',
// address no existe
};
// Sin optional chaining múltiples comprobaciones manuales
const ciudad = usuario && usuario.address && usuario.address.city;
// Con optional chaining limpio y seguro
const ciudadSegura = usuario?.address?.city;
console.log(ciudadSegura); // undefined sin TypeError
// También funciona con métodos y notación de corchetes
const longitud = usuario?.nombre?.toUpperCase()?.length;
const primerPermiso = usuario?.permisos?.[0];
const resultado = usuario?.calcular?.('arg1', 'arg2');
// Combinado con ?? para valores por defecto
const ciudad2 = usuario?.address?.city ?? 'Sin ciudad';
console.log(ciudad2); // 'Sin ciudad'
Caso real: normalizar datos de API
// Respuesta de API con campos opcionales variables
function normalizarUsuario(raw) {
return {
id: raw.id,
nombre: raw.name ?? raw.username ?? 'Anónimo',
email: raw.email ?? null,
avatar: raw.avatar_url ?? raw.profile?.picture?.url ?? '/img/default.png',
ciudad: raw.location?.city ?? raw.address?.city ?? '',
activo: raw.is_active ?? raw.active ?? true,
rol: raw.role?.name ?? raw.permissions?.[0] ?? 'usuario',
};
}
Operadores de asignación lógica
Los tres operadores de asignación lógica (ES2021) combinan la lógica de ??, || y && con la asignación, pero con una particularidad importante: solo asignan si la condición del operador se cumple. Si la condición no se cumple, no hay asignación (y no se disparan setters).
// ??= : asigna solo si el valor es null o undefined
let config = { timeout: null, debug: undefined, host: 'localhost' };
config.timeout ??= 5000; // null ? asigna 5000
config.debug ??= false; // undefined ? asigna false
config.host ??= 'default'; // 'localhost' ? NO asigna (ya tiene valor)
console.log(config); // { timeout: 5000, debug: false, host: 'localhost' }
// ||= : asigna solo si el valor actual es falsy
let nombre = '';
nombre ||= 'Anónimo'; // '' es falsy ? asigna
console.log(nombre); // 'Anónimo'
let activo = false;
activo ||= true; // false es falsy ? asigna
console.log(activo); // true
// &&= : asigna solo si el valor actual es truthy
let usuario = { nombre: 'Ana' };
usuario.nombre &&= usuario.nombre.toUpperCase(); // truthy ? asigna
console.log(usuario.nombre); // 'ANA'
let sinNombre = null;
sinNombre &&= sinNombre.toUpperCase(); // null ? NO asigna, NO lanza error
console.log(sinNombre); // null
Diferencia crítica: asignación vs evaluación
// ??= NO es igual a: obj.prop = obj.prop ?? valor
let contador = 0;
let llamadas = 0;
const objeto = {
get valor() {
llamadas++;
return this._valor;
},
set valor(v) {
this._valor = v;
},
_valor: undefined,
};
// Con ??=: el setter NO se llama si el valor ya existe
objeto._valor = 'existente';
objeto.valor ??= 'nuevo'; // getter devuelve 'existente', no es null/undefined ? setter NO se llama
// Con asignación manual: el setter SIEMPRE se llama
objeto.valor = objeto.valor ?? 'nuevo'; // Llama al setter aunque no cambie nada
Patrones comunes en configuración fetch
// Construir opciones de fetch con defaults
function crearOpcionesFetch(opciones = {}) {
opciones.method ??= 'GET';
opciones.headers ??= {};
opciones.headers['Content-Type'] ??= 'application/json';
opciones.credentials ??= 'same-origin';
opciones.timeout ??= 10_000;
return opciones;
}
// Gestión de estado con ??=
class Store {
#estado = {};
setDefault(clave, valorDefecto) {
this.#estado[clave] ??= valorDefecto;
}
activarSiExiste(clave) {
this.#estado[clave] &&= { ...this.#estado[clave], activo: true };
}
}
La distinción entre ?? y || es fundamental: si el código necesita tratar 0, false o '' como valores válidos, siempre hay que usar ??. Y los operadores de asignación lógica no son solo atajo de teclado; su semántica de no-asignación-si-innecesaria es relevante cuando hay getters y setters con lógica de negocio.
