0%
JavaScript moderno: las funciones

Funciones

Las funciones modernas en JavaScript

10-15 min

JavaScript moderno: las funciones avanzadas

Las funciones están en el corazón de JavaScript y constituyen uno de sus aspectos más potentes. En este tutorial exploraremos las funcionalidades modernas de las funciones en JavaScript, en particular las funciones flecha, las closures, las funciones de orden superior y los patrones funcionales esenciales.

JavaScript se describe a menudo como un lenguaje con «funciones de primera clase», lo que significa que las funciones se tratan como cualquier otra variable. Esta característica permite una gran flexibilidad y patrones de programación muy potentes.

Repaso de los conceptos básicos de las funciones

Antes de explorar los conceptos avanzados, repasemos las distintas formas de definir funciones en JavaScript.

Declaración de función

function saluer(nom) {
    return `Bonjour, ${nom} !`;
}

console.log(saluer("Marie")); // "Bonjour, Marie !"

Expresión de función

const saluer = function(nom) {
    return `Bonjour, ${nom} !`;
};

console.log(saluer("Pierre")); // "Bonjour, Pierre !"

Función flecha (ES6)

const saluer = (nom) => {
    return `Bonjour, ${nom} !`;
};

// Versión concisa para las funciones simples
const saluerConcis = nom => `Bonjour, ${nom} !`;

console.log(saluerConcis("Jean")); // "Bonjour, Jean !"

Funciones flecha (Arrow Functions)

Las funciones flecha, introducidas con ES6, aportan una sintaxis concisa y comportamientos específicos que las hacen especialmente útiles en determinados contextos.

Sintaxis simplificada

// Función estándar
const double = function(x) {
    return x * 2;
};

// Función flecha equivalente
const doubleArrow = (x) => {
    return x * 2;
};

// Versión ultraconcisa (sin llaves ni return explícito)
const doubleConc = x => x * 2;

console.log(doubleConc(5)); // 10

Paréntesis y parámetros

// Sin parámetros: paréntesis obligatorios
const salut = () => "Bonjour !";

// Un solo parámetro: paréntesis opcionales
const carre = x => x * x;

// Varios parámetros: paréntesis obligatorios
const somme = (a, b) => a + b;

// Devolver un objeto: paréntesis necesarios alrededor del objeto
const creerPersonne = (nom, age) => ({ nom, age });
console.log(creerPersonne("Alice", 30)); // { nom: "Alice", age: 30 }

Comportamiento de this

La principal diferencia funcional entre las funciones flecha y las funciones clásicas es cómo gestionan la palabra clave this.

// En una función clásica, this depende del contexto de llamada
function FonctionClassique() {
    this.valeur = 42;
    setTimeout(function() {
        console.log(this.valeur); // undefined (this hace referencia al objeto global)
    }, 1000);
}

// En una función flecha, this es léxico (heredado del contexto padre)
function FonctionAvecFleche() {
    this.valeur = 42;
    setTimeout(() => {
        console.log(this.valeur); // 42 (this es el de FonctionAvecFleche)
    }, 1000);
}

Cuándo usar funciones flecha

Las funciones flecha son ideales para:

  • Funciones cortas y simples
  • Callbacks, especialmente en los métodos de array (map, filter, etc.)
  • Funciones anónimas
  • Los casos en los que quieras conservar el contexto this

No son adecuadas para:

  • Los métodos de objeto (que deben hacer referencia al objeto con this)
  • Las funciones constructoras (no se puede usar new con funciones flecha)
  • Las funciones que utilizan arguments (las funciones flecha no tienen objeto arguments)
// Ejemplo de uso con los métodos de array
const nombres = [1, 2, 3, 4, 5];
const carres = nombres.map(x => x * x); // [1, 4, 9, 16, 25]

// No usar para los métodos de objeto
const objet = {
    valeur: 42,
    // Mal enfoque
    mauvaiseFonction: () => {
        console.log(this.valeur); // undefined, this es el objeto global
    },
    // Buen enfoque
    bonneFonction() {
        console.log(this.valeur); // 42
    }
};

Parámetros de función avanzados

ES6 y las versiones posteriores introdujeron varias funcionalidades para gestionar los parámetros de forma más flexible y expresiva.

Parámetros por defecto

function saluer(nom = "Utilisateur", message = "Bonjour") {
    return `${message}, ${nom} !`;
}

console.log(saluer()); // "Bonjour, Utilisateur !"
console.log(saluer("Marie")); // "Bonjour, Marie !"
console.log(saluer("Pierre", "Bienvenue")); // "Bienvenue, Pierre !"

// Expresiones como valores por defecto
function getDateFormatee(date = new Date()) {
    return date.toLocaleDateString();
}

// Usar parámetros anteriores como valores por defecto
function calculer(a = 1, b = a * 2) {
    return a + b;
}
console.log(calculer()); // 3 (1 + 2)

Parámetros rest (Rest Parameters)

function somme(...nombres) {
    return nombres.reduce((total, n) => total + n, 0);
}

console.log(somme(1, 2, 3, 4, 5)); // 15

// Combinación con parámetros normales
function formatMessage(prefix, suffix, ...messages) {
    return messages.map(msg => `${prefix}${msg}${suffix}`);
}

console.log(formatMessage("<<", ">>", "Info", "Erreur", "Avertissement"));
// ["<<Info>>", "<<Erreur>>", "<<Avertissement>>"]

Desestructuración de parámetros

// Desestructuración de objeto en los parámetros
function afficherUtilisateur({ nom, age, email = "Non fourni" }) {
    console.log(`Nom: ${nom}, Age: ${age}, Email: ${email}`);
}

afficherUtilisateur({ nom: "Alice", age: 30 });
// "Nom: Alice, Age: 30, Email: Non fourni"

// Desestructuración de array
function afficherCoordonnees([x, y, z = 0]) {
    console.log(`X: ${x}, Y: ${y}, Z: ${z}`);
}

afficherCoordonnees([10, 20]);
// "X: 10, Y: 20, Z: 0"

Closures (clausuras)

Las closures son uno de los conceptos más potentes de JavaScript: permiten que una función «recuerde» su entorno léxico incluso después de que su función padre haya terminado de ejecutarse.

Comprender las closures

function creerCompteur() {
    let compte = 0; // Variable privada
    
    return function() {
        compte++; // Acceso a la variable del scope padre
        return compte;
    };
}

const compteur = creerCompteur();
console.log(compteur()); // 1
console.log(compteur()); // 2
console.log(compteur()); // 3

// La variable compte está "encapsulada" en la closure
// y es inaccesible desde el exterior

Aplicaciones prácticas de las closures

Encapsulación de datos

function creerBanque(soldeInitial) {
    let solde = soldeInitial; // Variable privada
    
    return {
        deposer(montant) {
            solde += montant;
            return solde;
        },
        retirer(montant) {
            if (montant <= solde) {
                solde -= montant;
                return true;
            }
            return false;
        },
        getSolde() {
            return solde;
        }
    };
}

const monCompte = creerBanque(100);
console.log(monCompte.getSolde()); // 100
monCompte.deposer(50);
console.log(monCompte.getSolde()); // 150
monCompte.retirer(30);
console.log(monCompte.getSolde()); // 120
// solde no es accesible directamente: console.log(monCompte.solde) // undefined

Creación de funciones personalizadas

function multiplierPar(facteur) {
    return function(nombre) {
        return nombre * facteur;
    };
}

const double = multiplierPar(2);
const triple = multiplierPar(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Gestión de contexto asíncrono

function fetchData(url) {
    return function(callback) {
        fetch(url)
            .then(response => response.json())
            .then(data => callback(data))
            .catch(error => console.error(error));
    };
}

const getUserData = fetchData('https://api.example.com/users');
// Más adelante en el código
getUserData(data => {
    console.log("Données reçues:", data);
});

El problema clásico del bucle y la closure

Un problema habitual con las closures aparece al utilizar variables de bucle dentro de funciones asíncronas:

// Problema
function probleme() {
    const fonctions = [];
    
    for (var i = 0; i < 3; i++) {
        fonctions.push(function() {
            console.log(i);
        });
    }
    
    // En este punto, i vale 3
    return fonctions;
}

const fns = probleme();
fns[0](); // 3 (y no 0, como cabría esperar)
fns[1](); // 3
fns[2](); // 3

// Solución con let (ámbito de bloque)
function solution() {
    const fonctions = [];
    
    for (let i = 0; i < 3; i++) {
        // Cada iteración crea un nuevo i
        fonctions.push(function() {
            console.log(i);
        });
    }
    
    return fonctions;
}

const fnsSolution = solution();
fnsSolution[0](); // 0
fnsSolution[1](); // 1
fnsSolution[2](); // 2

// Solución con closure explícita (para var)
function solutionClassique() {
    const fonctions = [];
    
    for (var i = 0; i < 3; i++) {
        // IIFE (Immediately Invoked Function Expression)
        (function(index) {
            fonctions.push(function() {
                console.log(index);
            });
        })(i);
    }
    
    return fonctions;
}

Funciones de orden superior

Las funciones de orden superior son funciones que reciben otras funciones como argumentos o que devuelven funciones. Son fundamentales para la programación funcional en JavaScript.

Funciones que aceptan callbacks

// Función de orden superior que recibe una función como parámetro
function appliquerOperation(nombre, operation) {
    return operation(nombre);
}

// Funciones que pueden usarse como callbacks
const double = x => x * 2;
const carre = x => x * x;
const absolu = x => Math.abs(x);

console.log(appliquerOperation(5, double)); // 10
console.log(appliquerOperation(-3, absolu)); // 3
console.log(appliquerOperation(4, carre)); // 16

Funciones que devuelven funciones

// Currying: transformar una función con varios argumentos
// en una secuencia de funciones de un solo argumento
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args);
        } else {
            return function(...args2) {
                return curried(...args, ...args2);
            };
        }
    };
}

function ajouter(a, b, c) {
    return a + b + c;
}

const ajouterCurry = curry(ajouter);
console.log(ajouterCurry(1)(2)(3)); // 6
console.log(ajouterCurry(1, 2)(3)); // 6
console.log(ajouterCurry(1)(2, 3)); // 6

// Composición de funciones
const compose = (f, g) => x => f(g(x));

const ajouterUn = x => x + 1;
const multiplierParDeux = x => x * 2;

const ajouterPuisMultiplier = compose(multiplierParDeux, ajouterUn);
console.log(ajouterPuisMultiplier(5)); // (5 + 1) * 2 = 12

Ejemplos de funciones de orden superior integradas

JavaScript incluye varios métodos de orden superior para los arrays:

const nombres = [1, 2, 3, 4, 5, 6];

// map: transforma cada elemento
const doubles = nombres.map(n => n * 2); // [2, 4, 6, 8, 10, 12]

// filter: selecciona determinados elementos
const pairs = nombres.filter(n => n % 2 === 0); // [2, 4, 6]

// reduce: acumula los valores
const somme = nombres.reduce((acc, n) => acc + n, 0); // 21

// find: encuentra el primer elemento que cumple una condición
const premierPair = nombres.find(n => n % 2 === 0); // 2

// every: comprueba si todos los elementos cumplen una condición
const tousSontPositifs = nombres.every(n => n > 0); // true

// some: comprueba si al menos un elemento cumple una condición
const auMoinsUnGrand = nombres.some(n => n > 4); // true

Patrones funcionales avanzados

JavaScript permite implementar numerosos patrones procedentes de la programación funcional.

Función pura

Una función pura no tiene efectos secundarios y siempre devuelve el mismo resultado para las mismas entradas:

// Función pura
function ajouter(a, b) {
    return a + b;
}

// Función impura (efecto secundario)
let total = 0;
function ajouterAuTotal(valeur) {
    total += valeur; // Modifica un estado externo
    return total;
}

Inmutabilidad

La inmutabilidad consiste en no modificar nunca los datos, sino crear nuevas versiones de ellos:

// Enfoque inmutable
const array1 = [1, 2, 3];
const array2 = [...array1, 4]; // Crea un nuevo array
// array1 sigue siendo [1, 2, 3]

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // Nuevo objeto
// obj1 sigue siendo { a: 1, b: 2 }

Memoización

La memoización es una técnica de optimización que consiste en almacenar en caché los resultados de las llamadas a una función:

function memoize(fn) {
    const cache = new Map();
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            return cache.get(key);
        }
        
        const result = fn(...args);
        cache.set(key, result);
        return result;
    };
}

// Función recursiva costosa
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Versión optimizada con memoization
const fiboMemo = memoize(function(n) {
    if (n <= 1) return n;
    return fiboMemo(n - 1) + fiboMemo(n - 2);
});

console.time("sans memo");
console.log(fibonacci(35)); // Muy lento
console.timeEnd("sans memo");

console.time("avec memo");
console.log(fiboMemo(35)); // Mucho más rápido
console.timeEnd("avec memo");

Contexto de ejecución y this

La gestión del contexto this es crucial en JavaScript y fuente de numerosas confusiones.

Control del contexto this

JavaScript ofrece varios métodos para controlar el contexto de ejecución:

const personne = {
    nom: "Alice",
    saluer() {
        return `Bonjour, je suis ${this.nom}`;
    }
};

// Enfoque normal
console.log(personne.saluer()); // "Bonjour, je suis Alice"

// Pérdida de contexto
const fonctionSaluer = personne.saluer;
console.log(fonctionSaluer()); // "Bonjour, je suis undefined"

// Solución 1: bind
const fonctionLiee = personne.saluer.bind(personne);
console.log(fonctionLiee()); // "Bonjour, je suis Alice"

// Solución 2: call
console.log(personne.saluer.call({ nom: "Bob" })); // "Bonjour, je suis Bob"

// Solución 3: apply (como call, pero recibe un array de argumentos)
console.log(personne.saluer.apply({ nom: "Charlie" })); // "Bonjour, je suis Charlie"

Diferencias entre call, apply y bind

function presenter(prefix, suffix) {
    return `${prefix} ${this.nom} ${suffix}`;
}

const user = { nom: "Alice" };

// call: invoca inmediatamente con argumentos separados
console.log(presenter.call(user, "Bonjour", "!")); // "Bonjour Alice !"

// apply: invoca inmediatamente con los argumentos en un array
console.log(presenter.apply(user, ["Salut", "..."])); // "Salut Alice ..."

// bind: crea una nueva función ligada al contexto, sin invocarla
const presenterUtilisateur = presenter.bind(user, "Bienvenue");
console.log(presenterUtilisateur("!")); // "Bienvenue Alice !"

Funciones autogeneradas e IIFE

IIFE (Immediately Invoked Function Expression)

Las IIFE son funciones que se ejecutan inmediatamente después de su definición:

// Sintaxis clásica
(function() {
    const message = "Je m'exécute immédiatement";
    console.log(message);
})();

// Con parámetros
(function(nom) {
    console.log(`Bonjour, ${nom} !`);
})("Alice");

// Con funciones flecha
(() => {
    console.log("IIFE avec flèche");
})();

Utilidades de las IIFE

  1. Aislar variables: evitar contaminar el scope global
  2. Encapsular datos: crear módulos sencillos
  3. Ejecutar código inmediatamente: inicializar aplicaciones
  4. Crear closures: capturar valores del entorno
// Ejemplo de módulo con IIFE
const compteur = (function() {
    let compte = 0; // Variable privada
    
    return {
        incrementer() {
            return ++compte;
        },
        decrementer() {
            return --compte;
        },
        valeur() {
            return compte;
        }
    };
})();

console.log(compteur.incrementer()); // 1
console.log(compteur.incrementer()); // 2
console.log(compteur.decrementer()); // 1

Funciones generadoras y async/await

Las funciones generadoras y async/await son mecanismos potentes para gestionar flujos de ejecución complejos.

Funciones generadoras

Las funciones generadoras pueden suspender y reanudar su ejecución:

function* compterJusqua(n) {
    for (let i = 1; i <= n; i++) {
        yield i; // suspende la ejecución y devuelve el valor
    }
}

const compteur = compterJusqua(5);
console.log(compteur.next().value); // 1
console.log(compteur.next().value); // 2
console.log(compteur.next().value); // 3
console.log(compteur.next().value); // 4
console.log(compteur.next().value); // 5
console.log(compteur.next().value); // undefined

// Iteración con for...of
for (const valeur of compterJusqua(3)) {
    console.log(valeur); // 1, 2, 3
}

Funciones async y await

Las funciones async/await simplifican la gestión del código asíncrono:

// Función que devuelve una Promise
function attendre(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// Función asíncrona que usa await
async function processAsync() {
    console.log("Début");
    
    await attendre(1000);
    console.log("Après 1 seconde");
    
    await attendre(1000);
    console.log("Après 2 secondes");
    
    return "Terminé";
}

// Uso
processAsync().then(resultat => {
    console.log(resultat); // "Terminé" tras 2 segundos
});

// Gestión de errores
async function fetchData(url) {
    try {
        const response = await fetch(url);
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Erreur lors de la récupération:", error);
        throw error; // se relanza para un tratamiento posterior
    }
}

Ejercicios prácticos

Para consolidar tus conocimientos, prueba estos ejercicios:

  1. Filtrado avanzado: escribe una función de orden superior filtrerPar que reciba un criterio (una función) y devuelva una función que filtre un array según ese criterio.

  2. Debounce: crea una función debounce que limite la frecuencia de ejecución de una función. Resulta útil para los gestores de eventos, como la búsqueda en tiempo real.

  3. Pipeline: implementa una función pipeline que reciba varias funciones y las ejecute en secuencia, pasando el resultado de cada función a la siguiente.

  4. Memoización personalizada: mejora la función memoize para que tenga en cuenta una función de generación de claves personalizada.

  5. Gestor de promesas: crea una función que reciba un array de promesas y devuelva una nueva promesa que se resuelva con un array de resultados en el mismo orden.

Ejemplo completo: analizador de datos

A continuación, un ejemplo que pone en práctica varios de los conceptos que hemos visto:

// Simular una llamada a una API
const fetchData = () => Promise.resolve([
    { id: 1, nom: "Alice", age: 25, scores: [85, 90, 78] },
    { id: 2, nom: "Bob", age: 30, scores: [92, 85, 90] },
    { id: 3, nom: "Charlie", age: 22, scores: [70, 65, 80] },
    { id: 4, nom: "David", age: 28, scores: [95, 88, 92] }
]);

// Funciones puras para las operaciones
const calculerMoyenne = scores => 
    scores.reduce((total, score) => total + score, 0) / scores.length;

const evaluerPerformance = moyenne => {
    if (moyenne >= 90) return "Excellent";
    if (moyenne >= 80) return "Très bien";
    if (moyenne >= 70) return "Bien";
    return "Passable";
};

// Función de orden superior para filtrar
const filtrerPar = critere => tableau => 
    tableau.filter(critere);

// Closure para mantener una caché de los resultados
function createAnalyseur() {
    const cache = new Map();
    
    return {
        async analyserEtudiants(filtre = null) {
            // Comprobar la caché
            const cacheKey = filtre ? filtre.toString() : "tous";
            if (cache.has(cacheKey)) {
                console.log("Utilisation du cache pour", cacheKey);
                return cache.get(cacheKey);
            }
            
            // Obtener los datos
            const etudiants = await fetchData();
            
            // Aplicar el filtro si es necesario
            const donneesFiltrees = filtre 
                ? filtrerPar(filtre)(etudiants) 
                : etudiants;
            
            // Transformar los datos
            const resultat = donneesFiltrees.map(etudiant => {
                const moyenne = calculerMoyenne(etudiant.scores);
                return {
                    id: etudiant.id,
                    nom: etudiant.nom,
                    age: etudiant.age,
                    moyenne: parseFloat(moyenne.toFixed(2)),
                    performance: evaluerPerformance(moyenne)
                };
            });
            
            // Guardar en la caché y devolver
            cache.set(cacheKey, resultat);
            return resultat;
        },
        
        // Borrar la caché
        clearCache() {
            cache.clear();
        }
    };
}

// Uso
async function demoAnalyse() {
    const analyseur = createAnalyseur();
    
    // Análisis de todos los estudiantes
    console.log("Tous les étudiants:");
    const tous = await analyseur.analyserEtudiants();
    console.table(tous);
    
    // Filtro por rendimiento
    console.log("\nÉtudiants excellents:");
    const excellents = await analyseur.analyserEtudiants(
        etudiant => calculerMoyenne(etudiant.scores) >= 90
    );
    console.table(excellents);
    
    // Filtro por edad
    console.log("\nÉtudiants de moins de 25 ans:");
    const jeunes = await analyseur.analyserEtudiants(
        etudiant => etudiant.age < 25
    );
    console.table(jeunes);
    
    // Demostración de la caché
    console.log("\nSeconde demande (depuis le cache):");
    const deuxiemeAppel = await analyseur.analyserEtudiants();
    console.table(deuxiemeAppel);
}

// Ejecución de la demo
demoAnalyse().catch(error => console.error(error));

Conclusión

En este tutorial hemos explorado las funcionalidades avanzadas de las funciones en JavaScript moderno:

  • Las diferentes formas de definir funciones, en particular las funciones flecha
  • Las closures y su capacidad para encapsular datos
  • Las funciones de orden superior, que manipulan otras funciones
  • Los patrones funcionales como la inmutabilidad y la memoización
  • La gestión del contexto con this y los métodos bind, call y apply
  • Las IIFE y la creación de módulos
  • Las funciones generadoras y async/await para el código asíncrono

Las funciones están verdaderamente en el corazón de JavaScript, y dominarlas es esencial para escribir código moderno, eficiente y mantenible. Si adoptas un enfoque funcional, podrás crear aplicaciones más robustas, testables y extensibles.

En el próximo tutorial exploraremos el destructuring y el spread operator.


¿Necesitas repasar las variables? Consulta nuestro tutorial anterior sobre las variables.

¿Listo para continuar? Pasa al próximo tutorial sobre el destructuring.

Comentarios

Los comentarios funcionan con GitHub Discussions

Inicia sesión con GitHub para participar en la discusión

¡Enlace copiado!