Javascript
desarrollo web
tutorial
ES6
Si vienes de otro lenguaje de programación y estás empezando con JavaScript, probablemente te hayas dado cuenta de que el ecosistema ha cambiado muchísimo en los últimos años. Lo que solías ver en tutoriales antiguos (var, callbacks anidados, concatenación de strings con +) ya no es la forma en que se escribe JavaScript hoy.
Este artículo cubre las herramientas y patrones modernos que realmente se usan en proyectos actuales. No es una lista exhaustiva de características, sino las cosas que aparecen todos los días en código de producción.
Cuando JavaScript era joven, todo el mundo usaba var
. Pero var
tiene comportamientos extraños con el scope que pueden causar bugs sutiles. Hoy en día, se usa const
y let
. La regla es simple, usa const
por defecto. Solo cambia a let
cuando realmente necesites reasignar la variable. Esto hace que el código sea más predecible y fácil de razonar.
1const apiUrl = 'https://example.com'; 2const user = { name: 'Ana', age: 28 }; 3 4// Esto va a fallar 5// apiUrl = 'otra-url'; 6 7// Pero puedes modificar propiedades de objetos 8user.age = 29; // Esto está bien
Al principio puede parecer raro usar const
para objetos que modificas, pero recuerda: const
significa que la referencia no cambia, no que el objeto sea inmutable.
Las funciones flecha no son solo una forma más corta de escribir funciones. La diferencia real está en cómo manejan this
, lo cual puede evitar muchos dolores de cabeza.
1// Antes 2function doble(n) { 3 return n * 2; 4} 5 6// Ahora 7const doble = n => n * 2;
Las funciones flecha se usan casi todo el tiempo: en callbacks de arrays, en promesas, en event handlers. La única excepción es cuando necesitas que this
se refiera al objeto que contiene el método.
1const numbers = [1, 2, 3, 4]; 2const doubled = numbers.map(n => n * 2);
Este patrón es tan común que se vuelve segunda naturaleza rápidamente.
JavaScript moderno tiene algunas características que hacen que trabajar con objetos y arrays sea mucho más limpio.
Si estás creando un objeto y los nombres de las variables coinciden con los nombres de las propiedades, puedes omitir la repetición:
1const name = 'Carlos'; 2const age = 30; 3 4// Antes 5const user = { name: name, age: age }; 6 7// Ahora 8const user = { name, age };
Esto puede parecer un detalle pequeño, pero cuando estás construyendo muchos objetos, marca una diferencia real en legibilidad.
Destructuring es una de esas características que una vez que empiezas a usar, no puedes vivir sin ella. Te permite extraer valores de objetos y arrays de forma elegante:
1const user = { 2 name: 'Ana', 3 age: 28, 4 email: 'ana@example.com' 5}; 6 7// Extrae solo lo que necesitas 8const { name, age } = user; 9 10// O renombra si hay conflictos 11const { name: userName } = user;
Con arrays es igual de útil:
1const colors = ['rojo', 'verde', 'azul']; 2const [first, second] = colors;
Esto es especialmente útil en parámetros de funciones. En lugar de recibir un objeto completo y acceder a sus propiedades dentro de la función, puedes destructurar directamente en los parámetros:
1// Antes 2function greet(user) { 3 console.log(`Hola, ${user.name}`); 4} 5 6// Ahora 7function greet({ name }) { 8 console.log(`Hola, ${name}`); 9}
Si vienes de lenguajes funcionales, estos te resultarán familiares. Si no, puede que al principio parezcan extraños, pero una vez que se entienden, hacen el código mucho más limpio.
map transforma cada elemento:
1const numbers = [1, 2, 3, 4]; 2const doubled = numbers.map(n => n * 2); 3// [2, 4, 6, 8]
filter se queda solo con los elementos que cumplen una condición:
1const numbers = [1, 2, 3, 4, 5, 6]; 2const evens = numbers.filter(n => n % 2 === 0); 3// [2, 4, 6]
reduce es el más poderoso pero también el más difícil de entender al principio. Reduce un array a un solo valor:
1const numbers = [1, 2, 3, 4]; 2const sum = numbers.reduce((acc, n) => acc + n, 0); 3// 10
La clave con reduce es entender que estás "acumulando" algo. El primer argumento es el acumulador, el segundo es el elemento actual, y el último argumento (0 en este caso) es el valor inicial del acumulador.
Las operaciones asincrónicas solían manejarse con callbacks. Y si necesitabas hacer varias operaciones en secuencia, terminabas con lo que llamamos "callback hell":
1getData(function(a) { 2 getMoreData(a, function(b) { 3 getEvenMoreData(b, function(c) { 4 console.log(c); 5 }); 6 }); 7});
Es difícil de leer y aún más difícil de mantener. Luego llegaron las Promesas, que mejoraron las cosas:
1getData() 2 .then(a => getMoreData(a)) 3 .then(b => getEvenMoreData(b)) 4 .then(c => console.log(c)) 5 .catch(error => console.error(error));
Pero la verdadera revolución fue async/await. Hace que el código asincrónico se vea como código sincrónico:
1async function fetchUser(id) { 2 try { 3 const response = await fetch(`https://example.com/users/${id}`); 4 const user = await response.json(); 5 return user; 6 } catch (error) { 7 console.error('Error:', error); 8 } 9}
Esto es mucho más fácil de leer. El código hace exactamente lo que parece que hace: espera la respuesta, la convierte a JSON, y retorna el usuario. Si algo falla, lo captura en el catch.
Para hacer peticiones HTTP, se usa Fetch API. Es mucho más simple que XMLHttpRequest (que probablemente nunca necesitarás usar):
1async function getUsers() { 2 const response = await fetch('https://example.com/users'); 3 4 if (!response.ok) { 5 throw new Error(`Error: ${response.status}`); 6 } 7 8 return await response.json(); 9}
Una cosa importante: Fetch no rechaza la promesa en errores HTTP (como 404 o 500). Por eso siempre hay que revisar response.ok
antes de procesar la respuesta.
Si necesitas hacer múltiples peticiones y no dependen una de otra, usa Promise.all
para ejecutarlas en paralelo:
1async function fetchMultipleUsers(ids) { 2 const promises = ids.map(id => 3 fetch(`https://example.com/users/${id}`).then(r => r.json()) 4 ); 5 6 return await Promise.all(promises); 7}
Esto es mucho más rápido que hacer las peticiones en secuencia, especialmente cuando hay muchas.
En JavaScript moderno, cada archivo puede ser un módulo. Puedes exportar funciones, clases, o valores, e importarlos donde los necesites. Hay dos tipos de exports: named exports y default exports.
Named exports son útiles cuando quieres exportar múltiples cosas de un archivo:
1// utils.js 2export const PI = 3.14159; 3 4export function sum(a, b) { 5 return a + b; 6} 7 8// app.js 9import { PI, sum } from './utils.js';
Default exports son para cuando tienes una cosa principal que exportar:
1// User.js 2export default class User { 3 constructor(name) { 4 this.name = name; 5 } 6} 7 8// app.js 9import User from './User.js';
Puedes usar ambos en el mismo archivo, pero es mejor mantenerlo simple: si un archivo tiene una cosa principal, usa default export. Si tiene varias utilidades relacionadas, usa named exports.
Si has trabajado con Node.js, probablemente has visto require
y module.exports
. Esa es la sintaxis de CommonJS, el sistema de módulos original de Node.
1// CommonJS (viejo) 2const fs = require('fs'); 3module.exports = myFunction; 4 5// ES Modules (moderno) 6import fs from 'fs'; 7export default myFunction;
ES Modules es el estándar oficial de JavaScript y es lo que deberías usar en código nuevo. Node.js moderno soporta ambos, pero ES Modules es el futuro.
Antes se concatenaban strings con +
, y era feo:
1const name = 'Ana'; 2const greeting = 'Hola, ' + name + '!';
Ahora se usan template literals con backticks:
1const greeting = `Hola, ${name}!`;
Puedes poner cualquier expresión JavaScript dentro de ${}
:
1const price = 99.99; 2const message = `El total es: $${(price * 1.21).toFixed(2)}`;
Y lo mejor: puedes escribir strings multilínea sin trucos raros:
1const html = ` 2 <div class="card"> 3 <h2>${title}</h2> 4 <p>${description}</p> 5 </div> 6`;
Esto es especialmente útil cuando estás generando HTML o SQL queries (aunque para SQL deberías usar prepared statements por seguridad).
El spread operator es como "desempacar" un array u objeto:
1const arr1 = [1, 2, 3]; 2const arr2 = [4, 5, 6]; 3const combined = [...arr1, ...arr2]; 4// [1, 2, 3, 4, 5, 6]
Con objetos es súper útil para crear copias con modificaciones:
1const user = { name: 'Ana', age: 28 }; 2const updatedUser = { ...user, age: 29 }; 3// { name: 'Ana', age: 29 }
Este operador salva de escribir tanto código defensivo. Antes, si querías acceder a una propiedad anidada que podría no existir, tenías que hacer esto:
1const city = user && user.address && user.address.city;
Ahora:
1const city = user?.address?.city;
Si cualquier parte de la cadena es null o undefined, toda la expresión retorna undefined en lugar de explotar con un error.
Este es diferente del operador OR (||
). OR retorna el lado derecho si el izquierdo es cualquier valor "falsy" (incluyendo 0, '', false). Nullish coalescing solo retorna el lado derecho si el izquierdo es null o undefined:
1const value = 0; 2 3console.log(value || 10); // 10 (porque 0 es falsy) 4console.log(value ?? 10); // 0 (porque 0 no es null/undefined)
Esto es especialmente útil para valores por defecto:
1const port = process.env.PORT ?? 3000;
En JavaScript moderno, especialmente si usas React u otros frameworks modernos, se trata de evitar modificar datos directamente. En lugar de eso, se crean nuevas versiones.
1// ❌ Mutación 2const user = { name: 'Ana', age: 28 }; 3user.age = 29; 4 5// ✓ Inmutabilidad 6const updatedUser = { ...user, age: 29 };
¿Por qué? Porque hace que el código sea más predecible. Cuando pasas un objeto a una función, sabes que esa función no va a modificarlo. Esto también hace que sea más fácil rastrear cambios y debuggear.
Con arrays, usa métodos que retornan nuevos arrays en lugar de modificar el original:
1const numbers = [1, 2, 3]; 2 3// ❌ Métodos que mutan 4numbers.push(4); 5numbers.sort(); 6 7// ✓ Métodos inmutables 8const withNew = [...numbers, 4]; 9const sorted = [...numbers].sort();
ESLint es una herramienta que analiza tu código y te dice cuando estás haciendo algo que probablemente no deberías. Al principio puede parecer molesto, pero te va a salvar de bugs tontos.
Una configuración básica incluye:
No necesitas una configuración súper estricta para empezar. Es mejor comenzar con eslint:recommended
y ajustar según tus necesidades.
Todos estos conceptos pueden parecer mucho al principio, pero la verdad es que cuando empiezas a escribir JavaScript moderno, estos patrones se vuelven naturales rápidamente.
Aquí está un ejemplo real de cómo se ve todo esto junto:
1// api.js 2export const fetchUsers = async () => { 3 try { 4 const response = await fetch('https://example.com/users'); 5 if (!response.ok) throw new Error('Network error'); 6 return await response.json(); 7 } catch (error) { 8 console.error('Failed to fetch users:', error); 9 return []; 10 } 11}; 12 13// app.js 14import { fetchUsers } from './api.js'; 15 16const getActiveUsers = async () => { 17 const users = await fetchUsers(); 18 const active = users.filter(user => user.active); 19 const names = active.map(user => user.name); 20 21 return names; 22}; 23 24const displayUsers = async () => { 25 const names = await getActiveUsers(); 26 const message = `Active users: ${names.join(', ')}`; 27 console.log(message); 28};
Este código usa async/await, arrow functions, destructuring implícito en los callbacks de map/filter, template literals, y módulos ES6. Y lo mejor es que es fácil de leer y entender.
JavaScript ha evolucionado muchísimo. Las características modernas no son solo "syntax sugar" - realmente hacen que el código sea más mantenible, más fácil de razonar, y menos propenso a bugs. No necesitas aprender todo de una vez. Empieza usando const/let en lugar de var, funciones flecha, y template literals. Luego agrega destructuring y los métodos de arrays. Async/await vendrá naturalmente cuando necesites manejar operaciones asincrónicas.
Lo importante es escribir código, cometer errores, y aprender de ellos. JavaScript moderno es mucho más amigable de lo que era hace unos años.