title: 10 bonnes pratiques JavaScript url: http://www.js-attitude.fr/2013/01/21/dix-bonnes-pratiques-javascript/ hash_url: 39c6b8b0ae16fbd5b07385deb48dba10
Au fil des formations, je remarque que de nombreuses bonnes pratiques que je signale en passant dans le code, par acquis de conscience, ne sont pas connues, ou pas comprises, ou juste surprenantes pour les stagiaires. C’est l’occasion de me souvenir que dans tous les domaines, ce qui peut paraître évident et « connu de tous » ne l’est pas forcément, et qu’il est toujours bon de remettre en lumière des savoirs dont on imagine, souvent à tort, qu’ils sont monnaie courante.
Voici donc dix bonnes pratiques choisies parmi un large ensemble de candidates ; vous en connaîtrez sûrement certaines, mais probablement pas toutes. Je pensais qu’il en faudrait 10 pour faire un article un peu consistant, et au final c’est un mammouth qui aurait pu être découpé en plusieurs articles histoire de faire durer le plaisir… Mais je suis d’humeur généreuse et j’ai encore plein d’articles sous le coude, alors je vous livre tout ça d’un coup :-)
N’hésitez pas à faire vos retours en commentaires !
Court-circuiter le code, en JavaScript, c’est recourir à return
, break
ou continue
pour sauter tout ou partie du contexte en cours (fonction ou boucle), et s’épargner ainsi des blocs imbriqués inutiles, avec ce que ça suppose d’indentation supplémentaire…
Il est certes primordial de bien indenter son code, quel que soit le langage. Dès qu’on ne se limite pas à un code trivial de 2-3 lignes, visualiser les dépendances logiques des conditions, boucles et fonctions devient impératif.
Toutefois, il est dommage d’indenter inutilement. Trop d’indentations imbriquées crée souvent un a priori négatif sur le code en donnant une vision faussée de sa complexité. Cela rend aussi le code moins lisible et facile à appréhender.
Voyons quelques exemples.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Ici il est dommage d’avoir deux niveaux. Le cœur de code étant dans un if
, il suffit de tester la condition inverse et de court-circuiter avec un return
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Le même principe peut s’appliquer dans les boucles, en utilisant suivant les cas continue
ou break
. Ainsi, le code suivant :
1 2 3 4 5 6 7 |
|
…deviendrait celui-ci :
1 2 3 4 5 6 7 8 |
|
Au passage, vous ne le savez peut-être pas mais break
et continue
autorisent le recours à un label pour court-circuiter au-delà de leur boucle conteneur immédiate, et remonter ainsi de plusieurs niveaux (mais toujours au sein de la fonction courante). Voici un exemple de recherche dans une matrice, hors la diagonale. On ignore la diagonale en court-circuitant le tour courant avec un continue
simple, mais si on trouve la valeur recherchée, on court-circuite l’ensemble des boucles pour aller directement à la suite de la fonction, au moyen d’un label sur la boucle externe et de son emploi dans le break
interne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Enfin, il existe le fameux cas du « return else
», un cas classique de superflu. Voyez plutôt :
1 2 3 4 5 6 7 8 9 |
|
C’est parfaitement superflu, comme en atteste l’exemple plus haut : un return
court-circuite la fonction conteneur, le else
est donc sans intérêt : on peut utiliser directement le code qu’il contient.
Bref : l’indentation c’est bien, mais en faire trop c’est moins bien ! N’abusons pas des bonnes choses…
Qu’est-ce qu’une rvalue ? En jargon d’analyse syntaxique (parsing), on désigne ainsi l’opérande droit d’un opérateur. Par symétrie, l’opérande gauche est appelé lvalue. Par exemple, dans x == 42
, x
est la lvalue et 42
la rvalue. Dans str += 3
, ce seraient évidemment str
et 3
, respectivement.
Dans tous les langages où l’opérateur d’affectation (=
) est un sous-ensemble de celui de comparaison (==
), et où l’affectation est une expression, on risque de tomber dans le piège du égale manquant. Ainsi en JavaScript C, C++, Java, Ruby, etc. on peut se gaufrer comme suit :
1 2 3 4 |
|
Vous avez vu la blague ? Le =
au lieu du ==
? C’est un piège fréquent même pour les développeurs chevronnés : après tout, c’est « juste » une faute de frappe. Mais c’est tellement discret que ça peut être infernal à déboguer.
Si vous avez un bon outillage associé à votre éditeur de code, votre compilateur ou votre interpréteur, il vous fera probablement remarquer qu’il y a ici baleine sous gravier. C’est par exemple le cas de JSLint et JSHint, tous deux fournis par exemple dans Sublime Text 2 au moyen de l’excellent plugin SublimeLinter.
Vous avez alors le choix :
=
.Toutefois, le souci principal c’est que ça puisse arriver (ce code s’exécute, il est valide ; il fait juste n’importe quoi…) alors que vous visiez une comparaison. C’est pourquoi dans tous ces langages j’ai le réflexe d’inverser, pour les comparaisons d’égalité uniquement (==
et ===
, en JavaScript), la lvalue et la rvalue. Ça donnerait ceci :
1 2 3 4 |
|
Voyez-vous ce qui se passe ? Si j’oublie un =
, l’affectation résultante n’est pas valide : je ne peux pas modifier le litéral de gauche… Bien sûr, si je compare deux variables ou autres opérandes modifiables, ça ne m’aide pas. Et si j’opte normalement pour des égalités strictes (===
), le risque de n’avoir qu’un =
est bien moindre. Mais au global, c’est une habitude qui a sauvé un nombre incalculable de fois mes étudiants, stagiaires, collègues… et moi-même, naturellement.
Il existe de nombreux cas où une expression JS, aussi courte soit-elle, est lourde à exécuter.
Certaines sont intuitivement lourdes, comme le $(selector)
de jQuery (ce qui n’empêche pas les gens de faire quinze fois la même sélection dans leur fonction…) ou un appel à une de vos fonctions dont vous savez qu’elle mouline pas mal.
D’autres sont plus subtiles ou surprenantes, comme les litéraux d’expressions rationnelles ou les longueurs de tableaux.
Mettre en cache, ou si vous préférez memoizing ces expressions peut avoir une influence notable sur vos performances dans un code utilisé intensivement.
Commençons par les cas évidents :
1 2 3 4 5 6 7 |
|
Ce type de code est plus fréquent qu’on ne croie, tant les développeurs préfèrent copier-coller que refactoriser leur code, aussi simple cela soit-il ici. Naturellement, conserver l’objet jQuery résultat est une bien meilleure option (ou recourir au chaînage, quand c’est possible) :
1 2 3 4 5 6 7 |
|
Lorsque vous réalisez une fonction dont le résultat n’est pas censé changer pour une même série d’arguments, mais qui est susceptible d’être souvent appelée, il est souhaitable de recourir à la memoization pour cette fonction. Voyons un cas simple, sans arguments :
1 2 3 4 5 6 7 8 9 10 11 |
|
Si ça dépend des arguments, cached
peut devenir un hash ({}
) dont les clés sont des String
construites sur base des arguments. On peut même génériser tout ça facilement et en faire un mixin. La bibliothèque Underscore, parmi ses myriades de fonctions cool, propose ainsi _.memoize
à cet effet.
Plus subtil à présent : les expressions rationnelles.
Lorsqu’on recourt à une regex assez courte, il est fréquent de se contenter de la copier-coller, ou simplement de la laisser litérale au sein d’un boucle par exemple. C’est dommage, car alors on risque de la voir « compiler » à chaque utilisation, donc à chaque tour de boucle :
1 2 3 4 5 6 7 |
|
Il est préférable de sortir la regex de la boucle, voire de la fonction et d’en faire une « constante » (en ES3, on n’a pas const
, on utilisera donc var
) :
1 2 3 4 5 6 7 8 |
|
Cela a en outre le double avantage d’expliciter le sens de la regex et de faciliter la vie à certaines colorations syntaxiques :-)
Notez au passage une autre bonne pratique conjointe : lorsqu’on veut juste un résultat booléen sur la regex (correspondance ou non), sans s’intéresser au détail de la correspondance, on utilisera avec profit regex.test(string)
plutôt que string.match(regex)
. La première est nettement plus performante que la seconde, car elle renvoie juste un booléen et ne s’occupe pas de constituer des groupes, etc.
Dernier cas évoqué ici : les longueurs de tableaux.
Il est vain de vouloir deviner comment votre runtime JS représente en interne votre Array
JavaScript. Cela dépend de la runtime, des types de valeur stockées et de la longueur du tableau. Suivant les cas, il peut s’agir d’un véritable tableau (zone de mémoire continue à indexation directe), d’une liste liée, voire d’une structure plus avancée.
En conséquence, il n’est pas rare (surtout sur les vieux navigateurs) que le code en apparence bénin myArray.length
soit en réalité coûteux, exigeant en interne le parcours intégral du tableau. Une boucle « classique » sur la longueur aurait donc une complexité quadratique :
1 2 3 |
|
Dans la mesure où les boucles sur des tableaux dont la longueur change au fil des tours sont rarissimes (et sources de bien des bugs), il est toujours préférable de mettre la longueur (fixe) « en cache » dans une variable, au sein de la section d’initialisation de la boucle :
1 2 |
|
Naturellement, si vous passez par un itérateur (ceux d’ES5 : forEach
, map
, some
… ou ceux d’Underscore, de jQuery, etc.) vous pouvez leur faire confiance pour avoir fait au mieux.
On est là aussi dans les trucs rabâchés depuis bien 5 ans, mais je vois toujours plein de gens qui font le contraire, alors revenons-y.
Dans une page web, vous associez des gestionnaires d’événements (event handlers) à des événements sur divers endroits de la page. Le réflexe initial consiste à les associer « au plus près », sur l’élément lui-même en général.
La plupart du temps, ça ne pose aucun souci, c’est même une bonne idée.
Mais il y a au moins deux situations dans lesquelles c’est soit super lourd, soit carrément cassé.
Le premier cas, c’est lorsque vous écoutez le même événement sur pléthore d’éléments. Par exemple, tous les éléments d’une liste ; toutes les lignes d’un corps de tableau ; tous les liens porteurs d’une classe CSS donnée… Attacher un gestionnaire par élément, alors que le comportement est partagé, est inutilement lourd et peut vite dégrader les performances quand le nombre d’éléments est élevé.
1 2 3 4 5 |
|
On préfère alors tirer parti du bubbling (le fait que les événements « bouillonnent » au travers du DOM, d’un élément vers son parent et ainsi de suite jusqu’au document) et attacher le gestionnaire au plus proche commun parent (souvent le document, mais ça peut être bien plus ciblé, comme la liste qui contient tous les éléments souhaités). On aura alors recours à une syntaxe de capture appropriée. Par exemple :
1 2 3 4 5 |
|
(Dans les deux cas avec jQuery, this
fera bien référence au <li>
.)
L’autre cas de figure où une connexion trop ciblée des gestionnaires d’événement est problématique concerne le chargement/remplacement de contenus HTML (le plus souvent via Ajax). Si on attache ces gestionnaires sur une zone de la page qui va se faire remplacer par la suite (au moyen d’un appel manuel à .html(…)
ou $.load(…)
, par exemple), ces gestionnaires resteront attachés aux anciens éléments désormais disparus, alors que les nouveaux éléments fraîchement injectés ne disposeront pas, eux, de gestionnaires d’événements.
Il est alors préférable de déléguer ces gestionnaires au niveau du conteneur de la zone qui va évoluer (être remplacée/rechargée). jQuery assurant le bouillonnement même des événements qui, sur les vieux IE, ne bouillonnent pas (focus
, blur
, change
et submit
), cette solution est exploitable pour tous les événements voulus. Ainsi, nul besoin de les rattacher le moment venu, ou de nettoyer ceux installés avant le remplacement du contenu.
Encore aujourd’hui, en 2013, l’immense majorité des développeurs JS côté navigateur pondent leur code au niveau global. Imaginons qu’on vous demande de notifier Google Analytics de tout téléchargement de fichier sur votre site, sachant que vous avez pris soin de coller une classe CSS file
aux liens correspondants. Vous pourriez ajouter à votre page un script track_downloads.js
qui ressemblerait à ceci :
1 2 3 4 5 6 7 |
|
C’est un cas light, parce qu’en vérité, vos fichiers sont souvent multi-sujets, ou en tout cas « pourrissent le global » avec des tas de fonctions et variables partagées… (Si si, avouez, ça vous arrive, je le sais, je le vois tout le temps).
Outre que ça pose souci le jour où vous utilisez un autre morceau de code qui lui aussi aurait une fonction globale trackLink
(collision de noms : le dernier qui a parlé a raison), c’est juste dommage de polluer ainsi la portée globale pour rien. En effet, la majorité de vos codes sont en fait autonomes, ou comme disent les anglais self-contained : ils n’ont pas d’API publique à fournir, ils ont juste du code interne à leur fonctionnement, attaché à la page par gestionnaires d’événements.
Il est dès lors préférable d’enrober votre fichier dans un module. Je ne parle pas forcément d’un « vrai » module (au sens AMD ou CommonJS, par exemple), mais au minimum du module pattern, qui est au cœur des autres et repose simplement sur une fonction immédiatement exécutée (Immediately-Invoked Function Expression, ou IIFE, en anglais) :
1 2 3 4 5 6 7 8 9 |
|
Ici tout le code définit en fait une fonction anonyme, qu’il appelle immédiatement. En vertu des règles de portée et de closure de JavaScript, les déclarations de fonction et les var
à l’intérieur d’une fonction lui sont privées : l’extérieur ne sait donc rien, par exemple, de trackLink
.
J’utilise ici au passage des petites astuces habituelles quant aux arguments de l’IIFE (elle peut très bien ne pas en avoir) :
$
grâce à un argument dédié.undefined
en collant par exemple un undefined = 42;
quelque part, ça ne m’atteint pas car grâce à mon deuxième paramètre qui n’aura pas d’argument correspondant, dans mon « module », undefined
est bien undefined
(bon, ici on ne s’en sert pas, mais c’est un schéma désormais classique…).Si le sujet des module patterns et toutes les variations/évolutions possibles vous intéressent, cet article déjà ancien de Ben Cherry, développeur front-end chez Twitter, est une excellente lecture.
Le saviez-tu ? Ce n’est pas parce que du code externe a besoin de déclencher du code à toi que tu dois nécessairement en faire une API publique (c’est-à-dire des fonctions accessibles de l’extérieur). En fait, il existe notamment un cas de figure où une fonction publique n’est pas la bonne solution : la maintenance opérationnelle suite à un événement extérieur.
« Gni ? »
Imaginons que tu codes un module qui décore tout champ de type date avec un joli widget à toi (comment ça, tu n’utilises pas Kalendae ?!). Tu as respecté les préceptes du point précédent (module opaque décorant automatiquement les input[type=date], input.date
au chargement du DOM, par exemple), mais tu te retrouves soudain confronté à un problème : si quelqu’un met à jour la page plus tard (avec Ajax ou un système de templates…) les nouveaux champs ne seront pas décorés.
Ni une ni deux, tu « exportes » donc une méthode publique, probablement collée en global au niveau de window
ou d’un espace de noms dédié à ton module ; une méthode du genre refreshDatePickers()
. Peut-être que ton code ressemble à ça, exploitant ce que Ben Cherry appelle la loose augmentation :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Mais c’est dommage. On est ici dans le cas typique où :
Une solution plus discrète (dans le sens où elle ne te force pas à publier quelque API que ce soit) consisterait à utiliser un événement personnalisé, que tu déclencherais par exemple au niveau de la zone qui a changé (avec un bête .trigger
), et que ton module écouterait au niveau document. Ça donnerait quelque chose comme ça :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Dans l’idéal, tu « standardises » ces événements custom à travers toute la couche UI de ton code JavaScript, de sorte que ça devient une sorte d’habitude pour tous tes widgets internes, etc. En fait, ce n’est ni plus ni moins que du publish/subscribe (ou si tu préfères, observateur/observé)), mais avec le bouillonnement en plus, et donc la notion de zone DOM concernée.
Lorsqu’on écrit une masse minimum de code (à partir de quelques fonctions, surtout si elles sont publiques), il est toujours bénéfique de réfléchir à l’API design, c’est-à-dire à la conception générale de l’API ainsi produite. Qui utilisera ce code ? Comment ? Pour quels besoins ? Dans quels contextes ? Ce type de recul et de mise en perspective est hélas trop rare, même chez les designers d’API chevronnés, comme en attestent nombre d’exemples proéminents. Pour n’en citer que quelques-uns :
jQuery.each
, le seul itérateur à ma connaissance à coller l’index en premier plutôt que la valeur, source à lui seul de semaines-hommes entières perdues chaque jour à l’échelle mondiale…Net::HTTP
de Ruby. Prendre en charge HTTPS ou simplement traverser les redirections, deux besoins ultra-basiques, nécessitent quelques lignes de code non trivial.XMLHttpRequest
: outre sa casse discutable (XML en majuscules mais Http en casse Camel ?!), le cas de base Ajax nécessite une demi-douzaine de lignes de code, ce qui a immédiatement entraîné nombre de wrappers, au point qu’aujourd’hui peut-être un développeur front-end sur mille connait l’API d’origine…Bien concevoir une API change radicalement sa facilité de découverte et d’apprentissage, et le plaisir qu’on a à l’employer. Je ne saurais trop vous conseiller à cet égard l’excellente conférence de Jake Archibald à Paris Web 2010 : Reusable Code: For Good or For Awesome, qui regorge d’exemples concrets et d’humour ravageur.
Un des points les plus critiques du design d’une API reste pour moi la signature des fonctions/méthodes publiques. En particulier le passage à un hash d’options au lieu de paramètres positionnés dès lors qu’on dépasse, disons, 3 paramètres, ou que ceux-ci ont le même type de données attendu. Ainsi, on obtient un effet similaire aux paramètres nommés dans certains langages, ce qui fait que les appels à la fonction sont en quelque sorte « auto-documentés ».
Personne n’aime tomber sur du code de ce genre :
1 2 3 |
|
Quand on tombe sur ce code, que ce soit celui d’un autre ou le nôtre il y a 3 mois (ce qui revient au même), on se demande automatiquement « mais ?! Ça fait quoi ce true
? », et ce pour pratiquement chaque paramètre.
On préfèrerait tous tomber sur ceci :
1 2 3 |
|
Dans le même esprit, ce n’est pas parce qu’une fonction propose une trentaine d’options que je dois me les fader à chaque fois. De ce point de vue, les fonctionnalités Ajax de jQuery sont un excellent exemple de conception pratique : $.ajax
et ses aliases/wrappers, tels $.get
ou $(…).load
, sont tous conçus avec un hash d’options et des valeurs par défaut bien adaptées à la majorité des cas.
Mettre en place ce type de fonction ne nécessite pas forcément plus de travail que pour une fonction à la signature plus directe. Parfois, ça en nécessite même moins.
Avant d’écrire le code à l’intérieur d’une fonction à la signature non triviale, je vous encourage à la documenter immédiatement. Cash, juste au-dessus de la déclaration, dans un commentaire. Et surtout, surtout, mettez-y des exemples concrets d’appels. Ce n’est qu’en vous forçant ainsi à considérer les choses du point de vue « code appelant » (utilisateur, donc) que vous verrez rapidement si la signature que vous aviez initialement imaginée est adaptée ou doit être repensée. Et c’est à ce moment-là, alors qu’aucune ligne de code n’a été écrite encore dans la fonction, que le refactoring de la signature n’a aucun coût supplémentaire.
Ensuite, écrire une telle fonction repose sur quelques principes simples :
Prenons pour exemple une fonction callAjax
dans l’esprit de celle de jQuery. En documentant ses appels avant de l’implémenter, on isole plusieurs types d’appel afin d’être pratique à appeler pour tous :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Pour une signature polymorphe comme ici, on a plusieurs choix quant à la signature formelle (la liste des paramètres) de la fonction :
arguments
arguments
Nous allons opter ici pour la deuxième approche. L’idée est de normaliser l’appel vers sa version la plus complexe, donc avec un hash d’options. On commencerait donc comme ceci :
1 2 3 4 5 6 7 8 |
|
On gère ainsi les cas suivants :
options
est un objet vide ({}
)options.error
sera présent mais vaudra undefined
.Pour gérer les valeurs par défaut, il suffit de définir un conteneur pour toutes celles-ci et de fusionner dans options
toutes les clés absentes ou dont la valeur est undefined
. On documente en général les valeurs possibles pour chaque option en écrivant le conteneur. Un emplacement plutôt valable pour celui-ci est une propriété defaults
ou defaultOptions
sur la fonction :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Il est ainsi facile pour l’utilisateur de modifier globalement les valeurs par défaut. Si on souhaite interdire ça par mesure de sécurité/compatibilité, et qu’on est sur une runtime ES5, on peut toujours faire suivre la définition d’un Object.freeze(callAjax.defaults)
. Une autre approche consisterait à enfermer la fonction et ses défauts dans une closure et ne publier que la fonction.
Pour exploiter ces paramètres par défaut, rien de plus simple. Une fois options
normalisé dans la fonction, on procède ainsi :
1 2 3 4 |
|
En fait, ce type de schéma est si fréquent qu’il porte un nom usuel : extend
. On en trouve des implémentations globalement équivalentes dans la majorité des bibliothèques, de jQuery à Prototype en passant par Underscore. Du coup, le code plus fréquent pour ce type de besoin est le suivant :
1
|
|
Le reste du code de la fonction n’a plus qu’à exploiter les arguments fixes (url
) et le contenu d’options
.
En JavaScript il est fréquent de devoir construire petit à petit une chaîne de caractères massive ; il s’agit le plus souvent de HTML qu’on compose au fil de l’eau, à la main faute de mécanisme de templating. Vous savez, ce genre de code :
1 2 3 4 5 6 7 8 9 10 |
|
Ce genre de code est très peu performant. C’est comme en Java : une String
est non modifiable, et du coup +=
entraîne à chaque fois la création d’une nouvelle chaîne, ce qui implique qu’il faudra nettoyer la mémoire occupée par la précédente. Et plus c’est long, plus c’est lourd.
Tout comme on recommande en Java de recourir à StringBuffer
voire StringBuilder
, en JavaScript la meilleure approche consiste à enquiller les fragments dans un tableau, pour au final faire le join
qui regroupe tout ça :
1 2 3 4 5 6 7 8 9 |
|
Les performances n’ont rien à voir, surtout sur de grandes quantités. Et pour les petites, le code n’est pas plus compliqué, alors autant normaliser…
Il arrive de temps en temps qu’on ait fini de bosser sur un tableau et qu’on veuille repartir sur un « tableau neuf ». Parfois, ce sont de très gros tableaux, comme par exemple ceux qu’on utilise pour calculer un filtre sur les pixels d’un <canvas>
… On a alors tendance à faire :
1 2 3 4 |
|
En fait, cette affectation est sans surprise : elle envoie l’ancien tableau à la casse (il faudra en récupérer la mémoire) et crée un nouveau tableau, tout petit, qui devra donc lui aussi demander toujours davantage de mémoire au fil de son utilisation. C’est un peu dommage d’avoir basardé toute cette RAM alors qu’on en aura sans doute besoin juste après. Et jouer avec new Array(length)
ne change rien à l’affaire : ça ne préalloue pas la mémoire pour autant.
Mais surprise ! La propriété length
d’un tableau est en lecture/écriture. On peut donc s’en servir pour réutiliser un tableau existant et sa mémoire allouée, comme ceci :
1 2 3 4 |
|
Là aussi, les perfs obtenues sont largement meilleures pour les cas où les tableaux grandissent significativement au fil du temps.
Figurez-vous que parseInt
est un menteur doublé d’un salaud et que parseFloat
est juste la version pourrie de Number(…)
. Après cette phrase un brin raccoleuse (mais parfaitement véridique), je vous invite à consulter tous les détails dans l’article que je publiais fin décembre : Convertir un texte en nombre en JavaScript
Nos ateliers de formation JavaScript couvrent ce genre d’aspects explicitement ou à travers tous leurs codes, et beaucoup, beaucoup plus encore. Jetez-y un œil, en plus ils sont bien moins chers que la moyenne du marché !