La boucle for-of : s’il ne devait en rester qu’une…
Par Christophe Porteneuve • Publié le 20 mai 2020 • 4 min

This page is also available in English.

Bienvenue dans le dix-septième article de notre série quotidienne « 19 pépites de JS pur ». On va parler aujourd’hui d’une de mes fonctionnalités préférées du langage : la boucle forof. Encore largement sous-exploitée voire méconnue, celle-ci remplace avantageusement la majorité des for numériques, des forEach, et offre encore de nombreux avantages supplémentaires.

Dans la série…

Extrait de la liste des articles :

  1. Object spread vs. Object.assign
  2. Convertir un objet en Map et réciproquement
  3. La boucle for-of : s’il ne devait en rester qu’une… (cet article)
  4. Simuler une classe abstraite avec new.target
  5. Des indices de tableaux négatifs grâce aux proxies

Our story so far…

Historiquement, JavaScript a 4 boucles :

  1. Le for numérique, hérité du C et très répandu dans d’autres langages. Sa syntaxe parfaitement imbitable pour les débutants, mais bon, elle est ce qu’elle est…
for (var index = 0, len = items.length; index < len; ++index) {
// …
}
  1. Le forin, qui n’a rien à voir avec les trucs similaires dans d’autres langages. Elle sert spécifiquement à itérer sur les propriétés énumérables de l’objet, qu’elles soient héritées ou non :
var person = { first: 'John', last: 'Smith' }
for (var prop in person) {
// Successivement 'first' et 'last' (sauf si un !@# a pourri
// `Object.prototype`…)
console.log(prop, '=', person[prop])
}
  1. Le while, présent dans énormément de langages historiques. Au lieu de suivre une séquence, il utilise juste une condition de bascule, évaluée en amont (on peut donc ne jamais entrer dans la boucle) :
while (cond) {
// …
}
  1. Le dowhile, similaire mais qui évalue la condition à la fin : on a donc au moins un tour de boucle.
do {
// …
} while (cond)

À part le forin, on retrouve les 3 autres dans un très grand nombre de langages de programmation.

Que fait forof ?

ES2015 formalise une notion critique : les itérables. Le protocole d’itérabilité d’un objet est clairement défini (via le symbole prédéfini Symbol.iterator), et de nombreux objets prédéfinis sont itérables : les tableaux naturellement, mais aussi les chaînes de caractères, les Map, les Set, les NodeList… Qui plus est, de nombreux objets proposent plusieurs itérateurs au-delà de leur itérabilité par défaut.

La boucle forof fournit le seul mécanisme de consommation d’itérables sur lequel votre code a le plein contrôle : on peut consommer juste ce qu’on veut, et la quantité peut elle-même être dynamique, définie algorithmiquement.

Par exemple, pour consommer un bon vieux tableau, en se concentrant comme le plus souvent sur les valeurs, pas les index, c’est nettemment plus agréable :

// Bof
for (var index = 0, len = items.length; index < len; ++index) {
var item = items[index]
// …
}

// Kewl!
for (const item of items) {
// …
}

Comme toutes les boucles, on peut y utiliser break, continue, return, etc. Mais c’est nettement plus générique que le for numérique : ça marche même sans index / position (ex. un Set), et on n’a pas besoin de penser à optimiser en mettant la longueur en cache…

En profiter pour utiliser const

Tu as remarqué comme j’ai pu préférer const avec le forof ? C’est parce que je n’ai rien à réaffecter : je travaille directement sur la valeur, pas sur un index que je dois manuellement faire grimper. Du coup, autant le déclarer const pour s’éviter des risques.

Déstructurer à la volée

Lorsque l’itérateur que tu consommes émet plusieurs valeurs, n’hésite pas à déstructurer à la volée. Par exemple, plutôt que de faire ça :

const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
])
for (const pair of map) {
console.log(pair[0], '=', pair[1])
}

On préfèrera faire ça :

for (const [key, value] of map) {
console.log(key, '=', value)
}

Penser aux itérateurs complémentaires

De nombreux itérables proposent, outre leur itérabilité par défaut, des itérateurs complémentaires. La plupart proposent au moins trois itérateurs : keys(), values() et entries(). Lorsque la notion de clé n’existe pas (comme pour un Set), les clés sont… les valeurs. On peut bien sûr avoir des itérateurs spécifiques plus « métier ».

Par exemple, disons que tu veux consommer un Array grâce à forof, tout en récupérant aussi les index à la volée. C’est parfaitement possible :

for (const [index, item] of items.entries()) {
// …
}

Compatible évaluation paresseuse !

Le vrai point fort de forof, c’est que grâce au fait qu’il consomme des itérables quelconques pas forcément en entier, mais plutôt au fil de l’eau, c’est le principal moyen de consommer des calculs paresseux (lazy evaluation), notamment des itérables infinis, tels que des séquences mathématiques ou cryptographiques, des générateurs procédureaux de valeurs, etc.

Imaginons un générateur matérialisant la suite de Fibonacci :

function* fibonacci() {
let [current, next] = [1, 1]
while (true) {
yield current
;[current, next] = [next, current + next]
}
}

La suite n’a pas de fin : si j’essaie un Array.from(…) ou un spread dessus, je suis foutu. Je peux en revanche récupérer les premiers termes par déstructuration positionnelle :

const [a, b, c, d, e] = fibonacci()
// a === 1, b === 1, c === 2, d === 3, e === 5

Mais comment consommer au fil de l’eau, une quantité inconnue à l’avance ? Il faut une boucle, dont on puisse sortir à la demande. Par exemple, pour afficher tous les termes inférieurs à 100 :

for (const term of fibonacci()) {
if (term > 100) break
console.log(term)
}

Toutes les primitives d’évaluation paresseuse (un concept très présent en programmation fonctionnelle) sont implémentables comme ça, par exemple la limitation en amont de quantité avec take :

function* take(count, iter) {
if (count === 0) return
for (const term of iter) {
yield term
if (--count <= 0) break
}
}

(Si tu fais du RxJS, ça devrait te parler…)

Performance

Tu trouveras tout et son contraire en ligne côté benchmarks de performance entre les différents types de boucles. Le plupart ne sont pas représentatifs. Il faut garder à l’esprit deux choses :

  • Tant qu’on n’est pas sur des énormes tableaux (millions d’éléments au moins), la différence est négligeable.
  • Même au-delà, si la boucle n’est pas transpilée (cf. plus bas la prise en charge native), ça revient au même.

Il est donc rarissime de devoir vraiment revenir à un for numérique ou un while.

Adieu les vieux !

Du coup, tu peux dès à présent remplacer l’immense majorité de tes itérations historiques (notamment le for numérique, le .forEach(…) ou le each(…) de jQuery / Lodash) par un joli forof tout clean et générique.

C’est dispo où ?

C’est dispo nativement à partir de Firefox 13, Chrome 38, Opera 25, Edge 12, Safari 7 et Node 0.12.

Et pour le reste, Babel et TypeScript transpilent évidemment, mais sur d’énormes tableaux (1M+ éléments), la performance peut s’en ressentir (et encore, ça dépend largement du moteur, de ton algo, du contexte…).