EDIT : Je vous invite à lire les retours très pertinents et intéressants de @naholyr, @BAKfr et @KoonePoew suite auxquels j’ai modifié les exemples et une partie du contenu de cet article.
Ajoutée sur ECMAScript 5 (ES5) aux côtés de nombreuses autres fonctions tableaux, Array.prototype.forEach
permet de parcourir des tableaux javascript de manière moins verbeuse (plus moderne ?) qu’avec la classique boucle for
.
var array = [1,2,3,4,5]; for (var i = 0 ; i < array.length ; i++) { console.log(array[i]); }
Le bout de code ci-dessus peut alors être écrit de la manière suivante :
var array = [1,2,3,4,5]; array.forEach(function(value) { console.log(value); });
Il faut bien reconnaître que c’est quand même plus sexy ! Je passe volontairement sur les subtilités de l’utilisation d’un callback et sur le contexte de this
dans ce dernier pour m’attarder plutôt sur les quelques exemples de code qui vont suivre.
Je privilégie depuis longtemps l’utilisation du forEach
ES5 en lieu et place du classique for
et j’encourage également à se passer le plus possible des pseudo forEach
implémentés au sein des frameworks tels que AngularJs (angular.forEach
permet par exemple un parcours à la fois de tableaux classiques et de propriétés d’objets ~ tableaux associatifs…) en utilisant au besoin un script tel que es5-shim si le support des vieux navigateurs est obligatoire.
Mais si on applique bêtement cette règle, on en vient parfois à utiliser forEach
en dépit du bon sens…
function isElementVisible(element) { var isVisible = false; element.children.forEach(function(child) { if (child.visible) { isVisible = true; // Ici, on continue alors qu'on a déjà trouvé notre réponse... } }); return isVisible; } var plop = //... if (!isElementVisible(plop)) { // ... }
Beaucoup de développeurs habitués à jQuery et à son jQuery.each() avant de développer en « vrai » Javascript pensent qu’il suffit de faire un return false
dans le callback de l’appel à Array.prototype.forEach
pour breaker le parcours. Or, il n’y a en fait aucun moyen de stopper ce parcours. Partant de ce postulat, le bout de code ci-dessus a perdu en lisibilité, en performance et donc en intérêt.
Comme pointé par @naholyr, @BAKfr et @KoonePoew dans les commentaires, c’est en fait Array.prototype.some
qui répond parfaitement à notre besoin. Comme expliqué sur le site de Mozilla, la méthode some()
teste si certains éléments du tableau passent le test implémenté par la fonction fournie.
Notre code peut alors être très simplement réécrit de cette manière :
function isElementVisible(element) { return element.children.some(function(child) { return child.visible; }); }
Notez que some
s’arrête dès que le callback renvoie true
, ce qui est exactement ce que l’on recherche.
Prenons maintenant l’exemple de la recherche d’un élément par son code :
function findElementByCode(elements, code) { var searchedElement = null; elements.forEach(function(element) { if (element.code === code) { searchedElement = element; } }); return searchedElement; }
Là encore, pas moyen de breaker au sein du forEach()
ce qui peut poser des problèmes sérieux de performance.
Pour améliorer la performance de cette fonction, il est possible de la modifier pour utiliser l’alternative Array.prototype.some qui, elle, autorise bien le break dans le callback :
function findElementByCode(elements, code) { var searchedElement = null; elements.some(function(element) { if (element.code === code) { searchedElement = element; return true; } }); return searchedElement; }
Cette fois, les performances sont de retour mais on perd encore un peu plus en lisibilité. On a ajouté un return
pour breaker (on ne retourne pas la valeur recherchée) et on détourne l’utilisation de la méthode de test some
.
ECMAScript 6 ajoute une fonction find()
sur le prototype de Array qui permettra de réécrire à terme cette recherche de la manière suivante :
function findElementByCode(elements, code) { return elements.find(function(element) { return element.code === code; }); }
Et si finalement, en attendant la sortie et l’adoption d’ES6, la bonne vieille boucle for
n’était tout simplement pas LA solution :
function findElementByCode(elements, code) { for (var i = 0 ; i < elements.length ; i ++) { if (elements[i].code === code) { return elements[i]; } } return null; }
Certains pourraient être tentés de mettre en cache la propriété length dans cette fonction pour des raisons de performances :
function findElementByCode(elements, code) { var elementsLength = elements.length; for (var i = 0 ; i < elementsLength ; i ++) { if (elements[i].code === code) { return elements[i]; } } return null; }
Attention à ce type de micro-optimisation qui n’a plus de sens dans les navigateurs modernes où la mise en cache de length
est faite automatiquement à l’exécution. Comme souvent, il y a certainement des gains de performances bien plus important ailleurs avant qu’il ne soit nécessaire de complexifier le code source pour ce type de pseudo-optimisation (Voici le résultat d’un test intéressant sur jsperf pour vous convaincre : javascript length cache vs no cache).
Finalement, si je devais proposer une bonne pratique ce serait la suivante :
- Utiliser le
forEach
natif ES5 pour les parcours intégraux de tableaux - Rechercher « s’il existe une méthode avec la sémantique que l’on cherche » (cf commentaire de @BAKfr)
- Utiliser
for (var i ; ...)
dans les autres cas
Hello, le fond est vrai (le fait que la boucle sera toujours intégralement parcourue) mais l’exemple est mauvais. En plus tu présentes « some » et « every » qui sont faits pour traiter ton exemple… Et tu les réduis à de simple « forEach interruptibles » 🙁
Ton algorithme est « element est visible si au moins un de ses enfants est visible », qu’on pourrait traduire « element is visible if some child is visible ».
La traduction en JavaScript moderne devient triviale, « return element.children.some(function () { return child.visible }) ».
L’intérêt des méthodes de Array est d’apporter une approche fonctionnelle, et ce n’est qu’en adoptant cette approche qu’on utilise le plein potentiel de ces fonctions.
Cela dit ça ne retire rien au fond de l’article en terme de performance, là ce qui fait que l’exemple n’était pas une bonne illustration c’est que tu manipules un booléen. Mais cela dit quand ton traitement est « effectuer Action sur chaque élément et s’arrêter des que Condition est rencontrée » some() reste adapté. Ce qui rendait ton code moche c’est que tu utilisais every() en compliquant le callback inutilement.
Donc globalement, jamais besoin de retourner au vieux for sauf pour parcourir un objet ou si l’overhead du callback est trop élevé.
Salut @naholyr, @BAKfr, @KoonePoew.
Je vous remercie pour vos retours.
Effectivement mon exemple était particulièrement mal choisi puisqu’il représentait le candidat idéal pour l’utilisation de some (« element est visible si au moins un de ses enfants est visible »).
L’exemple pourrait être modifié pour une recherche dans un tableau par exemple, la fonction find étant encore à l’état de draft… Dans ce cas, ce n’est plus un booléen qu’on attend en sortie.
En revanche, je ne suis pas tout à fait d’accord avec toi @naholyr sur le fait que some() reste adapté au traitement « effectuer Action sur chaque élément et s’arrêter des que Condition est rencontrée ». Dans ce cas, je pense qu’on détourne la fonction de some() et que l’on rend l’interprétation du code plus difficile.
Pour « effectuer Action sur chaque élément et s’arrêter des que Condition est rencontrée », on peut aussi utiliser forEach au lieu du some en ne retournant rien.
Ça ne résout pas le fait que le code est moins compréhensible, mais au moins il n’y a plus à jouer avec some() ou every().
Ça dépend comment on voit les choses, dans le scénario (très générique) « effectuer Action sur chaque élément tant que Condition n’est pas rencontrée », on ne s’intéresse pas au résultat final (sinon c’est Array#map()) mais au fait que la condition ait été rencontrée.
Le code final devient
Il ne me semble pas illisible, ni inadapté au traitement 😉 mais effectivement on détourne Array#some() de son usage, c’est là que les librairies externes sont intéressantes puisque _.first() fera exactement la même chose avec un nom plus adapté.
En plus des _.property et autres outils simplifiant la programmation orientée fonction.
Excellent rappel.
Une question cependant, ne serait-il pas préférable (lisibilité ? efficacité ?) d’utiliser des
for (var i in monTableau)
plutot que le for plus classique ?
@Swanny Lorenzi,
Attention à l’utilisation de la syntaxe
for (var i in array)
qui pose souvent plus de problèmes qu’elle n’en résoud.Jetez un oeil sur SO ou Mozilla pour vous en convaincre.
Pour mémoire, certains vieux navigateurs renvoyaient même l’attribut length lors du parcours d’un tableau avec cette syntaxe…
Bonjour,
Je pense au contraire que dans la majorité des cas, il existe une fonction native dans
Array
pour faire ce que l’on veut. Dans l’exemple montré, c’est même le candidat idéal pour utilisersome()
. On cherche précisément si certains des éléments sont visibles (en anglais: « if some of the elements are visibles »)Bien sur, je ne dit pas que toutes les boucles peuvent être remplacées proprement par les méthodes de
Array
. Lorsque l’on doit coder une itération sur unArray
, la première étape devrait être de chercher s’il existe une méthode avec la sémantique que l’on cherche.Il existe bien un moyen de stopper l’exécution de forEach : lancer une exception.
C’est d’ailleurs ce qui est fait en interne dans le framework Prototype.js. Il suffit de lancer l’objet special $break, que Prototype va attraper, et arrêter le parcours.
Il est donc possible de le faire en vanilla en englobant le forEach d’un try-catch même si on en revient toujours au problème de laideur 🙂
Un
return;
suffit à stopper une boucle forEach, cela évite le bloctry-catch
.@KoonePoew Peut-être qu’on ne parle pas du même cas mais a priori, un
return
ne suffit pas à stopper une boucleforEach
…Attention aux throw « utilitaires » comme ça, les exceptions sont ce qui se fait de plus coûteux. Il vaut vraiment mieux passer par un for simple ou détourner `some` de son usage.
@Loick En effet, c’est une technique très connue pour stopper l’itération.
Et justement, comme vous le dites, notamment dans le cas des exemples de cet article, ce serait compliquer inutilement le code pour utiliser une fonction forEach faite pour boucler, comme son nom l’indique, sur « chaque » élément d’un tableau…
Et que penses-tu de l’utilisation des méthodes proposées par les Frameworks dans ce cas là ?
Exemple avec lodash :
function findElementByCode(elements, code) {
return _.find(elements, { 'code': code });
}
Salut @Jouj,
Dans ton exemple, je dirais que si lodash est déjà intégré au projet pour d’autres besoins, il faut l’utiliser, sinon, ce n’est pas la peine ici de rajouter cette dépendance pour un simple findByCode.
En gros je préfère utiliser quand c’est possible du JS standard dont l’API, effleurée dans cet article, est déjà extrêmement riche
L’article date d’il y’a maintenant 2 ans et entre temps, la méthode find() a été ajoutée à Array. (https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Array/find )
La plupart des framework comme lodash et underscore perdent une grande partie de leur utilité d’ailleurs.
C’est exact !