Variables et constantes — Javascript 103

Travailler avec ES2015 c’est quelques changements sur les déclarations des variables.

Premier changement : Les modules ES2015 sont implicitement en mode strict. Y utiliser une variable non déclarée provoque une erreur. Ce seul changement est une bénédiction vu l’ubuesque comportement par défaut de Javascript. Il reste que ça ne changera pas grand chose pour qui utilisait déjà un outil d’analyse de code (linter).

Pour déclarer les variables nous avons aussi le nouveau mot clef let en complément de l’ancien var.  La portée est alors limitée au bloc de code parent le plus proche (if, while, function, for…) au lieu d’être étendue à toute la fonction parente.

let a = 2;
for(let b=1; b<3; b++) {
  let a = 3;
  a = 4;
}
console.log(a); // 2 et non 4

Au début j’étais enthousiasmé. J’ai trouvé exceptionnel de pouvoir travailler avec des variables jetables le temps de quelques lignes et j’ai pensé éviter de nombreuses réutilisations par erreur.

À l’usage c’est un ajout sympa mais pas si révolutionnaire. Si c’est pratique c’est surtout pour gérer les fermetures syntaxiques (closure) au sein des boucles. Le reste du temps ça n’a que peu d’influence quand on a des fonctions de taille et de niveau d’imbrication raisonnables.

var fns = [ ];
for(var i=1; i<3; i++) {
  fns.push( function () { console.log(i); } );
}
var fn = fns[0];
fn(); // affichera 3 et non 1

//----

fns = [ ];
for(let j=1; j<3; j++) {
  fns.push( function () { console.log(j); } );
}
fn = fns[0];
fn(); // affichera 1

Je me demande si ça pourrait être la portée par défaut dans un langage. Visiblement certains pensent que non mais je n’ai pas été convaincu par l’argumentaire.

L’autre nouvel arrivé c’est const. Déclarée ainsi la variable a la même portée qu’un let mais on ne peut pas y affecter une valeur différente.

Là aussi c’est pas mal d’enthousiasme. Une variable qui est modifiée sans avertissement par une fermeture lexicale ou un module tiers, ça fait parfois des dégâts.

Le problème c’est que ça ne protège pas vraiment de ça. Si on ne peut pas affecter de nouvelle valeur au même nom de variable, la valeur elle même n’est pas immuable. On peut toujours faire changer d’état un objet ou modifier les éléments d’un tableau. Dommage…

const tab = [ ];
tab[3] = 4; // ne provoque pas d'erreur mais change `tab`

const obj = { 
  var priv = 2; 
  this.chg = function() { priv = 3; };
};
obj.chg(); // `obj` vient de changer d'état silencieusement

tab = [1, 2, 3]; // là par contre on génère une erreur.

Pour obtenir une vraie sûreté il faut utiliser des des structures de données explicitement prévues pour. On ne peut plus utiliser les raccourcis habituels { } ou [ ] et l’instanciation devient bien plus verbeuse. Facile d’oublier par inattention et par habitude.

import immutable from "immutable";
const map = Immutable.Map({a:1, b:2, c:3});
// map ne changera plus jamais de valeur

Le problème c’est que tous les modules tiers continuent à utiliser les structures de données variables habituelles. Malheureusement des modules tiers, vue la pauvreté de la bibliothèque standard de Nodejs, on en utilise des tonnes,  y compris pour des fonctions de base.

Pour se protéger vraiment des changements d’état il faudra non seulement utiliser explicitement nos structures de données immuables (aie), mais en plus faire régulièrement des conversions quand on communique avec des modules tiers (ouch).

Il y a tout lieu de penser qu’on finira par avoir des données variables et d’autres immuables, suivant d’où elles viennent et comment elles sont déclarées. Il faudra réfléchir à chaque utilisation, parfois remonter à la création de la donnée. Possible même que le comportement de const avec les structures de données natives nous incite plus d’une fois à nous croire en sécurité alors que ce ne sera pas le cas.

Pour moi c’est le scénario du pire. Non seulement on complexifie le code (plus verbeux) et la charge cognitive (savoir à chaque fois quel est le type de variable et le type de données), mais en plus on garde des risques.

Pour jouer à ça il aurait fallu que le langage s’assure que ce qu’on déclare comme tel soit réellement immuable, jusqu’en profondeur. Je comprends très bien pourquoi ça aurait été difficile voire impossible, mais du coup ce const m’a l’air d’une vraie fausse bonne idée. Dommage…

Pour autant je me prends quand même à utiliser const pour les littéraux. Ça ne coûte pas grand chose et ça fixe par écrit l’intention que j’ai en tête. Même si je n’y suis pas obligé, je fais tout de même attention à garder let quand j’utilise un tableau ou un objet natif. J’ai trop peur d’induire en erreur le prochain développeur qui passe (d’autant que ce sera probablement moi).

Je jetterai peut-être de nouveau un œil à immutable.js si une partie significative des modules s’y convertit. Entre temps le ratio bénéfices/(risques+défauts) me parait assez faible.


Au delà de ces questions de déclarations, j’en tire un avis plutôt négatif sur la propagation des variables en Javascript. On fait des fermetures lexicales de partout et on ne maitrise pas bien les effets de bord. Pas étonnant que les développeurs cherchent à avoir des variables et des structures de données immuables ! Si la propagation des variables était plus saines au départ, ce besoin ne ferait pas surface ainsi.

var i = 1;
function hello10times() {
  for(i=1; i<10; i++) {
    console.log("hello ");
  }
}
hello10times();
// oups ! i a changé et vaut désormais 10

PHP est plus strict (ouch, dire ça me fait mal) : Les variables ne sont pas déclarées mais ne sont jamais héritées d’un contexte parent sauf à le déclarer explicitement, avec global pour les variables globales, ou use pour les fermetures lexicales. On peut toujours faire des effets de bord assez moche, mais on les voit venir assez facilement.

$i = 3;
$hello10times = function () {
  for (i=1; i<10; i++) {
    echo "hello ";
  }
}
// $i vaut toujours 3

$hello10times = function () use ($i) {
 for (i=1; i<10; i++) {
 echo "hello ";
 }
}
// $i vaut 10 mais là c'est forcément intentionnel

La visibilité des variables de Python est plus proche de ce que fait Javascript mais, sauf à le déclarer explicitement, si on utilise une variable d’un contexte parent, c’est en lecture seule. On garde des effets de bord, mais c’est déjà plus limité (bon, en échange savoir si la variable utilisée est locale ou héritée n’est pas toujours super explicite).


D’ailleurs, quitte à parler d’explicite, je trouve dommage d’avoir à déclarer mes variables en Javascript. Il n’y a pas vraiment le choix vu l’historique du langage mais ça reste agaçant vue la faible valeur ajoutée de la chose, surtout quand je vois avec Crystal que même un langage à typage statique peut s’en passer.

Si j’avais à créer un langage de zéro je pense que j’apprécierais de ne pas avoir à déclarer mes variables, éventuellement leur donner la portée par défaut de let, mais avec la déclaration explicite de PHP pour les fermetures lexicales (avec peut-être une exception pour les petites fonctions anonymes d’une unique ligne/expression).

L’idée de const me plait beaucoup, mais uniquement si le langage se révèle capable de réellement rendre la donnée totalement immuable, et ça en profondeur. À minima le freeze de Ruby serait une demie-solution (une demie seulement parce qu’il faut penser à faire un freeze récursif sur un clone de tous les objets avant leur affectation par const, ce qui est assez pénible). Celui de Javascript ne me semble pas efficace pour bloquer les changements d’état d’objets qui contiennent des propriétés privées (implémentées via des fermetures syntaxiques).

Tiens, ça pourrait même être intégré au langage : si l’affectation se fait avec := plutôt qu’avec =, alors ni la variable se voit affecter une copie immuable de la donnée, sans possibilité de modification ou ré-affectation.

Si vous développez le nouveau langage de demain, vous avez désormais mon cahier des charges :-)