In the first part of this article, we explored the basics of modern JavaScript and some essential best practices to start writing cleaner, more efficient code. But as developers, we know that there is always more to learn and improve.
When working with objects or nested structures, we sometimes face the need to check if a property exists before trying to access it. The optional chaining operator (?.) is a powerful tool that simplifies this task, avoiding property access errors of null or undefined values.
Imagine that you have a complex object structure and you are not sure if certain properties exist in it. Without optional chaining, you would have to do manual checks at each step, which can make your code longer and less readable. With the ?. operator, you can safely access properties and get undefined if any of the intermediate properties do not exist.
const producto = {}; const impuesto = producto?.precio?.impuesto; console.log(impuesto); // undefined
In this case, since the product does not have the price property, the optional chaining returns undefined instead of generating an error.
Imagine that you have a list of products with different properties, some of which may be empty or undefined:
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 });
In this example, optional chaining allows us to avoid errors when trying to access product.details.tax, even if details is null or absent.
Optional chaining can also be used with functions, which is very useful when you have functions that may not be defined on an object:
const usuario = { nombre: 'Juan', obtenerEdad: null }; const edad = usuario.obtenerEdad?.(); console.log(edad); // undefined
Here, the getAge function is undefined (it's null), but it doesn't throw an error, it just returns undefined.
When you work with asynchronous operations in JavaScript, such as getting data from an API or reading files, the async/await syntax can be your best friend. Instead of using promises with .then() and .catch(), async/await allows you to write asynchronous code in a cleaner and more readable way, similar to how we would write synchronous code.
Suppose we are working with an API that returns data. Using async/await instead of .then() makes the flow much easier to follow:
const producto = {}; const impuesto = producto?.precio?.impuesto; console.log(impuesto); // undefined
Imagine that you have a web page where you need to display user information from an API. Here's an example of how you could do it using async/await to get the data and render it to the interface:
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 });
Returns an array with all the values of the properties of an object. Perfect when you just need the values without the keys.
Example:
const usuario = { nombre: 'Juan', obtenerEdad: null }; const edad = usuario.obtenerEdad?.(); console.log(edad); // undefined
This is the most versatile method. Returns an array of arrays, where each sub-array contains a key and its corresponding value. This is useful if you want to work with both keys and values in a single operation.
Example:
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); } }
Did you know that you can combine these methods with for...of to make your code even cleaner? Here is an example using Object.entries():
Example:
// 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> <h3> ¿Qué mejoramos con async/await? </h3> <ol> <li> <strong>Manejo claro de errores:</strong> 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.</li> <li> <strong>Código más legible:</strong> La estructura de await hace que el flujo del código se lea de manera secuencial, como si fuera código sincrónico.</li> <li> <strong>Evita el anidamiento:</strong> Con async/await puedes evitar los callbacks anidados (el famoso "callback hell") y las promesas encadenadas.</li> </ol> <p>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!</p> <h2> 10. Métodos modernos para objetos </h2> <p>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.</p> <h3> Object.keys() </h3> <p>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.</p> <p><strong>Ejemplo:</strong><br> </p> <pre class="brush:php;toolbar:false">const obj = { a: 1, b: 2, c: 3 }; const claves = Object.keys(obj); console.log(claves); // ["a", "b", "c"]
This approach is cleaner and easier to read, especially if you are working with large or complex objects.
When you need to associate values with keys that are not strings or symbols, use Map. It is more robust and maintains the type and order of the keys.
Example:
const producto = {}; const impuesto = producto?.precio?.impuesto; console.log(impuesto); // undefined
Symbols are a JavaScript feature that allows you to create unique and immutable keys, making them a powerful tool when we need to ensure that a value is not accidentally overwritten or accessed. Symbols cannot be accessed by methods like Object.keys(), for...in, or JSON.stringify(), making them perfect for private or "hidden" values.
When we create properties of an object using keys such as text strings, they can be easily manipulated or overwritten. However, symbols ensure that each key is unique, even if we create symbols with the same name. Additionally, symbols will not appear in object property enumerations.
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 });
In this example, the hiddenKey key is unique, and although another part of our code could have created another Symbol('hidden'), it would be completely different and would not affect the value stored in obj.
You can even use Symbol together with Object.defineProperty to add properties to objects in a more controlled way, ensuring that the properties are non-enumerable.
const usuario = { nombre: 'Juan', obtenerEdad: null }; const edad = usuario.obtenerEdad?.(); console.log(edad); // undefined
In this example, secretKey will not appear in the object's key enumeration, making it ideal for "private" values that should not be accessed or modified by accident.
In JavaScript, handling large numbers can be a real challenge. The Number data type has a limit on representing integers accurately: the largest safe integer value is 9007199254740991 (also known as Number.MAX_SAFE_INTEGER). If you try to work with numbers larger than this, you may lose precision, which could cause errors in your application.
For example, imagine you receive a large number from an external API:
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); } }
As you see, the number 9007199254740999 is incorrectly converted to 9007199254741000. This can be problematic if the number is critical to your application, such as a unique identifier or a financial amount.
How to avoid this problem?
A simple and elegant solution is to use the BigInt data type, introduced in ECMAScript 2020. BigInt can handle much larger numbers without losing precision. However, JSON doesn't natively handle BigInt, so you'll need to convert numbers to strings when serializing them and then convert them back when you deserialize them.
Here is an example of how you could do it:
const producto = {}; const impuesto = producto?.precio?.impuesto; console.log(impuesto); // undefined
By using this approach, you can maintain the accuracy of large numbers without losing important data. When you need the number again, just convert it back to BigInt:
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 });
If you don't want to work with BigInt or if performance is a concern, another strategy is to simply treat large numbers as strings in JSON. This avoids the precision problem at the cost of having to do conversions in your code.
Example:
const usuario = { nombre: 'Juan', obtenerEdad: null }; const edad = usuario.obtenerEdad?.(); console.log(edad); // undefined
Proper handling of large numbers is not only crucial for the accuracy of the calculations, but also for maintaining data integrity. This is especially important when you work with third-party APIs or systems that you don't fully control. A misinterpreted number could lead to failures in your application, or worse, errors in data that could be critical, such as the handling of financial transactions or unique identifiers in databases.
Remember: don't ignore precision limits. Although it may seem like a small detail, it is an area where applications can fail in unexpected and costly ways.
In JavaScript, if statements implicitly convert expressions to "truthy" or "falsy" values, which can lead to unexpected results if this behavior is not taken into account. Although this behavior can be useful at times, it is recommended to be explicit in the comparison to avoid subtle errors and improve code readability.
const producto = {}; const impuesto = producto?.precio?.impuesto; console.log(impuesto); // undefined
In the example above, the condition is not executed, since 0 is considered "falsy". However, this behavior can be difficult to detect when working with more complex values.
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 });
Tip: Whenever you are dealing with values that may be false, such as 0, null, false, or "", it is best to be explicit in your comparison. This way you ensure that the logic is executed according to your expectations and not because of implicit type coercion behavior.
Let's consider that you have an object that can be null, an empty array [], or an empty object {}. If you do something like this:
const usuario = { nombre: 'Juan', obtenerEdad: null }; const edad = usuario.obtenerEdad?.(); console.log(edad); // undefined
Although [] (an empty array) is a valid and truthful object, it can lead to confusion in the future if you don't fully understand the behavior. Instead of relying on implicit coercion, it is best to make more explicit comparisons, such as:
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); } }
By defining conditions explicitly, you reduce the risk of errors caused by JavaScript's automatic coercion. This approach makes your code clearer, more readable, and more predictable. Additionally, it improves maintainability, as other people (or yourself in the future) will be able to quickly understand the logic without having to remember the implicit behavior of falsy values in JavaScript.
One of the most confusing behaviors of JavaScript comes from the non-strict equality operator (==). This operator performs what is known as type coercion, which means that it attempts to convert values to a common type before comparing them. This can produce results surprisingly unexpected and very difficult to debug.
For example:
const producto = {}; const impuesto = producto?.precio?.impuesto; console.log(impuesto); // undefined
This is the kind of thing that can drive you crazy when you're developing. The == operator compares [] (an empty array) with ![] (which turns out to be false, since [] is considered a true value and ![] converts it to false). However, by JavaScript's internal coercion rules, this is a valid result, although it doesn't make sense at first glance.
JavaScript converts both sides of the comparison to a common type before comparing them. In this case, the empty array [] becomes false when compared to the boolean value of ![]. This type of coercion is a clear example of how subtle and difficult to identify errors can occur.
To avoid these problems, whenever possible, you should use strict equality (===). The difference is that this operator does not perform type coercion . This means that it compares both the value and type of the variables strictly.
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 });
Here are some more common examples of how non-strict equality (==) can be problematic:
const usuario = { nombre: 'Juan', obtenerEdad: null }; const edad = usuario.obtenerEdad?.(); console.log(edad); // undefined
The above is the detailed content of Best Practices in Modern JavaScript - Part 2. For more information, please follow other related articles on the PHP Chinese website!