10 bonnes pratiques JavaScript
Par Christophe Porteneuve • Publié le 21 janvier 2013
• 18 min
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 !
1. Court-circuiter les blocs plutôt que les imbriquer
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.
function processItems(list) {
var result = []
if (items.length > 0) {
normalize(items)
for (var index = 0, len = items.length; index < len; ++index) {
// code bien long…
}
}
return result
}
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
:
function processItems(list) {
var result = []
if (items.length <= 0) return result
normalize(items)
for (var index = 0, len = items.length; index < len; ++index) {
// code bien long…
}
return result
}
Le même principe peut s’appliquer dans les boucles, en utilisant suivant les cas continue
ou break
. Ainsi, le code suivant :
while (items.length > 0) {
var item = items.pop()
if (item.hasOwnProperty('taggable')) {
// code bien long…
}
}
…deviendrait celui-ci :
while (items.length > 0) {
var item = items.pop()
if (!item.hasOwnProperty('taggable')) continue
// code bien long…
}
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 :
function searchGridExceptDiagonal(grid, cell) {
var result
outer: for (
var rowIndex = 0, rows = grid.length;
rowIndex < rows;
++rowIndex
) {
for (
var colIndex = 0, cols = grid[colIndex].length;
colIndex < cols;
++colIndex
) {
console.log(rowIndex, colIndex)
if (rowIndex == colIndex) continue
if (cell === grid[rowIndex][colIndex]) {
result = [rowIndex, colIndex]
break outer
}
}
}
return processResult(result)
}
Enfin, il existe le fameux cas du « return else
», un cas classique de superflu. Voyez plutôt :
function processItems(items) {
if (items.length <= 0) {
return []
} else {
var result = []
// code bien long…
return result
}
}
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…
2. Comparer avec les rvalues à gauche
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 :
for (var index = 0, len = list.length; index < len; ++index)
if ((list[index] = 42)) {
// code de traitement…
}
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 :
- soit il s’agit bien d’une affectation à la volée (pour utilisation de la valeur dans le bloc, par exemple), et vous pouvez rendre ça explicite en doublant les parenthèses ;
- soit c’est une comparaison et vous rajoutez un
=
.
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 :
for (var index = 0, len = list.length; index < len; ++index)
if (42 == list[index]) {
// code de traitement…
}
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.
3. Mettre en cache les expressions lourdes
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 :
// BEUARK :
$('#foo .bar').on('click', handleBarClick)
if (escapesToo) $('#foo .bar').on('keypress', handleEscapes)
items.forEach(function (item) {
$('#foo .bar').append(item)
})
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) :
var scope = $('#foo .bar')
scope.on('click', handleBarClick)
if (escapesToo) scope.on('keypress', handleEscapes)
items.forEach(function (item) {
scope.append(item)
})
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 :
function heavyDuty() {
if (undefined !== heavyDuty.cached) return heavyDuty.cached
var result
// Gros code bien lourd et compliqué, voire coûteux financièrement (appels API, toussa…)
heavyDuty.cached = result
return result
}
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 :
// SAYMAL :
for (var index = 0, len = longArray.length; index < len; ++index) {
var line = longArray[index]
if (/[\s\u00a0][:;?!]/.test(line)) return line
}
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
) :
var WHITESPACE_AND_DOUBLE_SIGN = /[\s\u00a0][:;?!]/
for (var index = 0, len = longArray.length; index < len; ++index) {
var line = longArray[index]
if (WHITESPACE_AND_DOUBLE_SIGN.test(line)) return line
}
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 :
// RISKY
for (var index = 0; index < myArray.length; ++index)
// …
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 :
for (var index = 0, len = myArray.length; index < len; ++index)
// …
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.
4. Utiliser la délégation d’événements plutôt que des tas de gestionnaires étroits
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é.
// Un gestionnaire par élément (beuark) :
$('ul#files li').on('click', function (e) {
var item = $(this)
// …
})
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 :
// Un gestionnaire délégué pour tous les éléments :
$('ul#files').on('click', 'li', function (e) {
var item = $(this)
// …
})
(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.
5. Isoler son code avec le module pattern
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 :
function trackLink(link) {
_gaq.push(['_trackEvent', 'files', 'download', link.href])
}
$(document).on('click', 'a.file', function () {
trackLink(this)
})
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) :
;(function ($, undefined) {
function trackLink(link) {
_gaq.push(['_trackEvent', 'files', 'download', link.href])
}
$(document).on('click', 'a.file', function () {
trackLink(this)
})
})(jQuery)
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) :
- Au cas où jQuery serait utilisé en mode noConflict, je me permets tout de même d’y référer facilement avec
$
grâce à un argument dédié. - Si jamais un petit malin a pourri
undefined
en collant par exemple unundefined = 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 bienundefined
(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.
6. Découpler son code grâce aux événements personnalisés
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 :
var TotoWidgets = (function (exports) {
// ton joli code bien planqué ici…
function refreshDatePickers() {
// …
}
$(function () {
// ton init ici…
})
exports.refreshDatePickers = refreshDatePickers
return exports
})(TotoWidgets || {})
Mais c’est dommage. On est ici dans le cas typique où :
- ta méthode ne renvoie rien
- elle est en fait, conceptuellement, un gestionnaire d’événement… pour un événement « un morceau de la page a changé » qui n’existe pas (ou pas forcément)
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 :
// Dans ton module de widget :
;(function () {
var knownWidgets = {}
function initWidgets(e) {
var zone = $(e.target || document.body)
zone.find('input[type=date], input.date').each(function (i, element) {
element.id = element.id || _.uniqueId('datePicker')
if (knownWidgets[element.id]) return
knownWidgets[element.id] = new MyDatePickerWidget(element)
})
}
$(function () {
initWidgets()
})
$(document).on('pageupdate.ui', initWidgets)
})()
// Dans le code qui modifie un morceau de la page (ex. Ajax) :
$('ul#files').load('/files', function () {
$(this).trigger('pageupdate.ui')
})
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.
7. Proposer un hash d’options plutôt qu’une signature à rallonge
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…- L’API XSL/T du JDK ; le cas dominant (90% du besoin) étant l’application d’une feuille XSLT sur un document XML, cette situation requiert… une bonne douzaine de lignes de code.
- Dans le même esprit, le module
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…- Au bas mot la moitié de l’API standard PHP : entre les paramètres jamais placés dans l’ordre intuitif, les casses variables, les ordonnancements aléatoires dans les noms de fonctions, etc. c’est un des plus emblématiques foutoirs du développement contemporain.
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 :
Toolkit.initWidgets('container', true, false)
bindConnectors(25, '', 'foo', true, 1000)
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 :
Toolkit.initWidgets('container', { autoBind: true, hideNative: false })
bindConnectors({
interval: 25,
separator: '',
container: 'foo',
autoRefresh: true,
delay: 1000,
})
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 :
- passer à un hash d’options dès qu’on dépasse 3-4 paramètres, ou qu’on a au moins deux paramètres de même type (donc ambigüs à l’appel)
- fournir des valeurs par défaut clairement regroupées et explicitées pour toutes les options, voire pour certains paramètres.
- si on propose plusieurs signatures (de la plus courte pour les cas majoritaires à la plus longue pour un cas sur-mesure), normaliser vers la signature longue avant le reste du code, afin de n’écrire qu’une version de l’implémentation.
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 :
// Makes an Ajax call. This can be called in a very concise way, with just the endpoint
// URL, if you don't care about callbacks or behavior changes (the URL will be called through
// HTTP GET). You can also specify just a success callback, or a success and an error, or
// provide a full-blown options hash to fine-tune the behavior.
//
// Example calls:
//
// callAjax('/files')
// callAjax('/files', function success(res) { … })
// callAjax('/files', successCallback, errorCallback)
// callAjax('/files', {
// method: 'POST',
// dataType: 'json',
// success: function ajaxSuccess() { … },
// error: function ajaxError() { … }
// })
function callAjax() {}
Pour une signature polymorphe comme ici, on a plusieurs choix quant à la signature formelle (la liste des paramètres) de la fonction :
- ne rien mettre et tout traiter dynamiquement dans la fonction via
arguments
- ne poser que les paramètres invariables (ici l’URL) et gérer le reste via
arguments
- faire un mix des deux…
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 :
function callAjax(url) {
var options = arguments[1] || {}
if (_.isFunction(options)) {
options = { success: options, error: arguments[2] }
}
// …
}
On gère ainsi les cas suivants :
- pas de deuxième argument (ou deuxième argument falsy, ce qui n’est pas valide pour notre signature) :
options
est un objet vide ({}
) - le deuxième argument est une fonction : il s’agit alors du callback de succès, éventuellement suivi de celui d’erreur : on normalise vers un hash d’options avec les deux clés appropriées. Faute de troisième argument,
options.error
sera présent mais vaudraundefined
. - le deuxième argument est un hash d’options : on n’y touche pas.
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 :
callAjax.defaults = {
// Possible data types are 'html', 'json', 'js', 'jsonp' and 'xml'
dataType: 'html',
// Error callback. Called with the response text and XHR object.
error: $.noop,
// HTTP verb. If not 'GET' or 'POST', will emulate using a 'POST' and injecting
// a `_method` parameter with the passed verb.
method: 'GET',
// Success callback. Called with the response text (or parsed object according to
// `dataType`) and XHR object.
success: $.noop,
}
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 :
for (var opt in callAjax.defaults) {
if (_.isUndefined(options[opt])) options[opt] = callAjax.defaults[opt]
}
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 :
options = $.extend({}, callAjax.defaults, options)
Le reste du code de la fonction n’a plus qu’à exploiter les arguments fixes (url
) et le contenu d’options
.
8. Ne pas concaténer en masse les chaînes
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 :
// BEUARK
function buildHTML(names) {
var result = '<ul>'
for (var index = 0, len = names.length; index < len; ++index)
result += '<li>' + _.escape(names[index]) + '</li>'
result += '</ul>'
return result
}
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 :
function buildHTML(names) {
var result = ['<ul>']
for (var index = 0, len = names.length; index < len; ++index)
result.push('<li>' + _.escape(names[index]) + '</li>')
result.push('</ul>')
return result.join('') // ou join('\n')
}
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…
9. Réutiliser ses gros tableaux
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 :
// travail sur un tableau "arr" qui devient maousse…
// « remise à zéro » -- SAYBOF
arr = []
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 :
// travail sur un tableau "arr" qui devient maousse…
// « remise à zéro » -- La classe
arr.length = 0
Là aussi, les perfs obtenues sont largement meilleures pour les cas où les tableaux grandissent significativement au fil du temps.
10. Convertir les nombres correctement
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
Envie d’en savoir plus ?
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é !
- JS Puissant s’intéresse au langage lui-même, y compris pour des usages très avancés.
- JS Guru explore l’écosystème front-end en détail et permet la réalisation de bout-en-bout d’une Single-Page App ultra-moderne mettant en œuvre de façon intégrée tout plein de technos « HTML5 » à l’aide d’un outillage développeur de premier plan.
- JS Total reprend les deux autres et rajoute les problématiques de tests automatisés, documentation, optimisations tous azimuts et web mobile.