JavaScript a toujours offert plusieurs façons d’itérer sur des données. De la boucle numérique classique for héritée de C à la
méthode forEach, les
développeurs ont eu de nombreuses options. Cependant, ES2015 a introduit un véritable changement de jeu : la boucle for...of.
Cette boucle reste encore sous-utilisée par de nombreux développeurs, alors qu’elle remplace élégamment la plupart des patterns d’itération traditionnels. Elle fonctionne de manière fluide avec les tableaux, les chaînes, les Maps, les Sets, les NodeLists et tout objet qui implémente le protocole itérable. Plus important encore, elle vous donne un contrôle total sur la quantité de l’itérable consommée, ce qui la rend idéale pour l’évaluation paresseuse et les séquences infinies.
Bref historique des boucles JavaScript
Avant de plonger dans for...of, passons rapidement
en revue les boucles que JavaScript a proposées au fil des années :
La boucle for numérique
La boucle for
traditionnelle, empruntée à C et aux langages similaires, repose sur une approche basée sur un index :
for (var index = 0, len = items.length; index < len; ++index) {
// Work with items[index]
}
Bien que puissante, cette syntaxe peut être verbeuse et source d’erreurs. Vous devez gérer l’index manuellement, mettre en cache la longueur pour les performances et penser à incrémenter le compteur.
La boucle for...in
La boucle for...in est
spécifiquement conçue pour parcourir les propriétés énumérables d’un objet :
var person = { first: 'John', last: 'Smith' };
for (var prop in person) {
console.log(prop, '=', person[prop]);
// Outputs: 'first' = 'John', 'last' = 'Smith'
}
Notez que for...in ne
sert pas à parcourir les valeurs d’un tableau, mais les propriétés d’un objet. L’utiliser sur des tableaux peut
entraîner des résultats inattendus.
Les boucles while et do...while
Ces boucles conditionnelles existent dans de nombreux langages de programmation. while évalue la condition
avant chaque itération, tandis que do...while l’évalue après,
garantissant au moins une exécution.
Qu’est-ce que for...of ?
ES2015 a formalisé un concept crucial : les itérables. Un itérable est un objet qui implémente le
protocole itérable, défini via la méthode Symbol.iterator. De nombreux
objets natifs JavaScript sont itérables : tableaux, chaînes, Maps, Sets, NodeLists, et plus encore.
La boucle for...of est
le mécanisme principal pour consommer des itérables avec un contrôle complet. Vous pouvez consommer exactement ce
dont vous avez besoin, et la quantité peut être déterminée dynamiquement ou algorithmiquement.
Lorsque vous travaillez avec des tableaux, vous vous intéressez généralement aux valeurs, et non
aux indices. La boucle for...of
rend cela naturel :
// Old way: verbose and index-focused
for (var index = 0, len = items.length; index < len; ++index) {
var item = items[index];
// Work with item
}
// Modern way: clean and value-focused
for (const item of items) {
// Work with item directly
}
Comme pour toutes les boucles, vous pouvez utiliser break, continue et return dans for...of. Mais elle est
plus polyvalente que les boucles numériques : elle fonctionne même sans indices (comme avec les Sets), et vous
n’avez pas besoin de vous soucier de mettre en cache la longueur.
Utiliser const avec for...of
Remarquez que nous avons utilisé const pour la variable de
boucle ? C’est parce que nous ne réassignons rien : nous travaillons directement avec chaque valeur à mesure
qu’elle provient de l’itérateur. Il n’y a pas d’index à incrémenter manuellement, donc const est le choix naturel
et aide à éviter les réaffectations accidentelles.
// ✅ Good: const prevents reassignment
for (const item of items) {
// item is read-only in this scope
}
// ❌ Avoid: let is unnecessary unless you need to reassign
for (let item of items) {
item = transform(item); // Only affects local variable, not the array
}
Le destructuring à la volée
Lorsque votre itérateur renvoie plusieurs valeurs (comme les entrées d’une Map), vous pouvez les déstructurer directement dans la déclaration de la boucle. Cela rend le code beaucoup plus clair :
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
// Without destructuring: verbose
for (const pair of map) {
console.log(pair[0], '=', pair[1]);
}
// With destructuring: clean and readable
for (const [key, value] of map) {
console.log(key, '=', value);
}
Ce pattern fonctionne avec tout itérable qui renvoie des tableaux ou des tuples. Vous pouvez déstructurer de manière aussi profonde que nécessaire :
const entries = [
['user', { id: 1, name: 'Alice' }],
['admin', { id: 2, name: 'Bob' }],
];
for (const [role, { id, name }] of entries) {
console.log(`${role}: ${name} (${id})`);
}
Itérateurs complémentaires
De nombreux itérables fournissent des itérateurs supplémentaires au-delà de leur itération par défaut. La plupart
en proposent au moins trois : keys(), values() et entries(). Pour les objets
sans clés (comme les Sets), les clés sont simplement les valeurs elles-mêmes.
Par exemple, si vous souhaitez parcourir un tableau tout en récupérant l’indice, vous pouvez utiliser entries() :
const fruits = ['apple', 'banana', 'cherry'];
// Get both index and value
for (const [index, fruit] of fruits.entries()) {
console.log(`${index}: ${fruit}`);
}
// Output:
// 0: apple
// 1: banana
// 2: cherry
// Or iterate over just the keys (indices)
for (const index of fruits.keys()) {
console.log(index);
}
// Output: 0, 1, 2
// Or just the values (same as default)
for (const fruit of fruits.values()) {
console.log(fruit);
}
// Output: apple, banana, cherry
Les Maps et les Sets fournissent aussi ces méthodes, ce qui facilite le travail avec leurs structures de données spécifiques :
const userMap = new Map([
['alice', { role: 'admin' }],
['bob', { role: 'user' }],
]);
// Iterate over entries (default)
for (const [username, user] of userMap) {
console.log(`${username}: ${user.role}`);
}
// Iterate over just keys
for (const username of userMap.keys()) {
console.log(username);
}
// Iterate over just values
for (const user of userMap.values()) {
console.log(user.role);
}
L’évaluation paresseuse : la vraie puissance
La véritable force de for...of réside dans sa
capacité à fonctionner avec l’évaluation paresseuse. Comme elle consomme les itérables de manière
incrémentale plutôt que d’un seul coup, elle constitue la principale façon de travailler avec des calculs
paresseux, y compris les séquences infinies.
Prenons un générateur qui produit la suite de Fibonacci :
function* fibonacci() {
let [current, next] = [1, 1];
while (true) {
yield current;
[current, next] = [next, current + next];
}
}
Cette suite ne se termine jamais. Si vous essayez de la convertir en tableau avec Array.from() ou avec
l’opérateur spread, votre programme se bloquera. En revanche, vous pouvez la consommer sans risque avec for...of :
// Get first few terms via destructuring
const [a, b, c, d, e] = fibonacci();
// a === 1, b === 1, c === 2, d === 3, e === 5
// Consume until a condition is met
for (const term of fibonacci()) {
if (term > 100) break;
console.log(term);
}
// Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
Ce pattern permet de créer toutes sortes de primitives d’évaluation paresseuse. Par exemple, voici une fonction
take qui limite le
nombre d’éléments consommés :
function* take(count, iter) {
if (count === 0) return;
for (const term of iter) {
yield term;
if (--count <= 0) break;
}
}
// Get first 10 Fibonacci numbers
for (const num of take(10, fibonacci())) {
console.log(num);
}
// Output: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
Cette approche de l’évaluation paresseuse est fondamentale pour les concepts de programmation fonctionnelle et ressemble à la façon dont des bibliothèques comme RxJS travaillent avec les observables.
Considérations de performance
Vous trouverez de nombreux benchmarks contradictoires en ligne comparant différents types de boucles. La plupart ne sont pas représentatifs des cas réels. Gardez ces deux points à l’esprit :
- Pour la plupart des tableaux (jusqu’à des millions d’éléments), la différence de performance est négligeable.
- Lorsqu’elle n’est pas transpillée (avec la prise en charge native du navigateur),
for...ofa des performances similaires aux autres boucles.
Il est extrêmement rare que vous deviez revenir à une boucle numérique for ou à while pour des raisons de
performance. Les gains en lisibilité et en flexibilité de for...of dépassent de loin
les micro-optimisations dans la grande majorité des cas.
Remplacer les anciens patterns
Vous pouvez désormais remplacer la grande majorité de vos itérations traditionnelles par for...of. Cela inclut :
- Les boucles for numériques lorsqu’il s’agit de parcourir des tableaux
- forEach() lorsque vous avez besoin de
breakoucontinue - les méthodes each() de jQuery/Lodash
- for...in lorsqu’il est mal utilisé sur des tableaux
// ❌ Old: forEach (can't break early)
items.forEach(item => {
if (item.shouldStop) return; // Only exits callback, not the loop
process(item);
});
// ✅ New: for...of (can break early)
for (const item of items) {
if (item.shouldStop) break; // Exits the entire loop
process(item);
}
// ❌ Old: jQuery each
$.each(items, function(index, item) {
// ...
});
// ✅ New: for...of
for (const item of items) {
// ...
}
Prise en charge navigateur et transpilation
La boucle for...of est
prise en charge nativement dans :
- Firefox 13+
- Chrome 38+
- Opera 25+
- Edge 12+
- Safari 7+
- Node.js 0.12+
Pour les environnements plus anciens, Babel et TypeScript peuvent transpiler for...of en code compatible.
Sur de très grands tableaux (1M+ d’éléments), le code transpillé peut avoir un certain surcoût de performance,
mais cela dépend fortement du moteur JavaScript, de votre algorithme et du contexte.
Exemples concrets
Exemple 1 : traitement de réponses API
async function processUsers() {
const response = await fetch('/api/users');
const users = await response.json();
for (const user of users) {
if (user.status === 'inactive') continue;
await updateUserProfile(user);
}
}
Exemple 2 : travailler avec des collections DOM
// NodeList is iterable
const buttons = document.querySelectorAll('.action-button');
for (const button of buttons) {
button.addEventListener('click', handleClick);
}
// Works with HTMLCollection too (after conversion)
const divs = Array.from(document.getElementsByTagName('div'));
for (const div of divs) {
div.classList.add('processed');
}
Exemple 3 : itérables personnalisés
class NumberRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const range = new NumberRange(1, 5);
for (const num of range) {
console.log(num);
}
// Output: 1, 2, 3, 4, 5
Bonnes pratiques
- Utilisez const par défaut : puisque vous ne réassignez pas la variable de boucle,
constest le choix naturel. - Déstructurez quand c’est pertinent : si votre itérable renvoie des tableaux ou des objets, déstructurez-les directement dans la déclaration de boucle.
- Utilisez entries() pour les indices : quand vous avez besoin à la fois de l’indice et de la
valeur d’un tableau, utilisez
array.entries(). - Préférez for...of à forEach : quand vous avez besoin de
breakoucontinue,for...ofest votre allié. - Exploitez l’évaluation paresseuse : utilisez des générateurs et
for...ofpour les séquences infinies et les calculs à la demande.
Conclusion
La boucle for...of est
l’une des fonctionnalités les plus élégantes et les plus puissantes de JavaScript. Elle simplifie l’itération,
fonctionne avec une grande variété de structures de données et libère la puissance de l’évaluation paresseuse
grâce aux générateurs.
Bien qu’elle ne soit pas encore aussi largement adoptée qu’elle le devrait, for...of devrait être votre
choix par défaut pour l’itération en JavaScript moderne. Elle remplace la plupart des boucles numériques, élimine
souvent le besoin de forEach et fournit une
syntaxe claire et lisible qui fonctionne avec tous les types d’itérables.
Commencez à utiliser for...of
dans votre code dès aujourd’hui. Votre futur vous-même, et vos coéquipiers, vous remercieront pour ce code plus
clair et plus maintenable.