Les valeurs par défaut en ES2015+
Par Christophe Porteneuve • Publié le 16 févr. 2022

Mise à jour le 28 juin 2022, 21:48

Bienvenue dans notre sixième article de la série JS idiomatique.

Voici la fin de la « Sainte Trinité » ! Après la déstructuration et le rest/spread, voici enfin les valeurs par défaut !

Les valeurs par défaut existent dans la plupart des langages, et souvent bien avant que JavaScript les ajoute en 2015. Mais ça valait le coup d'attendre : les possibilités de la version JavaScript sont parfois plus avancées que dans les autres langages.

Tu préfères une vidéo ?

Si tu es du genre à préférer regarder que lire pour apprendre, on a pensé à toi :

Pourquoi des valeurs par défaut ?

On s'en sert pour formaliser les valeurs par défaut de données, et en limiter les cas d'application.

C'est utilisable partout où on a une affectation implicite, donc pour rappel :

  • les signatures de fonctions (les paramètres sont implicitement affectés par les valeurs des arguments de même position) ;
  • les déstructurations (dont les éléments sont implicitement affectés par les données issues de la source déstructurée).

L'opérateur est, on s'en doutait, =, et il n'est déclenché que pour undefined.

On faisait comment avant ?

C'est très différent de ce qui se passait avec la bidouille traditionnelle pour les valeurs par défaut, qui reposait le plus souvent sur le OU logique (||), et donc sur la coercition booléenne 😅 de la valeur d'origine :

// À l'ancienne… et pas top.
function repeat(term, times, separator) {
  times = times || 1
  separator = separator || '-'}

Dans cette ancienne approche, tout ce qui était falsy était ignoré au profit de la valeur par défaut. C'était un problème si certaines falsy values étaient légitimes, par exemple ci-dessus un times à zéro (0 || 1 vaudra 1, car 0 est falsy) ou un separator vide ('' est falsy).

Alors c'est vrai que des fois c'est pratique de gérer d'un même coup plusieurs valeurs jugées invalides (genre undefined et null, voire NaN), ce qui ne marchera pas avec la syntaxe des valeurs par défaut, mais le plus souvent, se concentrer sur undefined est approprié.

Lisibilité et complétion

Par ailleurs, un peu comme pour les rest positionnels en signature, utiliser les valeurs par défaut améliore le degré d'information apporté par la signature sur le comportement de la fonction, tout en allégeant le début de celle-ci :

// Mieux
function repeat(term, times = 1, separator = '-') {}

Cette approche a des avantages significatifs :

  • La signature est plus informative et explicite.
  • Les mécanismes de complétion / popup d'info sur arguments des éditeurs et EDI retranscrivent le plus souvent ces infos.
  • Le début de la fonction n'est pas encombré par le "massage des arguments", ne serait-ce que pour gérer leurs valeurs par défaut.

N'importe quelle expression, et même des backrefs !

À droite du signe égal, on peut avoir n'importe quelle expression JS, y compris avec des appels de fonction, ce qui est rare hors de JS. En attendant l'arrivée des throw expressions, certains s'en servent pour rendre un élément « obligatoire » en lui fournissant une valeur par défaut qui lève une exception :

function banner(title = required('title'), banner = '-'.repeat(42)) {}

// En utilisant ce type d'utilitaire :

function required(arg) {
  throw new SyntaxError(`${arg} is required`)
}

On peut même utiliser dans notre expression des termes présents plus à gauche dans la liste. Là aussi, peu de langages ont cette capacité dans leurs valeurs par défaut. Voici un exemple sympa :

function banner(title, line = '-'.repeat(title.length)) {}

Ici, le deuxième argument aura une valeur par défaut automatiquement alignée sur la longueur du texte dans le premier argument (bon, si on n'a pas de blagues Unicode, mais ça se corrige facilement). Si j'appelle juste banner('hello'), dans la fonction, l'argument line vaudra '-----' par défaut… Trop classe !

Un autre petit exemple que j'adore est lié à cette fois où j'avais une fonction qui extrayait les délimiteurs ouvrant et fermant d'un contenu balisé et me renvoyait un tableau ouvrant + fermant, à ceci près que si les délimiteurs n'étaient pas symétriques (comme le sont <>, (), [] et {}), le tableau ne contenait qu'un élément :

getSurroundingTags('<hello>') // => ['<', '>']
getSurroundingTags('[hello]') // => ['[', ']']
getSurroundingTags('@hello@') // => ['@']
getSurroundingTags('`hello`') // => ['`']

Mais à l'usage, j'avais parfois besoin de gérer les deux cas de façon unifiée, avec l'ouvrant et le fermant, même lorsqu'ils étaient identiques. La possibilité de recourir aux backrefs dans les valeurs par défaut me permet d'écrire ça de façon concise et très porteuse d'intention :

const [opener, closer = opener] = getSurroundingTags(text)

J'adore 😍 !

Bien concevoir sa signature

En mai 2020, dans notre série 19 pépites JavaScript, j'avais parlé de la bonne définition de paramètres nommés optionnels, et comme ça touche de près à la déstructuration nominative et aux valeurs par défaut, je vous en remets une couche ici.

Personne n'aime ce genre d'appels de fonction :

// Cas extrême mais *réel* (vieille API DOM Events)
// (D'ailleurs Prettier met ça vachement en évidence en passant à un
// argument par ligne, bim !)
initMouseEvent(
  'click',
  true,
  true,
  null,
  null,
  100,
  100,
  30,
  42,
  false,
  false,
  false,
  false,
  0,
  null
)

// Cas plus simple mais quand même déconcertant
new Node('Intro', true, true)

Les signatures avec plusieurs paramètres positionnels, non-intuitifs et utilisant le même type de donnée, sont imbitables à l'usage ; c'est un gros problème de maintenabilité. On préfère largement utiliser des paramètres nommés. JS n'a pas de syntaxe dédiée (contrairement à Ruby, Swift, Kotlin, PHP 8 ou C#, pour ne citer qu'eux), mais on s'en approche fort avec la déstructuration nominative :

class Node {
  constructor({ title, focusable = true, activable = true }) {}
}

// Définition (mono-ligne)
function banner({ text, bannerChar = '-', bannerSize = bannerChar.repeat(text.length) }) {}

// Définition (n'ayons pas peur d'être multi-ligne, Prettier le ferait souvent de toutes façons)
function banner({
  text,
  bannerChar = '-',
  bannerSize = bannerChar.repeat(text.length) }
}) {}

Du coup à l'appel c'est nettement plus sympa, ça « s’auto-documente » :

new Node({ title: 'Intro' })
new Node({ title: 'Intro', focusable: false })

banner({ text: 'hello', bannerSize: 42 })

C'est quand même nettement mieux !

Ceci dit… et si tous les arguments sont optionnels ? Soit parce qu'ils ont tous une valeur par défaut définie, soit parce que ceux qui n'en ont pas ne sont pas exigés de toutes façons ? En l'état, on a un problème. Regarde :

function formatDate({ date = new Date(), style = 'medium' }) {
  return date.toLocaleString('fr-FR', { dateStyle: style })
}

Outre que c'est pas hyper optimisé en cas d'appels fréquents (parce que ça ré-instancie sous le capot un Intl.DateTimeFormat à chaque appel), si je veux appeler cette fonction en mode « 100% par défaut », je fais quoi ? J'aimerais pouvoir faire ça :

formatDate() // => KABOOM! (TypeError: Cannot read properties of undefined (reading 'date'))

C'est parce qu'on essaie de déstructurer l'argument, et que faute d'en avoir passé un, on tente donc de déstructurer undefined, ce qui n'est pas autorisé. Je suis obligé de me fader ça :

formatDate({}) // Marche, mais 😪

Pour autoriser l'appel à vide, il me suffit de fournir une valeur par défaut à l'argument lui-même ! Regarde la fin de la signature ci-après :

function formatDate({ date = new Date(), style = 'medium' } = {}) {
  return date.toLocaleString('fr-FR', { dateStyle: style })
}

Ainsi, sans argument, on utilise un objet vide, et argument ou pas, on applique granulairement les valeurs par défaut, au sein de la déstructuration, pour chaque « paramètre nommé ». Cool 😎.

Ça t’a plu ? Ne manque pas la suite !

Il y a aussi encore tout plein de sujets merveilleux à venir dans cette série JS idiomatique.

Pour être sûr·e de ne rater aucun de nos tutos et articles, le mieux est encore de t’abonner à notre newsletter et à notre chaîne YouTube. Tu peux aussi nous suivre sur Twitter.

Et bien entendu, n'hésite pas à jeter un œil à nos formations ! Si explorer l'intégralité des recoins du langage t’intéresse, la ES Total notamment est faite pour toi !

Découvrez nos cours vidéo ! 🖥

Nos cours vidéo sont un complément idéal et bon marché à nos articles techniques et formations présentielles. Autour de Git, de JavaScript et d’autres sujets, retrouvez des contenus de très grande qualité à des prix abordables, spécialement conçus pour lever vos plus gros points de blocage.