Pourquoi "JavaScript c'est nul"

Voir la vidéo

Dans cette vidéo je vous propose de passer en revue les critiques faites à l'égart du JavaScript pour mieux les comprendre et voir si elles sont justifiées.

Sommaire

  • 00:18 La popularité
  • 01:57 Ça a été fait en 10 jours
  • 03:13 On est obligé de l'utiliser :(
  • 04:32 Runtime imprévisible
  • 06:02 Trop de modules
  • 07:00 La lourdeur de node_modules
  • 10:50 Accident left-pad
  • 12:28 Trop de librairies / frameworks
  • 14:14 Callback hell
  • 15:22 JavaScript est bizarre
  • 17:07 Pas de typage
  • 18:30 Injection de dépendance
  • 20:29 Single thread
  • 23:00 Conclusion

C'est populaire

La première raison, pas forcément la plus légitime, est la popularité du langage qui en fait une cible de choix pour récolter un maximum de likes avec un meme. Mais vu que plus de développeurs sont exposés au Javascript il y a aussi forcément plus de personnes qui n'en sont pas satisfaites et qui le critiqueront.

Il y a 2 types de langages, ceux que les gens critiquent et ceux que personne n'utilisent ― Bjarne Stroustrup

Cette citation résume assez bien le comportement des développeurs vis à vis des langages. Les langages plus niches, auront nécessairement des avis plus positifs car seul les personnes intéréssées par l'approche investiront dans le langage et vont avoir tendance à l'aimer. Mais lorsqu'un langage gagne en popularité certaines personnes peuvent se retrouver à l'utiliser sans affinités particulières et se mettront alors à être plus critiques.

JavaScript a été créé en 10 jours

Le JavaScript est un langage qui a un historique particulier car il a été créé dans un contexte particulier dans un temps réduit (le sous entendu est que le langage a donc été mal conçu à cause de cela). En réalité, la durée de création du langage n'a pas forcément d'incidence direct avec sa qualité :

  • Une technologie est créée en utilisant les avancées faites par les technologies déjà existantes, ce qui peut grandement améliorer l'efficacité dans la conception (un outil moderne peut donc être créé plus rapidement).
  • La création d'un langage consiste surtout à faire des choix / compromis qui vont avoir des avantages et des inconvénients et qui vont orienter le langage dans une direction ou une autre (sans pour autant le rendre "parfait").

L'argument de la durée est souvent utilisé par manque d'arguments car il permet souvent aux personnes qui l'utilisent de ne pas avoir à donner d'arguments plus constructifs...

C'est un langage obligatoire

Le problème du JavaScript est que c'est un langage incontournable dès lors qu'il s'agit de développer pour les navigateurs web. L'absence de choix s'avère forcément frustrant pour les développeurs qui ont l'habitude d'utiliser d'autres langages / paradigmes de programmation.

Même si ce sentiment est compréhensible, il ne serait pas raisonable d'attendre des navigateurs qu'ils supportent un grand nombre de langages (à la fois pour la maintenabilité et pour la rétrocompatibilité). La solution choisie ne pouvait de toute façon pas convenir à tout le monde.

Runtime imprévisible

L'autre problème, côté client, est que le JavaScript se retrouve déployé sur un environnement que l'on ne contrôle pas avec des contraintes variées. En réalité le langage "JavaScript" n'existe pas vraiment mais on se retrouve avec plusieurs interpréteurs qui se basent sur les spécifications de l'EcmaScript et qui gèrent leur propre version du langage.

Avec les autres langages de programmation, on a en général le contrôle de la cible sur laquelle on va déployer ou compiler. Cela permet de connaitre facilement ce que l'on peut faire avec le langage mais aussi d'avoir des langages qui évoluent plus rapidement car ils ne sont pas freinés par la rétrocompatibilité.

NPM la racine de tous les maux

Il y a trop de modules !

C'est surement l'un des arguments que vous entendrez le plus souvent à l'encontre du JavaScript. Même si ce n'est pas une critique direct du langage, mais de l'écosystème, elle reste intéréssante à analyser.

Dans le cadre du JavaScript on utilise npm pour gérer les dépendances et une particularité qui est assez spécifique est que l'on peut avoir deux paquets qui ont une même dépendance dans une version différente. Par exemple on peut utiliser une librairie A qui dépend de C en version 1.0 et une librairie B qui utilise C en version 2.0. Pour beaucoup de langages cette situation serait impossible et l'installation de A et B ne sera pas autorisée par le gestionnaire de paquet. Dans PHP par exemple tous les paquets sont installés à la racine du dossier vendor et ne sont pas dupliqués (car on ne peux pas avoir deux versions d'une même dépendance). Avec npm les choses sont différentes et chaque dossier peut avoir un sous dossier contenant les mêmes sous-dépendances.

/node_modules
  /a
    index.js
    package.json
    /node_modules
      /c
  /b
    index.js
    package.json
    /node_modules
      /c

Dans les premières versions, npm n'essayait pas d'applatir l'arborescence et on se retrouvait assez rapidement avec un dossier node_modules très lourd et des profondeurs énormes. Mais dans les versions plus récentes, si deux paquets ont une sous-dépendance commune avec une contrainte suffisante alors npm la placera à la racine afin d'éviter la duplication.

/node_modules
  /c
    index.js
    package.json
  /a
    index.js
    package.json
  /b
    index.js
    package.json

Le fait que les sous-dépendances ne créent pas de conflits peut être considéré comme un avantage mais aussi un inconvénient car un développeur de module aura tendance à utiliser plus de sous-dépendances car il ne sera pas obligé de gérer les conséquences des incompatibilités futures. Cette manière de gérer les dépendances limite la recherche d'interopabilité et freine l'évolution de l'écosystème.

Malgré tout, c'est à chacun de faire attention et d'éviter de sélectionner des librairies avec un trop grand nombre de sous-dépendances.

Il y a trop de librairies / frameworks

Vu que le langage est très populaire, l'écosystème l'est tout autant et JavaScript connait un flux constant de nouveaux frameworks / librairies (surtout côté front-end).

  • Besoin de valider des données ? il y a zod, io.ts, typebox, yup...
  • Pour les tests ? vitest, jappa, jest, mocha, ava...
  • Pour le front ? react, svelte, angular, vuejs, solidjs, preact...
  • Pour faire un petit serveur ? express, fastify, koa,

Vu de l'extérieur cela peut sembler déconcertant mais il faut faire la part des choses et remarquer certains points :

  • Parfois plusieurs versions d'un outil existent car il y a un outil destiné à l'écosystème NodeJS et un autre destiné à l'écosystème navigateur. NPM ne dispose pas d'un système d'organisation efficace pour différencier la cible d'une librairie...
  • L'évolution du langage permet de nouvelles choses et de nouvelles librairies permettent une utilisation plus moderne et viennent en réalité remplacer des méthodologies plus anciennes (utilisation des promesses par exemple).
  • L'apparition du TypeScript a créé un nouveau besoin et certaines librairies sont là pour apporter des fonctionnalités à ces nouveaux utilisateurs.

Le sentiment d'épuisement est compréhensible mais si vous vous intéressez un peu à l'écosystème vous réaliserez que chaque librairie apporte une approche différente et apporte sa pierre à l'édifice. Certaines approches intéressantes sont parfois ensuite adoptées par les autres librairies et cela permet à tout l'écosystème d'aller de l'avant. Mais même si c'est intéressant de suivre ces évolutions, il n'est absolument pas nécessaire de sauter sur chaque nouveau framework / librairie dès sa sortie. Si on regarde plus largement l'écosystème, il est plutôt stable pour les grosses librairies.

Le langage est surprenant

Le langage JavaScript a quelques particularités qui font de très bon meme.

La coercition

Certains exemples n'hésitent pas à montrer comment JavaScript est bizarre en mettant en avant la coercition.

[] + [] // ''
[] + {} // Object
{} + [] // 0
{} + {} // NaN

En réalité, ce sont des opérations qui n'ont pas de sens, et lorsqu'un langage faiblement typé essaie de convertir une variable d'un type à un autre c'est toujours un désastre... Pour cette raison (mais c'est aussi valable dans d'autres langages) l'utilisation du symbole == est à proscrire au profit du triple égal === qui sera plus strict et prévisible. On reviendra sur ce sujet lorsque l'on parlera des types.

Aussi certaines méthodes ont des signatures étranges, comme la fonction sort() qui réorganise alphabétiquement plutôt qu'en utilisant les valeurs numériques et qui en plus de ça change la variable originale.

[3, 10, 1].sort() // [1, 10, 3]

On a le même souci (voir pire) en PHP et c'est souvent lié à l'historique du langage et c'est un réel problème dans le développement car ces petites "surprises" sont une grosse source de bugs. Ces problèmes peuvent être mitigé avec des outils d'analyse et des documentations de types qui permettent aux éditeurs de voir les problèmes et de nous aider à les éviter (readonly pour indiquer qu'une variable n'est pas mutable par exemple). Avec l'expérience, on réalise vite que la plupart des langages possèdent des problèmes similaires et ont des méthodes "étranges" quelque part dans leur librairie standard.

Callback hell

C'est un argument un peu dépassé mais j'ai choisi d'en parler car on le rencontre encore aujourd'hui. Le callback hell désigne l'imbrication trop conséquente des fonctions dans le langages.

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

Cet inconvénient est beaucoup moins présent grâce à l'utilisation des promesses.

fs.readdir(source, function (err, files) {})
// En utilisant les promesses ce code devient
const files = await fsPromises.readdir(source)

De la même manière forEach peut souvent être remplacé par une boucle for..of.

widths.forEach(function (width) { })
// Deviendra
for (const width of widths) {

}

Aussi, si une personne vous parle du callback hell, il est fort propable qu'elle n'ait pas fait de JavaScript depuis un moment.

Single thread

Un autre problème avec le langage est le fait qu'il ne fonctionne que sur un seul thread. En effet, le côté asynchrone du langage repose sur un système de boucle d'évènement qui va permettre d'effectuer de l'I/O en tâche de fond mais le code JavaScript lui reste exécuté de manière synchrone. Ce côté asynchrone permet au langage de ne pas rester bloquer pendant l'accès à des systèmes externes mais n'est pas une solution parfaite.

  • Si vous avez une fonction "lente" écrite en JavaScript elle bloquera le fil principal et impactera votre application (là où d'autres langages comme go et rust permettent de créer un thread séparé).
  • L'asynchrone va s'immiscer dans beaucoup de votre code et cela peut parfois être contre-productif car un appel asynchrone introduit une latence supplémentaire.

Il faut cependant noter que NodeJS dispose d'un système de worker thread qui permet de créer un thread séparé avec lequel il sera ensuite possible de communiquer pour effectuer les tâches lourdes. Par contre, ce n'est pas aussi pratique à utiliser que dans des langages qui ont été pensés pour le multi-threading dès le départ. Côté navigateur les web workers permettent une utilisation similaire mais leur utilisation reste extrêmement complexe pour des besoins simples (à voir comment cela évolue).

Si votre projet consiste en des tâches intensives côté CPU le JavaScript n'est peut être pas le meilleur choix et il faudra regarder ailleurs. Être développeur ce n'est pas forcément être expert spécifiquement sur un langage mais c'est être capable de résoudre des problème précis et cela passe par l'utilisation de solutions adaptées aux problématiques auxquelles vous devez faire face. L'erreur ici est de vouloir utiliser un seul langage pour toutes les situations.

Pas de typage

Une autre critique du langage concerne l'absence de typage fort ce qui mène a de nombreux problèmes (lors de la coercition notamment) et à un code plus complexe à comprendre. La plupart des développeurs JavaScript seront d'accord avec cet argument et c'est pour cela qu'il existe le langage TypeScript qui permet d'ajouter les types au JavaScript. Cette approche n'est cependant pas parfaite.

  • On introduit un outil supplémentaire pour travailler avec le JavaScript.
  • La vérification de type ne se fait qu'à la compilation, le code éxécuté par le navigateur reste du JavaScript et des erreurs peuvent encore arriver suite à une mauvaise documentation des types.
  • Certaines situations sont difficiles à typer avec TypeScript et on se retrouve souvent avec des types complexes (qui donne des erreurs difficiles à déchiffrer).

Le problème du TypeScript est que l'on essaie d'ajouter un typage strict à un langage dynamique et ça ne fait pas forcément bon ménage. Malgré tout, le bénéfice du TypeScript est indéniable et il est très implanté dans le milieu professionel. Si le typage fort est un indispensable pour vous, et que vous avez le choix, il est peut être intéressant de regarder ce qui se fait ailleurs.

Injection de dépendances

Le dernier point est l'injection de dépendances. C'est une pratique que l'on retrouve dans de nombreux langages objets et qui permet de mieux séparer notre code. Le principe est d'être capable d'injecter un service lorsque l'on construit un objet.

<?php
class MaClass {

  private RendererInterface $renderer;

  function __construct(RendererInterface $renderer) {
      $this->renderer = $renderer;
  }
}

Cela permet d'avoir des classes qui ont des rôles bien définis et qui ne dépendent pas d'une implémentation spécifique (par exemple si on a besoin d'écrire un fichier, on utilise une interface et on injectera l'implémentation au besoin). Dans le cadre du JavaScript on ne peut pas utiliser d'approche automatisée car il n'y a pas de type (qui permettrait de déduire le service à utiliser) ni d'interface qui permet de définir le contrat attendu.

On peut par exemple définir les injections à faire manuellement dans un objet mais ce n'est pas idéal.

class FileSystem {}

class UserService {
  static containerInjections = {
    _constructor: [FileSystem],
  }

  constructor(fs) {
    this.fs = fs
  }
}

const service = await container.make(UserService)

Le TypeScript permet d'éviter le boilerplate à l'aide des décorateurs

@inject()
class UserService {
  constructor(private fs: FileSystem) {}
}

Mais l'utilisation d'interface pour l'injection n'est pas possible car les interfaces n'existent pas dans le code compilé. Si ce concept de programmation orienté objet est important pour vous vous serez forcément déçu.

Critiquer c'est bien, comprendre c'est mieux

Pour conclure, oui le JavaScript est loin d'être un langage parfait et certaines critiques sont justifiées. Mais les critiques aveugles n'apportent pas grand chose au débat et sont contre-productives car souvent répétées de manière automatique par les personnes qui les lisent. Si une personne vous dit qu'elle n'aime pas une technologie n'hésitez pas à demander plus de détails car les raisons peuvent vous apporter plus d'éclaircissements sur les contextes d'utilisation du langage.

Publié
Technologies utilisées
Auteur :
Grafikart
Partager