Array.forEach n’est pas toujours la meilleure solution !

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
Cette entrée a été publiée dans Développement Web, Javascript le par .

À propos Olivier Balais

Jeune ingénieur logiciel basé à Lyon (@overnetcity) passionné par les NTIC et le développement Web, je suis actuellement salarié chez Reputation VIP et effectue en parallèle des missions ponctuelles en temps que Freelance. Passionné depuis toujours par l'informatique et le développement, suite à une formation solide à l'INSA de Lyon, je me suis spécialisé dans la réalisation de bout en bout de projets web complexes.

17 réflexions au sujet de « Array.forEach n’est pas toujours la meilleure solution ! »

  1. naholyr

    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é.

    Répondre
    1. Olivier Balais Auteur de l’article

      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.

      Répondre
      1. KoonePoew

        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.

        function findElementByCode(elements, code) {
         
            var searchedElement = null;
            elements.forEach(function(element) {
                if (element.code === code) {
                    searchedElement = element;
                    return; 
                }
            });
         
            return searchedElement;
        }
        

        Ç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().

      2. naholyr

        Ç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

        var conditionAEteRencontree = array.some(function (element) {
          Action(element)
          return Condition(element)
        });
        

        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.

  2. Swanny Lorenzi

    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 ?

    Répondre
    1. Olivier Balais Auteur de l’article

      @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…

      Répondre
  3. BAKfr

    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 utiliser some(). On cherche précisément si certains des éléments sont visibles (en anglais: « if some of the elements are visibles »)

    function isElementVisible(element) {
    
        return element.children.some(function(child) {
            return child.visible;
        });
    }
    

    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 un Array, la première étape devrait être de chercher s’il existe une méthode avec la sémantique que l’on cherche.

    Répondre
  4. Loick

    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 🙂

    Répondre
      1. Olivier Balais Auteur de l’article

        @KoonePoew Peut-être qu’on ne parle pas du même cas mais a priori, un return ne suffit pas à stopper une boucle forEach

        var array = [1, 2, 3, 4];
        array.forEach(function(elt) {
          console.log(elt);
          return;
        }); 
        // sortie : 1 2 3 4
        
      2. naholyr

        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.

    1. Olivier Balais Auteur de l’article

      @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…

      Répondre
  5. Jouj

    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 });
    }

    Répondre
    1. Olivier Balais Auteur de l’article

      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

      Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *