Fonctions
Les fonctions modernes en JavaScript
JavaScript Moderne : Les Fonctions Avancées
Les fonctions sont au cœur de JavaScript et constituent l’un de ses aspects les plus puissants. Dans ce tutoriel, nous explorerons les fonctionnalités modernes des fonctions en JavaScript, notamment les fonctions fléchées, les closures, les fonctions d’ordre supérieur et les patterns fonctionnels essentiels.
JavaScript est souvent décrit comme un langage à “fonctions de première classe”, ce qui signifie que les fonctions sont traitées comme n’importe quelle autre variable. Cette caractéristique permet une grande flexibilité et des patterns de programmation puissants.
Rappel sur les bases des fonctions
Avant d’explorer les concepts avancés, rappelons les différentes façons de définir des fonctions en JavaScript.
Déclaration de fonction
function saluer(nom) {
return `Bonjour, ${nom} !`;
}
console.log(saluer("Marie")); // "Bonjour, Marie !"
Expression de fonction
const saluer = function(nom) {
return `Bonjour, ${nom} !`;
};
console.log(saluer("Pierre")); // "Bonjour, Pierre !"
Fonction fléchée (ES6)
const saluer = (nom) => {
return `Bonjour, ${nom} !`;
};
// Version concise pour les fonctions simples
const saluerConcis = nom => `Bonjour, ${nom} !`;
console.log(saluerConcis("Jean")); // "Bonjour, Jean !"
Fonctions fléchées (Arrow Functions)
Les fonctions fléchées, introduites avec ES6, apportent une syntaxe concise et des comportements spécifiques qui les rendent particulièrement utiles dans certains contextes.
Syntaxe simplifiée
// Fonction standard
const double = function(x) {
return x * 2;
};
// Fonction fléchée équivalente
const doubleArrow = (x) => {
return x * 2;
};
// Version ultra-concise (sans accolades ni return explicite)
const doubleConc = x => x * 2;
console.log(doubleConc(5)); // 10
Parenthèses et paramètres
// Sans paramètre - parenthèses obligatoires
const salut = () => "Bonjour !";
// Un seul paramètre - parenthèses optionnelles
const carre = x => x * x;
// Plusieurs paramètres - parenthèses obligatoires
const somme = (a, b) => a + b;
// Retour d'un objet - parenthèses requises autour de l'objet
const creerPersonne = (nom, age) => ({ nom, age });
console.log(creerPersonne("Alice", 30)); // { nom: "Alice", age: 30 }
Comportement de this
La principale différence fonctionnelle entre les fonctions fléchées et les fonctions classiques est leur gestion du mot-clé this
.
// Dans une fonction classique, this dépend du contexte d'appel
function FonctionClassique() {
this.valeur = 42;
setTimeout(function() {
console.log(this.valeur); // undefined (this se réfère à l'objet global)
}, 1000);
}
// Dans une fonction fléchée, this est lexical (hérité du contexte parent)
function FonctionAvecFleche() {
this.valeur = 42;
setTimeout(() => {
console.log(this.valeur); // 42 (this est celui de FonctionAvecFleche)
}, 1000);
}
Quand utiliser des fonctions fléchées
Les fonctions fléchées sont idéales pour :
- Les fonctions courtes et simples
- Les callbacks, particulièrement dans les méthodes de tableau (map, filter, etc.)
- Les fonctions anonymes
- Les cas où vous souhaitez préserver le contexte
this
Elles ne sont pas appropriées pour :
- Les méthodes d’objet (qui doivent se référer à l’objet avec
this
) - Les fonctions constructeur (pas de
new
avec les fonctions fléchées) - Les fonctions qui utilisent
arguments
(les fonctions fléchées n’ont pas d’objetarguments
)
// Exemple d'utilisation avec les méthodes de tableau
const nombres = [1, 2, 3, 4, 5];
const carres = nombres.map(x => x * x); // [1, 4, 9, 16, 25]
// Ne pas utiliser pour les méthodes d'objet
const objet = {
valeur: 42,
// Mauvaise approche
mauvaiseFonction: () => {
console.log(this.valeur); // undefined, this est l'objet global
},
// Bonne approche
bonneFonction() {
console.log(this.valeur); // 42
}
};
Paramètres de fonction avancés
ES6 et les versions suivantes ont introduit plusieurs fonctionnalités pour gérer les paramètres de manière plus flexible et expressive.
Paramètres par défaut
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 !"
// Expressions comme valeurs par défaut
function getDateFormatee(date = new Date()) {
return date.toLocaleDateString();
}
// Utiliser des paramètres précédents comme valeurs par défaut
function calculer(a = 1, b = a * 2) {
return a + b;
}
console.log(calculer()); // 3 (1 + 2)
Paramètres du reste (Rest Parameters)
function somme(...nombres) {
return nombres.reduce((total, n) => total + n, 0);
}
console.log(somme(1, 2, 3, 4, 5)); // 15
// Combinaison avec des paramètres normaux
function formatMessage(prefix, suffix, ...messages) {
return messages.map(msg => `${prefix}${msg}${suffix}`);
}
console.log(formatMessage("<<", ">>", "Info", "Erreur", "Avertissement"));
// ["<<Info>>", "<<Erreur>>", "<<Avertissement>>"]
Déstructuration de paramètres
// Déstructuration d'objet dans les paramètres
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"
// Déstructuration de tableau
function afficherCoordonnees([x, y, z = 0]) {
console.log(`X: ${x}, Y: ${y}, Z: ${z}`);
}
afficherCoordonnees([10, 20]);
// "X: 10, Y: 20, Z: 0"
Closures (Fermetures)
Les closures sont l’un des concepts les plus puissants en JavaScript, permettant à une fonction de “se souvenir” de son environnement lexical même après que sa fonction parente ait terminé son exécution.
Comprendre les closures
function creerCompteur() {
let compte = 0; // Variable privée
return function() {
compte++; // Accès à la variable du scope parent
return compte;
};
}
const compteur = creerCompteur();
console.log(compteur()); // 1
console.log(compteur()); // 2
console.log(compteur()); // 3
// La variable compte est "encapsulée" dans la closure
// et inaccessible de l'extérieur
Applications pratiques des closures
Encapsulation de données
function creerBanque(soldeInitial) {
let solde = soldeInitial; // Variable privée
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 est inaccessible directement: console.log(monCompte.solde) // undefined
Création de fonctions personnalisées
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
Gestion de contexte asynchrone
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');
// Plus tard dans le code
getUserData(data => {
console.log("Données reçues:", data);
});
Le problème classique de boucle et closure
Un problème courant avec les closures survient lors de l’utilisation de variables de boucle dans des fonctions asynchrones :
// Problème
function probleme() {
const fonctions = [];
for (var i = 0; i < 3; i++) {
fonctions.push(function() {
console.log(i);
});
}
// À ce stade, i est devenu 3
return fonctions;
}
const fns = probleme();
fns[0](); // 3 (et non 0 comme on pourrait s'y attendre)
fns[1](); // 3
fns[2](); // 3
// Solution avec let (portée de bloc)
function solution() {
const fonctions = [];
for (let i = 0; i < 3; i++) {
// Chaque itération crée un nouveau i
fonctions.push(function() {
console.log(i);
});
}
return fonctions;
}
const fnsSolution = solution();
fnsSolution[0](); // 0
fnsSolution[1](); // 1
fnsSolution[2](); // 2
// Solution avec closure explicite (pour 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;
}
Fonctions d’ordre supérieur
Les fonctions d’ordre supérieur sont des fonctions qui prennent d’autres fonctions comme arguments ou qui renvoient des fonctions. Elles sont fondamentales pour la programmation fonctionnelle en JavaScript.
Fonctions qui acceptent des callbacks
// Fonction d'ordre supérieur qui prend une fonction en paramètre
function appliquerOperation(nombre, operation) {
return operation(nombre);
}
// Fonctions qui peuvent être utilisées comme 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
Fonctions qui retournent des fonctions
// Currying - transformer une fonction avec plusieurs arguments
// en une séquence de fonctions à un seul argument
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
// Composition de fonctions
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
Exemples de fonctions d’ordre supérieur intégrées
JavaScript possède plusieurs méthodes d’ordre supérieur intégrées pour les tableaux :
const nombres = [1, 2, 3, 4, 5, 6];
// map - transforme chaque élément
const doubles = nombres.map(n => n * 2); // [2, 4, 6, 8, 10, 12]
// filter - sélectionne certains éléments
const pairs = nombres.filter(n => n % 2 === 0); // [2, 4, 6]
// reduce - accumule les valeurs
const somme = nombres.reduce((acc, n) => acc + n, 0); // 21
// find - trouve le premier élément qui satisfait une condition
const premierPair = nombres.find(n => n % 2 === 0); // 2
// every - vérifie si tous les éléments satisfont une condition
const tousSontPositifs = nombres.every(n => n > 0); // true
// some - vérifie si au moins un élément satisfait une condition
const auMoinsUnGrand = nombres.some(n => n > 4); // true
Patterns fonctionnels avancés
JavaScript permet d’implémenter de nombreux patterns issus de la programmation fonctionnelle.
Fonction pure
Une fonction pure n’a pas d’effets secondaires et retourne toujours le même résultat pour les mêmes entrées :
// Fonction pure
function ajouter(a, b) {
return a + b;
}
// Fonction impure (effet secondaire)
let total = 0;
function ajouterAuTotal(valeur) {
total += valeur; // Modifie un état externe
return total;
}
Immutabilité
L’immutabilité consiste à ne jamais modifier les données, mais à créer de nouvelles versions :
// Approche immutable
const array1 = [1, 2, 3];
const array2 = [...array1, 4]; // Crée un nouveau tableau
// array1 reste [1, 2, 3]
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // Nouvel objet
// obj1 reste { a: 1, b: 2 }
Memoization
La mémoïsation est une technique d’optimisation qui consiste à mettre en cache les résultats des appels de fonction :
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;
};
}
// Fonction récursive coûteuse
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Version optimisée avec 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)); // Très lent
console.timeEnd("sans memo");
console.time("avec memo");
console.log(fiboMemo(35)); // Beaucoup plus rapide
console.timeEnd("avec memo");
Contexte d’exécution et this
La gestion du contexte this
est cruciale en JavaScript et source de nombreuses confusions.
Contrôle du contexte this
JavaScript fournit plusieurs méthodes pour contrôler le contexte d’exécution :
const personne = {
nom: "Alice",
saluer() {
return `Bonjour, je suis ${this.nom}`;
}
};
// Approche normale
console.log(personne.saluer()); // "Bonjour, je suis Alice"
// Perte de contexte
const fonctionSaluer = personne.saluer;
console.log(fonctionSaluer()); // "Bonjour, je suis undefined"
// Solution 1: bind
const fonctionLiee = personne.saluer.bind(personne);
console.log(fonctionLiee()); // "Bonjour, je suis Alice"
// Solution 2: call
console.log(personne.saluer.call({ nom: "Bob" })); // "Bonjour, je suis Bob"
// Solution 3: apply (comme call mais prend un tableau d'arguments)
console.log(personne.saluer.apply({ nom: "Charlie" })); // "Bonjour, je suis Charlie"
Différences entre call, apply et bind
function presenter(prefix, suffix) {
return `${prefix} ${this.nom} ${suffix}`;
}
const user = { nom: "Alice" };
// call: invoque immédiatement avec des arguments séparés
console.log(presenter.call(user, "Bonjour", "!")); // "Bonjour Alice !"
// apply: invoque immédiatement avec des arguments dans un tableau
console.log(presenter.apply(user, ["Salut", "..."])); // "Salut Alice ..."
// bind: crée une nouvelle fonction liée au contexte, sans l'invoquer
const presenterUtilisateur = presenter.bind(user, "Bienvenue");
console.log(presenterUtilisateur("!")); // "Bienvenue Alice !"
Fonctions autogénérées et IIFE
IIFE (Immediately Invoked Function Expression)
Les IIFE sont des fonctions qui s’exécutent immédiatement après leur définition :
// Syntaxe classique
(function() {
const message = "Je m'exécute immédiatement";
console.log(message);
})();
// Avec des paramètres
(function(nom) {
console.log(`Bonjour, ${nom} !`);
})("Alice");
// Avec des fonctions fléchées
(() => {
console.log("IIFE avec flèche");
})();
Utilités des IIFE
- Isoler les variables : Éviter de polluer le scope global
- Encapsuler des données : Créer des modules simples
- Exécuter du code immédiatement : Initialiser des applications
- Créer des closures : Capturer des valeurs de l’environnement
// Exemple de module avec IIFE
const compteur = (function() {
let compte = 0; // Variable privée
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
Fonctions génératrices et async/await
Les fonctions génératrices et async/await sont des mécanismes puissants pour gérer des flux d’exécution complexes.
Fonctions génératrices
Les fonctions génératrices peuvent suspendre et reprendre leur exécution :
function* compterJusqua(n) {
for (let i = 1; i <= n; i++) {
yield i; // suspend l'exécution et renvoie la valeur
}
}
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
// Itération avec for...of
for (const valeur of compterJusqua(3)) {
console.log(valeur); // 1, 2, 3
}
Fonctions async et await
Les fonctions async/await simplifient la gestion du code asynchrone :
// Fonction qui retourne une Promise
function attendre(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Fonction asynchrone utilisant 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é";
}
// Utilisation
processAsync().then(resultat => {
console.log(resultat); // "Terminé" après 2 secondes
});
// Gestion d'erreurs
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; // rethrow pour traitement ultérieur
}
}
Exercices pratiques
Pour solidifier vos connaissances, essayez ces exercices :
-
Filtrage avancé : Écrivez une fonction d’ordre supérieur
filtrerPar
qui prend un critère (une fonction) et retourne une fonction qui filtre un tableau selon ce critère. -
Debounce : Créez une fonction
debounce
qui limite la fréquence d’exécution d’une fonction. Utile pour les gestionnaires d’événements comme la recherche en temps réel. -
Pipeline : Implémentez une fonction
pipeline
qui prend plusieurs fonctions et les exécute en séquence, en passant le résultat de chaque fonction à la suivante. -
Memoization personnalisée : Améliorez la fonction
memoize
pour prendre en compte une fonction de génération de clé personnalisée. -
Gestionnaire de promesses : Créez une fonction qui prend un tableau de promesses et retourne une nouvelle promesse qui se résout avec un tableau de résultats dans le même ordre.
Exemple complet : Analyseur de données
Voici un exemple qui met en pratique plusieurs concepts que nous avons vus :
// Simuler un appel 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] }
]);
// Fonctions pures pour les opérations
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";
};
// Fonction d'ordre supérieur pour filtrer
const filtrerPar = critere => tableau =>
tableau.filter(critere);
// Closure pour garder un cache des résultats
function createAnalyseur() {
const cache = new Map();
return {
async analyserEtudiants(filtre = null) {
// Vérifier le cache
const cacheKey = filtre ? filtre.toString() : "tous";
if (cache.has(cacheKey)) {
console.log("Utilisation du cache pour", cacheKey);
return cache.get(cacheKey);
}
// Récupérer les données
const etudiants = await fetchData();
// Appliquer le filtre si nécessaire
const donneesFiltrees = filtre
? filtrerPar(filtre)(etudiants)
: etudiants;
// Transformer les données
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)
};
});
// Sauvegarder dans le cache et retourner
cache.set(cacheKey, resultat);
return resultat;
},
// Effacer le cache
clearCache() {
cache.clear();
}
};
}
// Utilisation
async function demoAnalyse() {
const analyseur = createAnalyseur();
// Analyse de tous les étudiants
console.log("Tous les étudiants:");
const tous = await analyseur.analyserEtudiants();
console.table(tous);
// Filtre par performance
console.log("\nÉtudiants excellents:");
const excellents = await analyseur.analyserEtudiants(
etudiant => calculerMoyenne(etudiant.scores) >= 90
);
console.table(excellents);
// Filtre par âge
console.log("\nÉtudiants de moins de 25 ans:");
const jeunes = await analyseur.analyserEtudiants(
etudiant => etudiant.age < 25
);
console.table(jeunes);
// Démonstration du cache
console.log("\nSeconde demande (depuis le cache):");
const deuxiemeAppel = await analyseur.analyserEtudiants();
console.table(deuxiemeAppel);
}
// Exécution de la démo
demoAnalyse().catch(error => console.error(error));
Conclusion
Dans ce tutoriel, nous avons exploré les fonctionnalités avancées des fonctions en JavaScript moderne :
- Les différentes façons de définir des fonctions, notamment les fonctions fléchées
- Les closures et leur capacité à encapsuler des données
- Les fonctions d’ordre supérieur qui manipulent d’autres fonctions
- Les patterns fonctionnels comme l’immutabilité et la mémoïsation
- La gestion du contexte avec
this
et les méthodesbind
,call
etapply
- Les IIFE et la création de modules
- Les fonctions génératrices et async/await pour le code asynchrone
Les fonctions sont véritablement au cœur de JavaScript, et leur maîtrise est essentielle pour écrire du code moderne, efficace et maintenable. En adoptant une approche fonctionnelle, vous pourrez créer des applications plus robustes, testables et extensibles.
Dans le prochain tutoriel, nous explorerons le destructuring et le spread operator.
Besoin de revoir les variables ? Consultez notre tutoriel précédent sur les variables.
Prêt à continuer ? Passez au prochain tutoriel sur le destructuring.
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion