Mejores Prácticas en JavaScript Moderno - Parte 2


En la primera parte de este artículo, exploramos las bases de JavaScript moderno y algunas mejores prácticas esenciales para comenzar a escribir código más limpio y eficiente. Pero como desarrolladores, sabemos que siempre hay más por aprender y mejorar.

8. Encadenamiento opcional (?.)

Cuando trabajamos con objetos o estructuras anidadas, a veces nos enfrentamos a la necesidad de verificar si una propiedad existe antes de intentar acceder a ella. El operador de encadenamiento opcional (?.) es una herramienta poderosa que simplifica esta tarea, evitando errores de acceso a propiedades de valores null o undefined.

¿Por qué es útil?

Imagina que tienes una estructura de objeto compleja y no estás seguro si ciertas propiedades existen en ella. Sin el encadenamiento opcional, tendrías que hacer verificaciones manuales en cada paso, lo cual puede hacer que tu código sea más largo y menos legible. Con el operador ?., puedes acceder a propiedades de manera segura y obtener undefined si alguna de las propiedades intermedias no existe.

Ejemplo básico

const producto = {};
const impuesto = producto?.precio?.impuesto;
console.log(impuesto); // undefined

En este caso, como producto no tiene la propiedad precio, el encadenamiento opcional devuelve undefined en lugar de generar un error.

Ejemplo con un objeto más complejo

Imagina que tienes una lista de productos con diferentes propiedades, algunas de las cuales pueden estar vacías o no definidas:

const productos = [
  { nombre: 'Laptop', detalles: { precio: 1000 } },
  { nombre: 'Teléfono', detalles: null },
  { nombre: 'Tablet', detalles: { precio: 500, impuesto: 50 } }
];

// Acceso seguro a la propiedad 'impuesto' de cada producto
productos.forEach(producto => {
  const impuesto = producto?.detalles?.impuesto;
  console.log(impuesto); // undefined, null o el valor real
});

En este ejemplo, el encadenamiento opcional nos permite evitar errores al intentar acceder a producto.detalles.impuesto, incluso si detalles es null o está ausente.

¿Cómo mejora tu código?

  • Evita errores de acceso a propiedades nulas o indefinidas.
  • Simplifica el código, haciendo que las verificaciones de seguridad sean más limpias y legibles.
  • Permite trabajar con datos inciertos o incompletos, algo muy común cuando trabajas con respuestas de APIs o bases de datos.

Bonus: Encadenamiento opcional con funciones

El encadenamiento opcional también se puede usar con funciones, lo cual es muy útil cuando tienes funciones que pueden no estar definidas en un objeto:

const usuario = { nombre: 'Juan', obtenerEdad: null };
const edad = usuario.obtenerEdad?.();
console.log(edad); // undefined

Aquí, la función obtenerEdad no está definida (es null), pero no se produce un error, simplemente retorna undefined.


9. Usa async/await para manejo de asincronía

Cuando trabajas con operaciones asíncronas en JavaScript, como obtener datos desde una API o leer archivos, la sintaxis de async/await puede ser tu mejor amiga. En lugar de usar promesas con .then() y .catch(), async/await permite escribir código asíncrono de manera más limpia y legible, similar a cómo escribiríamos código sincrónico.

¿Por qué usar async/await?

  • Simplicidad y legibilidad: Evitas las “cadenas de promesas” que pueden volverse complicadas de leer y mantener.
  • Manejo de errores más intuitivo: Usar try/catch para manejar errores es mucho más claro que usar .catch().
  • Control más preciso: Permite usar await en cualquier parte de la función, lo que facilita el control de flujos asíncronos más complejos.

Ejemplo básico de uso:

Supongamos que estamos trabajando con una API que devuelve datos. Usar async/await en lugar de .then() hace que el flujo sea mucho más fácil de seguir:

async function obtenerDatos() {
  try {
    const respuesta = await fetch('https://api.ejemplo.com/datos');
    if (!respuesta.ok) {
      throw new Error('Error al obtener los datos');
    }
    const datos = await respuesta.json();
    console.log(datos);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

Ejemplo práctico: obteniendo datos de una API y mostrando en la UI

Imagina que tienes una página web donde necesitas mostrar información de usuarios de una API. Aquí hay un ejemplo de cómo podrías hacerlo usando async/await para obtener los datos y renderizarlos en la interfaz:

// Función para obtener y mostrar los datos de usuarios
async function obtenerUsuarios() {
  try {
    const respuesta = await fetch('https://api.ejemplo.com/usuarios');
    if (!respuesta.ok) {
      throw new Error('No se pudieron cargar los usuarios');
    }
    const usuarios = await respuesta.json();
    mostrarUsuariosEnUI(usuarios);
  } catch (error) {
    console.error('Hubo un problema con la carga de los usuarios:', error);
    alert('Error al cargar los usuarios. Intenta más tarde.');
  }
}

// Función para renderizar usuarios en el HTML
function mostrarUsuariosEnUI(usuarios) {
  const contenedor = document.getElementById('contenedor-usuarios');
  contenedor.innerHTML = usuarios.map(usuario => `
    <div class="usuario">
      <h3>${usuario.nombre}</h3>
      <p>Email: ${usuario.email}</p>
    </div>
  `).join('');
}

// Llamada a la función para obtener y mostrar los usuarios
obtenerUsuarios();

¿Qué mejoramos con async/await?

  1. Manejo claro de errores: Usamos try/catch para capturar cualquier error que pueda ocurrir durante la obtención de datos, ya sea un problema con la red o con la API.
  2. Código más legible: La estructura de await hace que el flujo del código se lea de manera secuencial, como si fuera código sincrónico.
  3. Evita el anidamiento: Con async/await puedes evitar los callbacks anidados (el famoso “callback hell”) y las promesas encadenadas.

Usar async/await no solo mejora la calidad de tu código, sino que también hace que sea mucho más fácil depurar y mantener proyectos a largo plazo. ¡Es una herramienta poderosa que deberías incorporar siempre que trabajes con asincronía en JavaScript!


10. Métodos modernos para objetos

Cuando trabajamos con objetos en JavaScript, es común que necesitemos iterar sobre las claves y los valores, o incluso extraer solo las claves o valores. Los métodos modernos como Object.entries(), Object.values() y Object.keys() hacen que estas tareas sean mucho más fáciles y legibles.


Object.keys()

Este método devuelve un array con todas las claves de un objeto. Es útil cuando solo necesitas acceder a las claves y no a los valores.

Ejemplo:

const obj = { a: 1, b: 2, c: 3 };
const claves = Object.keys(obj);
console.log(claves); // ["a", "b", "c"]

Object.values()

Devuelve un array con todos los valores de las propiedades de un objeto. Perfecto cuando solo necesitas los valores sin las claves.

Ejemplo:

const obj = { a: 1, b: 2, c: 3 };
const valores = Object.values(obj);
console.log(valores); // [1, 2, 3]

Object.entries()

Este es el método más versátil. Devuelve un array de arrays, donde cada sub-array contiene una clave y su valor correspondiente. Esto es útil si deseas trabajar tanto con claves como con valores en una sola operación.

Ejemplo:

const obj = { a: 1, b: 2, c: 3 };
Object.entries(obj).forEach(([clave, valor]) => {
  console.log(`La clave ${clave} tiene el valor ${valor}`);
});

Bonus: Iteración con for...of

¿Sabías que puedes combinar estos métodos con for...of para hacer tu código aún más limpio? Aquí te dejo un ejemplo usando Object.entries():

Ejemplo:

const obj = { a: 1, b: 2, c: 3 };
for (const [clave, valor] of Object.entries(obj)) {
  console.log(`La clave ${clave} tiene el valor ${valor}`);
}

Este enfoque es más limpio y fácil de leer, especialmente si estás trabajando con objetos grandes o complejos.


11. Usa Map para claves no primitivas

Cuando necesitas asociar valores a claves que no sean cadenas o símbolos, utiliza Map. Es más robusto y mantiene el tipo y el orden de las claves.

Ejemplo:

const mapa = new Map();
const clave = { id: 1 };
mapa.set(clave, 'valor');
console.log(mapa.get(clave)); // 'valor'

12. Utiliza Symbol para claves únicas

Los Symbol son una característica de JavaScript que permite crear claves únicas e inmutables, lo que las convierte en una herramienta poderosa cuando necesitamos asegurarnos de que un valor no sea sobrescrito o accesible accidentalmente. Los símbolos no pueden ser accedidos mediante métodos como Object.keys(), for...in, o JSON.stringify(), lo que los hace perfectos para valores privados o “ocultos”.

¿Por qué usar Symbol?

Cuando creamos propiedades de un objeto utilizando claves como cadenas de texto, estas pueden ser fácilmente manipuladas o sobrescritas. Sin embargo, los símbolos garantizan que cada clave sea única, incluso si creamos símbolos con el mismo nombre. Además, los símbolos no aparecerán en las enumeraciones de propiedades del objeto.

Ejemplo básico:

const obj = {};
const claveOculta = Symbol('oculta');
obj[claveOculta] = 'valor secreto';
console.log(obj[claveOculta]); // 'valor secreto'

En este ejemplo, la clave claveOculta es única, y aunque otra parte de nuestro código pudiera haber creado otro Symbol('oculta'), este sería completamente distinto y no afectaría el valor almacenado en obj.


Ejemplo avanzado: Combinando Symbol con Object.defineProperty

Puedes incluso usar Symbol junto con Object.defineProperty para agregar propiedades a objetos de manera más controlada, asegurando que las propiedades sean no enumerables.

const claveSecreta = Symbol('secreta');
const obj = {};

Object.defineProperty(obj, claveSecreta, {
  value: 'información confidencial',
  enumerable: false,  // La propiedad no será enumerable
  writable: false,    // La propiedad no será modificable
});

console.log(obj[claveSecreta]);  // 'información confidencial'
console.log(Object.keys(obj));   // []

En este ejemplo, claveSecreta no aparecerá en la enumeración de claves del objeto, lo que lo hace ideal para valores “privados” que no deben ser accesibles o modificados por accidente.

Consideraciones:

  • Los Symbol son útiles para evitar conflictos de nombres de propiedades, especialmente cuando trabajas con bibliotecas de terceros o APIs.
  • Aunque los símbolos no son estrictamente “privados”, el hecho de que no sean accesibles de manera convencional ayuda a proteger la integridad de los datos.
  • Ten en cuenta que los símbolos no se pueden serializar a JSON, por lo que si necesitas transmitir datos, asegúrate de manejar las propiedades Symbol de manera adecuada.

13. Ten cuidado con JSON y números grandes

En JavaScript, manejar números grandes puede ser un verdadero reto. El tipo de dato Number tiene un límite para representar enteros de manera precisa: el mayor valor entero seguro es 9007199254740991 (también conocido como Number.MAX_SAFE_INTEGER). Si intentas trabajar con números mayores que este, puedes perder precisión, lo que podría generar errores en tu aplicación.

Por ejemplo, imagina que recibes un número grande de una API externa:

console.log(
  JSON.parse('{"id": 9007199254740999}')
); 
// Salida: { id: 9007199254741000 } (precisión perdida)

Como ves, el número 9007199254740999 se convierte incorrectamente en 9007199254741000. Esto puede ser problemático si el número es crítico para tu aplicación, como un identificador único o un monto financiero.

¿Cómo evitar este problema?
Una solución sencilla y elegante es usar el tipo de dato BigInt, introducido en ECMAScript 2020. BigInt puede manejar números mucho más grandes sin perder precisión. Sin embargo, JSON no maneja de forma nativa BigInt, por lo que necesitarás convertir los números a cadenas al serializarlos y luego volver a convertirlos cuando los deserialices.

Aquí te dejo un ejemplo de cómo podrías hacerlo:

Solución con BigInt y JSON.stringify

const data = { id: 9007199254740999n }; // Usa BigInt para manejar el número grande
console.log(
  JSON.stringify(data, (key, value) => {
    if (typeof value === 'bigint') {
      return value.toString(); // Convierte BigInt a cadena
    }
    return value;
  })
);
// Salida: {"id":"9007199254740999"}

Al usar este enfoque, puedes mantener la precisión de los números grandes sin perder datos importantes. Cuando necesites el número de nuevo, solo conviértelo de nuevo a BigInt:

const parsedData = JSON.parse('{"id":"9007199254740999"}', (key, value) => {
  if (key === 'id') {
    return BigInt(value); // Convierte la cadena a BigInt
  }
  return value;
});
console.log(parsedData.id); // Salida: 9007199254740999n

Otras estrategias

Si no quieres trabajar con BigInt o si el rendimiento es una preocupación, otra estrategia es simplemente tratar los números grandes como cadenas en JSON. Esto evita el problema de precisión al costo de tener que hacer conversiones en tu código.

Ejemplo:

const data = { id: '9007199254740999' }; // Almacena el número como cadena
console.log(data.id); // Usa la cadena directamente cuando sea necesario

¿Por qué es importante?

El manejo adecuado de números grandes no solo es crucial para la precisión de los cálculos, sino también para mantener la integridad de los datos. Esto es especialmente importante cuando trabajas con APIs de terceros o sistemas que no controlas completamente. Un número mal interpretado podría llevar a fallos en tu aplicación, o lo que es peor, errores en los datos que podrían ser críticos, como el manejo de transacciones financieras o identificadores únicos en bases de datos.

Recuerda: no ignores los límites de precisión. Aunque puede parecer un detalle pequeño, es un área donde las aplicaciones pueden fallar de manera inesperada y costosa.


14. Maneja expresiones en sentencias if de forma explícita

En JavaScript, las sentencias if convierten implícitamente las expresiones en valores “truthy” o “falsy”, lo que puede generar resultados inesperados si no se tiene en cuenta este comportamiento. Aunque este comportamiento puede ser útil en ocasiones, se recomienda ser explícito en la comparación para evitar errores sutiles y mejorar la legibilidad del código.

¿Qué significa “truthy” o “falsy”?

  • “Falsy” se refiere a valores que se consideran equivalentes a false cuando se evalúan en una expresión condicional. Ejemplos: 0, "" (cadena vacía), null, undefined, NaN.
  • “Truthy” son todos los valores que no son falsy, es decir, cualquier valor que no sea uno de los anteriores. Ejemplo: cualquier número diferente de 0, cualquier cadena no vacía, los objetos, etc.

Ejemplo implícito (puede dar resultados inesperados)

const value = 0; // Aquí el valor es 0, que es "falsy"
if (value) {
  console.log('Esto no se ejecutará porque 0 es "falsy".');
}

En el ejemplo anterior, la condición no se ejecuta, ya que 0 se considera “falsy”. Sin embargo, este comportamiento puede ser difícil de detectar cuando se trabaja con valores más complejos.

Ejemplo explícito (mejor para la legibilidad)

const value = 0;
// Comprobación explícita para asegurarse de que value no sea 0
if (value !== 0) {
  console.log('Esto se ejecutará solo si value no es 0.');
} else {
  console.log('Esto se ejecutará porque value es 0.');
}

Consejo: Siempre que estés tratando con valores que puedan ser falsy, como 0, null, false o "", es mejor ser explícito en tu comparación. De esta manera, garantizas que la lógica se ejecute de acuerdo con tus expectativas y no por el comportamiento implícito de coerción de tipos.

Otro ejemplo con valores ambiguos

Consideremos que tienes un objeto que puede ser null, un arreglo vacío [], o un objeto vacío {}. Si haces algo como esto:

const obj = [];
if (obj) {
  console.log('El objeto es truthy');
} else {
  console.log('El objeto es falsy');
}

Aunque [] (un arreglo vacío) es un objeto válido y “truthy”, puede llevar a confusión en el futuro si no entiendes bien el comportamiento. En vez de depender de la coerción implícita, lo mejor es hacer comparaciones más explícitas, como:

const obj = [];
if (obj !== null && obj.length > 0) {
  console.log('El objeto no está vacío');
} else {
  console.log('El objeto está vacío o es nulo');
}

¿Por qué es importante?

Al definir las condiciones de forma explícita, reduces el riesgo de errores causados por la coerción automática de JavaScript. Este enfoque hace que tu código sea más claro, legible y predecible. Además, mejora la mantenibilidad, ya que otras personas (o tú mismo en el futuro) podrán entender rápidamente la lógica sin tener que recordar el comportamiento implícito de los valores falsy en JavaScript.


15. Usa igualdad estricta (===) siempre que sea posible

Uno de los comportamientos más confusos de JavaScript proviene del operador de igualdad no estricta (==). Este operador realiza lo que se conoce como coerción de tipos, lo que significa que intenta convertir los valores a un tipo común antes de compararlos. Esto puede producir resultados sorprendentemente inesperados y muy difíciles de depurar.

Por ejemplo:

console.log([] == ![]); // true (sorprendente y poco intuitivo)

Esto es el tipo de cosa que puede volverte loco cuando estás desarrollando. El operador == compara [] (un arreglo vacío) con ![] (que resulta ser false, ya que [] se considera un valor verdadero y ![] lo convierte a false). Sin embargo, por las reglas internas de coerción de JavaScript, este es un resultado válido, aunque no tiene sentido a primera vista.

¿Por qué ocurre esto?

JavaScript convierte ambos lados de la comparación a un tipo común antes de compararlos. En este caso, el arreglo vacío [] se convierte en false al ser comparado con el valor booleano de ![]. Este tipo de coerción es un ejemplo claro de cómo pueden ocurrir errores sutiles y difíciles de identificar.

Consejo: Usa siempre la igualdad estricta

Para evitar estos problemas, siempre que sea posible, debes usar igualdad estricta (===). La diferencia es que este operador no realiza coerción de tipos. Esto significa que compara tanto el valor como el tipo de las variables de manera estricta.

console.log([] === ![]); // false (como se espera)

Ejemplos más típicos

Aquí te dejo algunos ejemplos más comunes de cómo la igualdad no estricta (==) puede ser problemática:

console.log(0 == '');  // true (porque 0 se convierte a '' antes de la comparación)
console.log(0 === ''); // false (porque 0 y '' son de tipos diferentes)

console.log(false == 'false'); // false (comparación de tipo booleano con cadena)
console.log(false === 'false'); // false (diferente tipo y valor)

console.log(null == undefined); // true (pero no son lo mismo, aunque lo pasen con `==`)
console.log(null === undefined); // false (compara tipo y valor, y son distintos)

¿Por qué es importante usar ===?

  • Predicción y confiabilidad: Usar === te da comparaciones más predecibles, sin sorpresas ni conversiones de tipos.
  • Evita errores difíciles de detectar: Cuando tu código comienza a ser más grande y complejo, los errores relacionados con la coerción de tipos pueden ser muy difíciles de encontrar y depurar.
  • Mejora la legibilidad del código: Es más fácil para otros desarrolladores (o para ti mismo en el futuro) entender tu código cuando ves comparaciones explícitas, sin la confusión de las conversiones implícitas.
- Escrito por Juan Beresiarte -