Arrêtez les fonctions de nidification! (Mais pas tous)

JavaScript a plus de quinze ans. néanmoins, le langage est encore mal compris par ce qui est peut-être la majorité des développeurs et des concepteurs utilisant le langage. Un des aspects les plus puissants, mais encore mal compris, de JavaScript sont les fonctions. Bien qu'essentiellement vitales pour JavaScript, leur utilisation abusive peut entraîner une inefficacité et nuire aux performances d'une application..


Préférer un tutoriel vidéo?


La performance est importante

À ses débuts sur le Web, les performances n'étaient pas très importantes.

À ses débuts sur le Web, les performances n'étaient pas très importantes. Des connexions par modem 56K (ou pire) à l'ordinateur Pentium 133 MHz de l'utilisateur final doté de 8 Mo de RAM, le Web devait être lent (même si cela n'a pas empêché tout le monde de s'en plaindre). C’est pour cette raison que JavaScript a tout d’abord été créé pour décharger le navigateur de traitements simples, tels que la validation de formulaires, afin de rendre certaines tâches plus faciles et plus rapides pour l’utilisateur final. Au lieu de remplir un formulaire, de cliquer sur envoyer et d’attendre au moins trente secondes que vous ayez saisi des données incorrectes dans un champ, les auteurs Web de JavaScript ont activé JavaScript pour valider votre saisie et vous avertir de toute erreur antérieure à la soumission du formulaire..

Avance rapide à aujourd'hui. Les utilisateurs finaux bénéficient d’ordinateurs multi-cœur et multi-GHz, d’une abondance de RAM et de vitesses de connexion rapides. JavaScript n'est plus relégué à la validation de formulaire simpliste, mais il peut traiter de grandes quantités de données, modifier n'importe quelle partie d'une page à la volée, envoyer et recevoir des données du serveur et ajouter de l'interactivité à une page par ailleurs statique dans son nom. d'améliorer l'expérience de l'utilisateur. C'est un modèle assez bien connu dans l'industrie informatique: un nombre croissant de ressources système permet aux développeurs d'écrire des systèmes d'exploitation et des logiciels plus sophistiqués et dépendants des ressources. Mais même avec cette quantité de ressources abondante et croissante, les développeurs doivent être conscients de la quantité de ressources que leur application consomme, en particulier sur le Web..

Les moteurs JavaScript actuels ont des années-lumière d'avance sur ceux d'il y a dix ans, mais ils n'optimisent pas tout. Ce qu'ils n'optimisent pas est laissé aux développeurs.

Il existe également un nouvel ensemble d'appareils, de téléphones intelligents et de tablettes activés pour le Web, fonctionnant sur un ensemble de ressources limité. Leurs systèmes d’exploitation et leurs applications réduites sont certes un succès, mais les principaux fournisseurs d’OS mobiles (et même de fournisseurs d’OS de bureau) se tournent vers les technologies Web en tant que plateforme de développement de choix, poussant les développeurs JavaScript à garantir que leur code est efficace et performant.

Une application peu performante va gâcher une bonne expérience.

Plus important encore, l'expérience de l'utilisateur dépend de bonnes performances. Des interfaces utilisateur jolies et naturelles enrichissent certes l'expérience d'un utilisateur, mais une application peu performante gâchera une bonne expérience. Si les utilisateurs ne veulent pas utiliser votre logiciel, à quoi sert-il de l'écrire? Il est donc primordial que, à l’ère du développement centré sur le Web, les développeurs JavaScript écrivent le meilleur code possible..

Alors, qu'est-ce que tout cela a à voir avec les fonctions?

Où vous définissez vos fonctions a un impact sur les performances de votre application.

Il existe de nombreux anti-patterns JavaScript, mais l'un d'eux impliquant des fonctions est devenu quelque peu populaire, en particulier parmi la foule qui s'efforce de contraindre JavaScript à imiter des fonctionnalités dans d'autres langues (des fonctionnalités telles que la confidentialité). Il imbrique des fonctions dans d'autres fonctions et, si cela est mal fait, cela peut avoir un effet négatif sur votre application..

Il est important de noter que cet anti-motif ne s'applique pas à toutes les instances de fonctions imbriquées, mais il est généralement défini par deux caractéristiques. Tout d'abord, la création de la fonction en question est généralement différée, ce qui signifie que la fonction imbriquée n'est pas créée par le moteur JavaScript au moment du chargement. En soi, cela n’est pas une mauvaise chose, mais c’est la deuxième caractéristique qui entrave les performances: la fonction imbriquée est créée à plusieurs reprises en raison d’appels répétés à la fonction externe. Ainsi, même s’il est facile de dire «toutes les fonctions imbriquées sont mauvaises», ce n’est certainement pas le cas, et vous pourrez identifier les fonctions imbriquées problématiques et les corriger pour accélérer votre application..


Fonctions d'imbrication dans des fonctions normales

Le premier exemple de cet anti-motif imbrique une fonction dans une fonction normale. Voici un exemple simplifié:

fonction foo (a, b) fonction bar () retour a + b;  retour bar ();  foo (1, 2);

Vous ne pouvez pas écrire ce code exact, mais il est important de reconnaître le motif. Une fonction extérieure, foo (), contient une fonction interne, bar(), et appelle cette fonction interne à faire le travail. Beaucoup de développeurs oublient que les fonctions sont des valeurs en JavaScript. Lorsque vous déclarez une fonction dans votre code, le moteur JavaScript crée un objet fonction correspondant, une valeur pouvant être affectée à une variable ou transmise à une autre fonction. La création d’un objet fonction ressemble à celle de tout autre type de valeur; le moteur JavaScript ne le crée pas avant qu'il en ait besoin. Ainsi, dans le cas du code ci-dessus, le moteur JavaScript ne crée pas le contenu interne. bar() fonctionner jusqu'à foo () exécute. Quand foo () les sorties, les bar() l'objet de fonction est détruit.

Le fait que foo () a un nom implique qu'il sera appelé plusieurs fois dans l'application. Alors qu'une exécution de foo () être considéré comme OK, les appels suivants entraînent un travail inutile pour le moteur JavaScript car il doit recréer une bar() objet de fonction pour chaque foo () exécution. Donc, si vous appelez foo () 100 fois dans une application, le moteur JavaScript doit créer et détruire 100 bar() objets de fonction. Big deal, non? Le moteur doit créer d'autres variables locales dans une fonction à chaque appel, alors pourquoi s'intéresser aux fonctions?

Contrairement aux autres types de valeurs, les fonctions ne changent généralement pas. une fonction est créée pour effectuer une tâche spécifique. Il n’est donc pas logique de perdre du temps en cycles CPU à recréer une valeur quelque peu statique.

Idéalement, le bar() Dans cet exemple, l'objet function ne doit être créé qu'une seule fois, ce qui est facile à atteindre, même si, naturellement, des fonctions plus complexes peuvent nécessiter une refactorisation poussée. L’idée est de déplacer le bar() déclaration en dehors de foo () de sorte que l'objet fonction n'est créé qu'une seule fois, comme ceci:

fonction foo (a, b) barre de retour (a, b);  barre de fonctions (a, b) return a + b;  foo (1, 2);

Notez que la nouvelle bar() la fonction est pas exactement comme il était à l'intérieur de foo (). Parce que le vieux bar() fonction utilisée le une et b paramètres dans foo (), la nouvelle version avait besoin d'un refactoring pour accepter ces arguments afin de faire son travail.

Selon le navigateur, ce code optimisé est de 10% à 99% plus rapide que la version imbriquée. Vous pouvez afficher et exécuter le test pour vous-même à l'adresse jsperf.com/nested-named-functions. Gardez à l’esprit la simplicité de cet exemple. Un gain de performance de 10% (au plus bas du spectre des performances) ne semble pas beaucoup, mais il serait supérieur si davantage de fonctions imbriquées et complexes sont impliquées.

Pour peut-être confondre le problème, placez ce code dans une fonction anonyme et auto-exécutable, comme ceci:

(fonction () fonction foo (a, b) barre de retour (a, b); barre de fonction (a, b) retour a + b; foo (1, 2); ());

Envelopper du code dans une fonction anonyme est un motif courant, et à première vue, il pourrait sembler que ce code reproduise le problème de performances mentionné ci-dessus en encapsulant le code optimisé dans une fonction anonyme. Bien que l'exécution de la fonction anonyme ait un léger impact négatif sur les performances, ce code est parfaitement acceptable. La fonction auto-exécutable sert uniquement à contenir et à protéger le foo () et bar() fonctions, mais plus important encore, la fonction anonyme n'exécute qu'une fois, donc le plus interne foo () et bar() les fonctions sont créées une seule fois. Cependant, il y a des cas où les fonctions anonymes sont tout aussi (ou plus) problématiques que les fonctions nommées.


Fonctions anonymes

En ce qui concerne ce sujet de performance, les fonctions anonymes peuvent être plus dangereuses que les fonctions nommées..

Ce n'est pas l'anonymat de la fonction qui est dangereux, mais c'est la façon dont les développeurs les utilisent. Il est assez courant d'utiliser des fonctions anonymes lors de la configuration de gestionnaires d'événements, de fonctions de rappel ou de fonctions d'itérateur. Par exemple, le code suivant affecte un Cliquez sur écouteur d'événement sur le document:

document.addEventListener ("clic", fonction (evt) alert ("vous avez cliqué sur la page."););

Ici, une fonction anonyme est transmise au addEventListener () méthode pour brancher le Cliquez sur événement sur le document; la fonction s'exécute donc chaque fois que l'utilisateur clique n'importe où sur la page. Pour illustrer une autre utilisation courante des fonctions anonymes, considérons cet exemple qui utilise la bibliothèque jQuery pour tout sélectionner. éléments dans le document et les parcourir avec la chaque() méthode:

$ ("a"). each (fonction (index) this.style.color = "red";);

Dans ce code, la fonction anonyme passée à l'objet jQuery chaque() la méthode s'exécute pour chaque élément trouvé dans le document. Contrairement aux fonctions nommées, où elles sont impliquées pour être appelées à plusieurs reprises, l'exécution répétée d'un grand nombre de fonctions anonymes est plutôt explicite. Pour des raisons de performance, il est impératif qu’elles soient efficaces et optimisées. Jetez un coup d’œil au plugin suivant jQuery (encore une fois trop simplifié):

$ .fn.myPlugin = fonction (options) retour this.each (fonction () var $ ceci = $ (ceci); fonction changeColor () $ this.css (color: options.color)) changeColor ();); ;

Ce code définit un plugin extrêmement simple appelé monPlugin; c'est tellement simple que beaucoup de traits communs de plugin sont absents. Normalement, les définitions de plug-in sont encapsulées dans des fonctions anonymes auto-exécutables, et des valeurs par défaut sont généralement fournies pour les options afin de garantir la disponibilité des données valides. Ces choses ont été enlevées pour plus de clarté.

Le but de ce plugin est de changer la couleur des éléments sélectionnés en ce qui est spécifié dans le options objet passé au myPlugin () méthode. Il le fait en passant une fonction anonyme à la chaque() itérateur, faisant que cette fonction s'exécute pour chaque élément de l'objet jQuery. Dans la fonction anonyme, une fonction interne appelée changer de couleur() effectue le travail réel de modification de la couleur de l'élément. Comme écrit, ce code est inefficace parce que, vous l’avez deviné, le changer de couleur() la fonction est définie dans la fonction itérative? faire recréer le moteur JavaScript changer de couleur() à chaque itération.

Rendre ce code plus efficace est assez simple et suit le même schéma que précédemment: refactoriser le changer de couleur() fonction à définir en dehors de toute fonction contenant, et lui permettre de recevoir les informations nécessaires à son travail. Dans ce cas, changer de couleur() a besoin de l'objet jQuery et de la nouvelle valeur de couleur. Le code amélioré ressemble à ceci:

fonction changeColor ($ obj, color) $ obj.css (color: color);  $ .fn.myPlugin = fonction (options) retour this.each (fonction () var $ ceci = $ (ceci); changeColor ($ this, options.color);); ;

Fait intéressant, ce code optimisé augmente les performances d’une marge bien inférieure à celle du foo () et bar() Par exemple, avec Chrome en tête du groupe avec un gain de performances de 15% (jsperf.com/function-nesting-with-jquery-plugin). La vérité est que l'accès au DOM et l'utilisation de l'API de jQuery ajoutent leur propre succès à la performance, en particulier celle de jQuery. chaque(), ce qui est notoirement lent comparé aux boucles natives de JavaScript. Mais comme auparavant, gardez à l'esprit la simplicité de cet exemple. Plus les fonctions sont imbriquées, plus le gain en performances de l'optimisation est grand.

Fonctions d'imbrication dans les fonctions du constructeur

Une autre variante de cet anti-motif est l'imbrication de fonctions dans des constructeurs, comme indiqué ci-dessous:

fonction Person (firstName, lastName) this.firstName = prénom; this.lastName = lastName; this.getFullName = function () return this.firstName + "" + this.lastName; ;  var jeremy = nouvelle personne ("Jeremy", "McPeak"), jeffrey = nouvelle personne ("Jeffrey", "Way");

Ce code définit une fonction constructeur appelée La personne(), et cela représente (si ce n'était pas évident) une personne. Il accepte les arguments contenant le nom et le prénom d'une personne et stocke ces valeurs dans Prénom et nom de famille propriétés, respectivement. Le constructeur crée également une méthode appelée getFullName (); il concatène le Prénom et nom de famille propriétés et renvoie la valeur de chaîne résultante.

Lorsque vous créez un objet en JavaScript, celui-ci est stocké en mémoire.

Ce modèle est devenu assez courant dans la communauté JavaScript d'aujourd'hui, car il peut émuler la confidentialité, une fonctionnalité pour laquelle JavaScript n'est actuellement pas conçu (notez que la confidentialité ne figure pas dans l'exemple ci-dessus; vous l'examinerez plus tard). Mais en utilisant ce modèle, les développeurs créent une inefficacité non seulement en termes de temps d'exécution, mais également en termes d'utilisation de la mémoire. Lorsque vous créez un objet en JavaScript, celui-ci est stocké en mémoire. Il reste en mémoire jusqu'à ce que toutes ses références soient définies sur nul ou sont hors de portée. Dans le cas du Jeremy objet dans le code ci-dessus, la fonction affectée à getFullName est généralement stocké en mémoire aussi longtemps que le Jeremy l'objet est en mémoire. Quand le Jeffrey Si un objet est créé, un nouvel objet fonction est créé et affecté à Jeffreyde getFullName membre, et il consomme aussi de la mémoire aussi longtemps que Jeffrey est en mémoire. Le problème ici est que jeremy.getFullName est un objet de fonction différent de jeffrey.getFullName (jeremy.getFullName === jeffrey.getFullName résulte en faux; exécutez ce code à l'adresse http://jsfiddle.net/k9uRN/). Ils ont tous les deux le même comportement, mais ce sont deux objets fonction complètement différents (et consomment donc chacun de la mémoire). Pour plus de clarté, regardez la Figure 1:

Figure 1

Ici, vous voyez le Jeremy et Jeffrey objets, qui ont chacun leur propre getFullName () méthode. Alors, chacun La personne objet créé a son propre unique getFullName () méthode qui consomme chacune sa part de mémoire. Imaginez en créer 100 La personne objets: si chacun getFullName () méthode consomme 4 Ko de mémoire, puis 100 La personne les objets consomment au moins 400 Ko de mémoire. Cela peut s’ajouter, mais il peut être considérablement réduit en utilisant le prototype objet.

Utilisez le prototype

Comme mentionné précédemment, les fonctions sont des objets en JavaScript. Tous les objets de fonction ont un prototype propriété, mais il n’est utile que pour les fonctions du constructeur. En bref, le prototype la propriété est littéralement un prototype pour la création d'objets; tout ce qui est défini sur le prototype d'une fonction constructeur est partagé entre tous les objets créés par cette fonction constructeur.

Malheureusement, les prototypes ne sont pas assez soulignés dans l'éducation JavaScript.

Malheureusement, les prototypes ne sont pas suffisamment soulignés dans l'enseignement de JavaScript, mais ils sont absolument essentiels à JavaScript, car il est basé sur des prototypes et est construit à l'aide de prototypes. Il s'agit d'un langage prototypique. Même si vous n'avez jamais tapé le mot prototype dans votre code, ils sont utilisés dans les coulisses. Par exemple, chaque méthode native basée sur des chaînes, telle que Divisé(), substr (), ou remplacer(), sont définis sur Chaîne()Le prototype de. Les prototypes sont si importants pour le langage JavaScript que si vous n'acceptez pas le caractère prototype de JavaScript, vous écrivez du code inefficace. Considérez la mise en œuvre ci-dessus de la La personne type de données: création d'un La personne objet nécessite le moteur JavaScript pour effectuer plus de travail et allouer plus de mémoire.

Alors, comment utiliser le prototype propriété rendre ce code plus efficace? Eh bien, voyons d’abord le code refactorisé:

fonction Person (firstName, lastName) this.firstName = prénom; this.lastName = lastName;  Person.prototype.getFullName = function () return this.firstName + "" + this.lastName; ; var jeremy = nouvelle personne ("Jeremy", "McPeak"), jeffrey = nouvelle personne ("Jeffrey", "Way");

Ici le getFullName () la définition de la méthode est déplacée du constructeur vers le prototype. Ce changement simple a les effets suivants:

  • Le constructeur effectue moins de travail et s'exécute donc plus rapidement (18% à 96% plus rapidement). Exécutez le test dans votre navigateur si vous le souhaitez.
  • le getFullName () méthode est créée une seule fois et partagée entre tous La personne objets (jeremy.getFullName === jeffrey.getFullName résulte en vrai; exécutez ce code à l'adresse http://jsfiddle.net/Pfkua/). Pour cette raison, chaque La personne objet utilise moins de mémoire.

Reportez-vous à la figure 1 et notez comment chaque objet a son propre getFullName () méthode. Maintenant que getFullName () est défini sur le prototype, le diagramme d'objets change et est illustré à la figure 2:

Figure 2

le Jeremy et Jeffrey les objets n'ont plus leur propre getFullName () méthode, mais le moteur JavaScript le trouvera sur La personne()Le prototype de. Dans les moteurs JavaScript plus anciens, le processus de recherche d'une méthode sur le prototype pouvait entraîner des pertes de performances, mais pas dans les moteurs JavaScript actuels. La vitesse à laquelle les moteurs modernes trouvent des méthodes prototypées est extrêmement rapide.

Intimité

Mais qu'en est-il de la vie privée? Après tout, cet anti-modèle est né d'un besoin perçu de membres d'objet privé. Si vous n'êtes pas familier avec le motif, regardez le code suivant:

fonction Foo (paramOne) var thisIsPrivate = paramOne; this.bar = function () return thisIsPrivate; ;  var foo = new Foo ("Hello, Privacy!"); alerte (foo.bar ()); // alerte "Bonjour, confidentialité!"

Ce code définit une fonction constructeur appelée Foo (), et il a un paramètre appelé paramOne. La valeur passée à Foo () est stocké dans une variable locale appelée thisIsPrivate. Notez que thisIsPrivate est une variable, pas une propriété; donc, il est inaccessible en dehors de Foo (). Il y a aussi une méthode définie dans le constructeur, et elle s'appelle bar(). Parce que bar() est défini dans Foo (), il a accès au thisIsPrivate variable. Alors, quand vous créez un Foo objet et appel bar(), la valeur attribuée à thisIsPrivate est retourné.

La valeur attribuée à thisIsPrivate est préservé. Il n'est pas accessible en dehors de Foo (), et ainsi, il est protégé contre les modifications extérieures. C'est génial, non? Eh bien oui et non. On comprend pourquoi certains développeurs souhaitent émuler la confidentialité en JavaScript: vous pouvez vous assurer que les données d'un objet sont protégées contre la falsification extérieure. Mais en même temps, vous introduisez de l’inefficacité dans votre code en n’utilisant pas le prototype..

Encore une fois, qu'en est-il de la vie privée? Eh bien, c'est simple: ne le faites pas. Actuellement, la langue ne prend pas officiellement en charge les membres d'objet privé, bien que cela puisse changer lors d'une future révision de la langue. Au lieu d'utiliser des fermetures pour créer des membres privés, la convention désignant les "membres privés" consiste à ajouter un identificateur de soulignement à l'identificateur (par exemple: _thisIsPrivate). Le code suivant réécrit l'exemple précédent en utilisant la convention:

fonction Foo (paramOne) this._thisIsPrivate = paramOne;  Foo.prototype.bar = function () return this._thisIsPrivate; ; var foo = new Foo ("Bonjour, Convention concernant la protection de la vie privée!"); alerte (foo.bar ()); // alertes "Bonjour, Convention pour dénoter la confidentialité!"

Non, ce n'est pas privé, mais la convention du soulignement dit en gros "ne me touchez pas". Jusqu'à ce que JavaScript prenne pleinement en charge les propriétés et méthodes privées, vous ne préférez pas un code plus efficace et plus performant que la confidentialité? La réponse correcte est: oui!


Résumé

La définition des fonctions dans votre code a une incidence sur les performances de votre application. Gardez cela à l'esprit lorsque vous écrivez votre code. Ne pas imbriquer des fonctions dans une fonction fréquemment appelée. Cela gaspille les cycles de la CPU. En ce qui concerne les fonctions du constructeur, adoptez le prototype; ne pas le faire entraîne un code inefficace. Après tout, les développeurs écrivent des logiciels que les utilisateurs peuvent utiliser, et les performances d'une application sont tout aussi importantes pour l'expérience utilisateur que l'interface utilisateur..