Les objets Iterable sont une généralisation des tableaux. C’est un concept qui permet de rendre n’importe quel objet utilisable dans une boucle for..of
.
Bien sûr, les tableaux sont itérables. Mais il existe de nombreux autres objets intégrés, qui sont également itérables. Par exemple, les chaînes de caractères sont également itérables.
Si un objet n’est pas techniquement un tableau, mais représente une collection (liste, set) de quelque chose, alors for..of
est une excellente syntaxe pour boucler dessus, voyons comment le faire fonctionner.
Symbol.iterator
Nous pouvons facilement saisir le concept des itérables en faisant le nôtre.
Par exemple, nous avons un objet qui n’est pas un tableau, mais qui semble convenir pour une boucle for..of
.
Comme un objet range
qui représente un intervalle de nombres :
let range = {
from: 1,
to: 5
};
// Nous voulons que le for..of fonctionne :
// for (let num of range) ... num=1,2,3,4,5
Pour rendre la range
itérable (et donc laisser for..of
faire son travail), nous devons ajouter une méthode à l’objet nommé Symbol.iterator
(un symbole intégré spécial que pour cela).
- Lorsque
for..of
démarre, il appelle cette méthode une fois (ou des erreurs si il n’est pas trouvé). La méthode doit retourner un iterator – un objet avec la méthodenext
. - À partir de là,
for..of
ne fonctionne qu’avec cet objet retourné. - Quand
for..of
veut la valeur suivante, il appellenext()
sur cet objet. - Le résultat de
next()
doit avoir la forme{done: Boolean, value: any}
, oùdone = true
signifie que l’itération est terminée, sinonvalue
doit être la nouvelle valeur.
Voici l’implémentation complète de range
avec les remarques :
let range = {
from: 1,
to: 5
};
// 1. l'appel d'un for..of appelle initialement ceci
range[Symbol.iterator] = function() {
// ...il retourne l'objet itérateur :
// 2. À partir de là, for..of fonctionne uniquement avec cet itérateur, lui demandant les valeurs suivantes
return {
current: this.from,
last: this.to,
// 3. next() est appelée à chaque itération par la boucle for..of
next() {
// 4. il devrait renvoyer la valeur sous forme d'objet {done: .., valeur: ...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
};
// maintenant ça marche !
for (let num of range) {
alert(num); // 1, ensuite 2, 3, 4, 5
}
Veuillez noter la fonctionnalité principale des iterables : separation of concerns (séparation des préoccupations).
- Le
range
lui-même n’a pas la méthodenext()
. - Au lieu de cela, un autre objet, appelé “iterator”, est créé par l’appel à
range[Symbol.iterator]()
, et sa méthodenext()
génère des valeurs pour l’itération.
Ainsi, l’objet itérateur est séparé de l’objet sur lequel il est itéré.
Techniquement, nous pouvons les fusionner et utiliser range
lui-même comme itérateur pour simplifier le code.
Comme ça :
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
for (let num of range) {
alert(num); // 1, ensuite 2, 3, 4, 5
}
Maintenant, range[Symbol.iterator]()
renvoie l’objet range
lui-même : il dispose de la méthode next()
et se souvient de la progression de l’itération en cours dans this.current
. C’est plus court? Oui. Et parfois c’est aussi bien.
L’inconvénient est qu’il est maintenant impossible d’avoir deux boucles for..of
s’exécutant simultanément sur l’objet: elles partageront l’état d’itération, car il n’y a qu’un seul itérateur – l’objet lui-même. Cependant, il est rare de disposer de deux for-of parallèles, faisables avec certains scénarios asynchrones.
Des itérateurs infinis sont également possibles. Par exemple, range
devient infini pour range.to = Infinity
. Ou nous pouvons créer un objet itérable qui génère une suite infinie de nombres pseudo-aléatoires. Il peut être aussi utile.
Il n’y a pas de limitation sur next
, il peut renvoyer de plus en plus de valeurs, c’est normal.
Bien sûr, la boucle for..of
sur une telle itération serait sans fin. Mais on peut toujours l’arrêter en utilisant break
.
String est iterable
Les tableaux et les chaînes de caractères sont les iterables intégrés les plus largement utilisés.
Pour une chaîne de caractères, for..of
boucle sur ses caractères :
for (let char of "test") {
// se déclenche 4 fois: une fois pour chaque caractère
alert( char ); // t, ensuite e, ensuite s, ensuite t
}
Et cela fonctionne correctement avec les paires de substitution !
let str = '𝒳😂';
for (let char of str) {
alert( char ); // 𝒳, et ensuite 😂
}
Appeler explicitement un itérateur
Pour une compréhension plus approfondie, voyons comment utiliser explicitement un itérateur.
Nous allons parcourir une chaîne de caractères de la même manière que for..of
, mais avec des appels directs. Ce code crée un itérateur de chaîne de caractères et en récupère la valeur “manuellement” :
let str = "Hello";
// fait la même chose que
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // affiche les caractères un par un
}
Cela est rarement nécessaire, mais nous donne plus de contrôle sur le processus que for..of
. Par exemple, nous pouvons scinder le processus d’itération : itérer un peu, puis arrêter, faire autre chose, puis reprendre plus tard.
Iterables et array-likes
Il existe deux termes officiels qui se ressemblent mais qui sont très différents. Assurez-vous de bien les comprendre pour éviter la confusion.
- Iterables sont des objets qui implémentent la méthode
Symbol.iterator
, comme décrit ci-dessus. - Array-likes sont des objets qui ont des index et des
length
, ils ressemblent donc à des tableaux.
Lorsque nous utilisons JavaScript pour des tâches pratiques dans un navigateur ou d’autres environnements, il est possible que nous rencontrions des objets qui sont iterables ou des array-like, ou les deux.
Par exemple, les chaînes de caractères sont à la fois iterables (for..of
fonctionne dessus) et des array-likes (elles ont des index numériques et une longueur).
Mais un itérable peut ne pas ressembler à un array-like. Et inversement, un array-like peut ne pas être itérable.
Par exemple, la range
dans l’exemple ci-dessus est itérable, mais pas comme un array-like, car elle n’a pas de propriétés indexées et de length
.
Et voici l’objet qui ressemble à un tableau, mais pas itérable :
let arrayLike = { // a des index et une longueur => semblable à un tableau
0: "Hello",
1: "World",
length: 2
};
// Erreur (pas de Symbol.iterator)
for (let item of arrayLike) {}
Les iterables et les array-likes ne sont généralement pas des tableaux, ils n’ont pas push
, pop
, etc. C’est plutôt gênant si nous avons un tel objet et que nous voulons travailler avec lui comme avec un tableau. Par exemple, nous aimerions travailler avec une plage en utilisant les méthodes de tableau. Comment y parvenir ?
Array.from
Il existe une méthode universelle Array.from qui prend une valeur itérable ou array-like et en fait un “vrai” Array
. Ensuite, nous pouvons appeler des méthodes de tableau dessus.
Par exemple :
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World (la méthode fonctionne)
Array.from
à la ligne (*)
prend l’objet, l’examine comme étant un objet itérable ou un array-like, crée ensuite un nouveau tableau et y copie tous les éléments.
Il en va de même pour un itérable :
// en supposant que cette "range" est tirée de l'exemple ci-dessus
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (array toString conversion fonctionne)
La syntaxe complète de Array.from
permet aussi de fournir une fonction optionnelle de “mapping” :
Array.from(obj[, mapFn, thisArg])
Le second argument mapFn
peut être une fonction à appliquer à chaque élément avant de l’ajouter au tableau, et thisArg
permet d’en définir le this
.
Par exemple :
// en supposant que cette "range" est tirée de l'exemple ci-dessus
// met au carré chaque nombre
let arr = Array.from(range, num => num * num);
alert(arr); // 1,4,9,16,25
Ici, nous utilisons Array.from
pour transformer une chaîne en un tableau de caractères :
let str = '𝒳😂';
// divise une chaîne en un tableau de caractères
let chars = Array.from(str);
alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2
Contrairement à str.split
, il repose sur la nature itérable de la chaîne et donc, tout comme for..of
, fonctionne correctement avec des paires de substitution.
Techniquement, il fait la même chose que :
let str = '𝒳😂';
let chars = []; // Array.from en interne fait la même boucle
for (let char of str) {
chars.push(char);
}
alert(chars);
…Mais c’est plus court.
Nous pouvons même créer une fonction slice
qui prend en compte les paires de substitution :
function slice(str, start, end) {
return Array.from(str).slice(start, end).join('');
}
let str = '𝒳😂𩷶';
alert( slice(str, 1, 3) ); // 😂𩷶
// les méthodes native ne supporte pas les paires de substitution
alert( str.slice(1, 3) ); // ordures (deux pièces de paires de substitution différentes)
Résumé
Les objets pouvant être utilisés dans for..of
s’appellent iterable.
- Techniquement, les iterables doivent implémenter la méthode nommée
Symbol.iterator
.- Le résultat de
obj[Symbol.iterator]()
s’appelle un itérateur. Il gère le processus d’itération ultérieur. - Un itérateur doit avoir la méthode nommée
next()
qui retourne un objet{done: Boolean, value: any}
, icidone: true
dénote la fin du processus de l’itération, sinonvalue
est la valeur suivante.
- Le résultat de
- La méthode
Symbol.iterator
est appelée automatiquement parfor..of
, mais nous pouvons aussi le faire directement. - Les iterables intégrés tels que des chaînes de caractères ou des tableaux implémentent également
Symbol.iterator
. - L’itérateur de chaîne de caractères connaît les paires de substitution.
Les objets qui ont des propriétés indexées et des length
sont appelés array-like. De tels objets peuvent également avoir d’autres propriétés et méthodes, mais ne possèdent pas les méthodes intégrées des tableaux.
Si nous regardons à l’intérieur de la spécification – nous verrons que la plupart des méthodes intégrées supposent qu’elles fonctionnent avec des éléments iterables ou des array-like au lieu de “vrais” tableaux, car c’est plus abstrait.
Array.from(obj[, mapFn, thisArg])
créer un véritable Array
à partir d’un obj
itérable ou array-like, et nous pouvons ensuite utiliser des méthodes de tableau sur celui-ci. Les arguments optionnels mapFn
et thisArg
nous permettent d’appliquer une fonction à chaque élément.