20 ejercicios avanzados de JavaScript resueltos paso a paso

20 ejercicios avanzados de JavaScript resueltos paso a paso: reduce, filtros, paginación, debounce, throttle, Promises, fetch, APIs, localStorage y más.


Si ya dominás variables, condicionales, ciclos, funciones y arrays, estos 20 ejercicios avanzados de JavaScript te permiten practicar problemas más cercanos a aplicaciones reales.

La guía combina manipulación de datos, reduce(), ordenamiento, paginación, asincronía, fetch(), Promises, localStorage, patrones como debounce y throttle, y un pequeño proyecto final.

Antes de empezar, podés repasar 20 ejercicios resueltos de JavaScript para principiantes y 20 ejercicios resueltos de arrays en JavaScript.

Índice de ejercicios

  1. Contar la frecuencia de palabras en un texto
  2. Agrupar objetos por una propiedad con reduce()
  3. Ordenar objetos por múltiples criterios
  4. Crear una búsqueda con varios filtros
  5. Implementar paginación de un array
  6. Crear un carrito de compras
  7. Calcular subtotal, descuento e impuestos
  8. Transformar datos anidados de una API
  9. Eliminar objetos duplicados por id
  10. Comparar dos arrays y detectar cambios
  11. Crear una función debounce()
  12. Crear una función throttle()
  13. Ejecutar tareas asíncronas en secuencia
  14. Ejecutar varias Promises en paralelo
  15. Manejar errores con async/await
  16. Consumir una API y filtrar resultados
  17. Reintentar una petición fallida
  18. Guardar objetos complejos en localStorage
  19. Crear un sistema de eventos personalizado
  20. Mini proyecto: buscador de usuarios desde una API

1. Contar la frecuencia de palabras en un texto

Enunciado: Contar cuántas veces aparece cada palabra ignorando mayúsculas y signos básicos.

const texto = "JavaScript es potente. JavaScript es flexible.";

const palabras = texto
    .toLowerCase()
    .replace(/[.,]/g, "")
    .split(" ");

const frecuencia = {};

for (const palabra of palabras) {
    frecuencia[palabra] = (frecuencia[palabra] || 0) + 1;
}

console.log(frecuencia);

toLowerCase() normaliza el texto, replace() elimina signos simples y split() crea un array de palabras. Después usamos un objeto como tabla de frecuencias.

2. Agrupar objetos por una propiedad con reduce()

Enunciado: Agrupar productos por categoría utilizando reduce().

const productos = [
    { nombre: "Mouse", categoria: "tecnologia" },
    { nombre: "Silla", categoria: "muebles" },
    { nombre: "Teclado", categoria: "tecnologia" }
];

const agrupados = productos.reduce(function (resultado, producto) {
    const categoria = producto.categoria;

    if (!resultado[categoria]) {
        resultado[categoria] = [];
    }

    resultado[categoria].push(producto);

    return resultado;
}, {});

console.log(agrupados);

reduce() transforma el array completo en una única estructura. En este caso, el resultado es un objeto cuyas propiedades son las categorías.

3. Ordenar objetos por múltiples criterios

Enunciado: Ordenar personas primero por edad y, en caso de empate, por nombre.

const personas = [
    { nombre: "Luis", edad: 30 },
    { nombre: "Ana", edad: 25 },
    { nombre: "Carlos", edad: 30 }
];

personas.sort(function (a, b) {
    if (a.edad !== b.edad) {
        return a.edad - b.edad;
    }

    return a.nombre.localeCompare(b.nombre);
});

console.log(personas);

sort() permite definir múltiples criterios. localeCompare() compara cadenas respetando reglas lingüísticas. Recordá que sort() modifica el array original.

4. Crear una búsqueda con varios filtros

Enunciado: Filtrar productos por texto, categoría y precio máximo.

const productos = [
    { nombre: "Mouse gamer", categoria: "tecnologia", precio: 40 },
    { nombre: "Teclado", categoria: "tecnologia", precio: 70 },
    { nombre: "Silla", categoria: "muebles", precio: 120 }
];

const texto = "mouse";
const categoria = "tecnologia";
const precioMaximo = 100;

const resultado = productos.filter(function (producto) {
    return producto.nombre.toLowerCase().includes(texto.toLowerCase())
        && producto.categoria === categoria
        && producto.precio <= precioMaximo;
});

console.log(resultado);

filter() crea un nuevo array con los elementos que cumplen todas las condiciones. Este patrón aparece mucho en buscadores y paneles administrativos.

5. Implementar paginación de un array

Enunciado: Obtener una página concreta de resultados.

function paginar(elementos, pagina, porPagina) {
    const inicio = (pagina - 1) * porPagina;
    const fin = inicio + porPagina;

    return elementos.slice(inicio, fin);
}

const datos = [1,2,3,4,5,6,7,8,9,10];

console.log(paginar(datos, 2, 3));

slice() devuelve una porción del array sin modificar el original. Para la página 2 con 3 elementos por página devuelve los elementos 4, 5 y 6.

6. Crear un carrito de compras

Enunciado: Calcular el total de un carrito con cantidades y precios.

const carrito = [
    { producto: "Mouse", precio: 25, cantidad: 2 },
    { producto: "Teclado", precio: 60, cantidad: 1 }
];

const total = carrito.reduce(function (acumulador, item) {
    return acumulador + item.precio * item.cantidad;
}, 0);

console.log(total);

El acumulador suma el subtotal de cada producto. Este patrón se usa constantemente en e-commerce y sistemas de facturación.

7. Calcular subtotal, descuento e impuestos

Enunciado: Aplicar un descuento y luego calcular impuestos.

const subtotal = 1000;
const descuentoPorcentaje = 10;
const impuestoPorcentaje = 21;

const descuento = subtotal * descuentoPorcentaje / 100;
const base = subtotal - descuento;
const impuesto = base * impuestoPorcentaje / 100;
const total = base + impuesto;

console.log({ subtotal, descuento, impuesto, total });

Separar cada etapa en variables hace que el cálculo sea más legible y fácil de auditar.

8. Transformar datos anidados de una API

Enunciado: Convertir una estructura anidada en una lista más simple.

const respuesta = {
    usuarios: [
        {
            id: 1,
            perfil: {
                nombre: "Ana",
                ciudad: "Santa Rosa"
            }
        },
        {
            id: 2,
            perfil: {
                nombre: "Luis",
                ciudad: "Córdoba"
            }
        }
    ]
};

const usuarios = respuesta.usuarios.map(function (usuario) {
    return {
        id: usuario.id,
        nombre: usuario.perfil.nombre,
        ciudad: usuario.perfil.ciudad
    };
});

console.log(usuarios);

map() es ideal para adaptar respuestas de APIs a la estructura que necesita nuestra interfaz.

9. Eliminar objetos duplicados por id

Enunciado: Conservar un solo objeto por cada id.

const usuarios = [
    { id: 1, nombre: "Ana" },
    { id: 2, nombre: "Luis" },
    { id: 1, nombre: "Ana duplicada" }
];

const mapa = new Map();

for (const usuario of usuarios) {
    mapa.set(usuario.id, usuario);
}

const unicos = [...mapa.values()];

console.log(unicos);

Map permite usar el id como clave. Cuando se repite una clave, el valor anterior se reemplaza por el nuevo.

10. Comparar dos arrays y detectar cambios

Enunciado: Obtener elementos agregados y eliminados entre dos listas.

const anterior = [1, 2, 3, 4];
const actual = [2, 3, 4, 5];

const agregados = actual.filter(function (id) {
    return !anterior.includes(id);
});

const eliminados = anterior.filter(function (id) {
    return !actual.includes(id);
});

console.log({ agregados, eliminados });

Comparar listas de esta forma es útil cuando necesitamos detectar cambios entre un estado anterior y uno nuevo.

11. Crear una función debounce()

Enunciado: Evitar ejecutar una función en cada pulsación y esperar a que el usuario deje de escribir.

function debounce(funcion, demora) {
    let temporizador;

    return function (...args) {
        clearTimeout(temporizador);

        temporizador = setTimeout(function () {
            funcion(...args);
        }, demora);
    };
}

const buscar = debounce(function (texto) {
    console.log("Buscando:", texto);
}, 500);

buscar("java");
buscar("javascript");

debounce() reinicia el temporizador cada vez que se llama. Es muy útil en buscadores, autocompletados y validaciones mientras el usuario escribe.

12. Crear una función throttle()

Enunciado: Limitar cuántas veces puede ejecutarse una función dentro de un intervalo.

function throttle(funcion, intervalo) {
    let habilitado = true;

    return function (...args) {
        if (!habilitado) {
            return;
        }

        habilitado = false;
        funcion(...args);

        setTimeout(function () {
            habilitado = true;
        }, intervalo);
    };
}

const informarScroll = throttle(function () {
    console.log("Scroll detectado");
}, 1000);

throttle() permite una ejecución cada cierto tiempo. Suele utilizarse con eventos frecuentes como scroll o resize.

13. Ejecutar tareas asíncronas en secuencia

Enunciado: Ejecutar varias operaciones respetando un orden.

function esperar(ms) {
    return new Promise(function (resolve) {
        setTimeout(resolve, ms);
    });
}

async function ejecutar() {
    console.log("Paso 1");
    await esperar(500);

    console.log("Paso 2");
    await esperar(500);

    console.log("Paso 3");
}

ejecutar();

await pausa la función async hasta que la Promise termina. Esto permite expresar procesos secuenciales de forma clara.

14. Ejecutar varias Promises en paralelo

Enunciado: Esperar varias tareas independientes al mismo tiempo.

function tarea(nombre, tiempo) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(nombre);
        }, tiempo);
    });
}

async function ejecutar() {
    const resultados = await Promise.all([
        tarea("A", 500),
        tarea("B", 300),
        tarea("C", 700)
    ]);

    console.log(resultados);
}

ejecutar();

Promise.all() espera que todas las Promises terminen y devuelve sus resultados en el mismo orden en que fueron enviadas.

API utilizada en los ejercicios

En los ejemplos que utilizan fetch() vamos a trabajar con JSONPlaceholder, una API REST gratuita pensada para pruebas, aprendizaje y prototipos.

La API proporciona datos ficticios de usuarios, publicaciones, comentarios, tareas y otros recursos. Esto permite practicar peticiones HTTP, Promises, async/await y procesamiento de JSON sin necesitar un backend propio.

En estos ejercicios usamos principalmente el recurso de usuarios:

https://jsonplaceholder.typicode.com/users

Los usuarios devueltos son datos de prueba: no corresponden a personas reales ni a información de producción.

15. Manejar errores con async/await

Enunciado: Capturar errores de una operación asíncrona.

async function cargarDatos() {
    try {
        const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");

        if (!respuesta.ok) {
            throw new Error("Error HTTP: " + respuesta.status);
        }

        const datos = await respuesta.json();

        console.log(datos);
    } catch (error) {
        console.log("No se pudieron cargar los datos:", error.message);
    }
}

cargarDatos();

try/catch permite capturar errores. También conviene revisar response.ok, porque fetch() no rechaza automáticamente todos los errores HTTP.

16. Consumir una API y filtrar resultados

Enunciado: Descargar usuarios y conservar sólo aquellos que cumplan una condición.

async function cargarUsuarios() {
    const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");
    const usuarios = await respuesta.json();

    const filtrados = usuarios.filter(function (usuario) {
        return usuario.name.toLowerCase().includes("a");
    });

    console.log(filtrados);
}

cargarUsuarios();

Este ejercicio combina fetch(), JSON y filter(), una secuencia muy habitual en interfaces que consumen APIs.

17. Reintentar una petición fallida

Enunciado: Crear una función que intente varias veces antes de devolver un error.

async function fetchConReintentos(url, intentos = 3) {
    let ultimoError;

    for (let i = 1; i <= intentos; i++) {
        try {
            const respuesta = await fetch(url);

            if (!respuesta.ok) {
                throw new Error("HTTP " + respuesta.status);
            }

            return await respuesta.json();
        } catch (error) {
            ultimoError = error;
        }
    }

    throw ultimoError;
}

fetchConReintentos("https://jsonplaceholder.typicode.com/users")
    .then(function (datos) {
        console.log(datos);
    })
    .catch(function (error) {
        console.log(error.message);
    });

Los reintentos pueden ser útiles ante fallos temporales de red. En sistemas reales suele agregarse una espera progresiva entre intentos.

18. Guardar objetos complejos en localStorage

Enunciado: Persistir un objeto convirtiéndolo a JSON.

const configuracion = {
    tema: "oscuro",
    idioma: "es",
    filtros: ["activos", "recientes"]
};

localStorage.setItem(
    "configuracion",
    JSON.stringify(configuracion)
);

const recuperada = JSON.parse(
    localStorage.getItem("configuracion")
);

console.log(recuperada);

localStorage guarda cadenas. Por eso usamos JSON.stringify() al guardar y JSON.parse() al recuperar.

19. Crear un sistema de eventos personalizado

Enunciado: Implementar métodos on, emit y off.

class EventBus {
    constructor() {
        this.eventos = {};
    }

    on(nombre, callback) {
        if (!this.eventos[nombre]) {
            this.eventos[nombre] = [];
        }

        this.eventos[nombre].push(callback);
    }

    emit(nombre, datos) {
        const callbacks = this.eventos[nombre] || [];

        for (const callback of callbacks) {
            callback(datos);
        }
    }

    off(nombre, callback) {
        const callbacks = this.eventos[nombre] || [];

        this.eventos[nombre] = callbacks.filter(function (item) {
            return item !== callback;
        });
    }
}

const bus = new EventBus();

function saludar(nombre) {
    console.log("Hola", nombre);
}

bus.on("saludo", saludar);
bus.emit("saludo", "Ana");
bus.off("saludo", saludar);

Este patrón desacopla componentes: quien emite un evento no necesita conocer directamente a quien lo escucha.

20. Mini proyecto: buscador de usuarios desde una API

Enunciado: Crear una búsqueda que cargue usuarios, filtre por texto y maneje estados de carga y error.

const estado = {
    usuarios: [],
    filtro: ""
};

async function cargarUsuarios() {
    try {
        console.log("Cargando...");

        const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");

        if (!respuesta.ok) {
            throw new Error("No se pudieron cargar los usuarios");
        }

        estado.usuarios = await respuesta.json();

        renderizar();
    } catch (error) {
        console.log(error.message);
    }
}

function obtenerFiltrados() {
    const texto = estado.filtro.toLowerCase();

    return estado.usuarios.filter(function (usuario) {
        return usuario.name.toLowerCase().includes(texto)
            || usuario.email.toLowerCase().includes(texto);
    });
}

function renderizar() {
    const usuarios = obtenerFiltrados();

    console.log(usuarios);
}

function buscar(texto) {
    estado.filtro = texto;
    renderizar();
}

cargarUsuarios();

El ejemplo separa estado, carga de datos, filtrado y renderizado. Esa separación facilita escalar luego hacia una interfaz DOM completa.

Qué conceptos avanzados practicamos

Estos ejercicios trabajan transformación de datos, composición de filtros, estructuras Map, funciones de orden superior, asincronía, Promises, APIs, persistencia local y patrones para controlar eventos frecuentes.

En varios casos existe más de una solución válida. El objetivo no es memorizar código, sino reconocer qué herramienta resulta apropiada para cada problema y entender si una operación modifica los datos originales o devuelve una nueva estructura.

Seguí aprendiendo JavaScript


¿Te sirvió esta guía? ☕

Si este contenido te ayudó a aprender JavaScript, podés apoyar a Club Programador. Cada aporte ayuda a mantener el sitio online y a seguir publicando material gratuito.

🇦🇷 Apoyar desde Argentina

☕ Invitame un café con Mercado Pago

🌎 Apoyar desde otro país

☕ Apoyar con PayPal


Descubre más desde Club Programador

Suscríbete y recibe las últimas entradas en tu correo electrónico.

Deja un comentario

Descubre más desde Club Programador

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo