JavaScript, les bons éléments, façon 2020 : let, const, Map et Set

Les mots-clés let et const

Un besoin de block-scoping

On l’avait vu, en JavaScript, on déclarait des variables avec var. Celles-ci étaient function-scopées, c’est à dire qu’une déclaration était valide au niveau d’une fonction.

function test() {
  var hello = 'hello';

  if (hello) {
    var i = 0;
  }

  console.log(i); // 0 (i est déclaré !)
}

Si cette limite était acceptable quand JavaScript était un langage destiné à faire des scripts simples, elle pose bien plus problème en commençant à faire des applications complexes.

Toute variable déclarée dans une boucle par exemple, restait valide en dehors de celle-ci pouvant créer des effets de bord.

Pour contrer ça, on avait l’habitude de wrapper certaines parties du code dans des fonctions anonymes exécutées immédiatement (IIFE, pour Immediately Invoked Function Expression).

function hello() {
  var obj = { a: 3, b: 6 };

  // Key et value sont locales à cette boucle
  (function () {
    var key, value;
    for (key in obj) {
      value = obj[key];

      // do sth with value... 
    }
  })();
}

Deux mots clés pour gouverner var

Pour l’ES6, sorti officiellement en 2015, l’ECMA a standardisé deux nouveaux mots clés, let et const, pouvant déclarer une variable au niveau du bloc.

function test() {
  let hello = 'hello';

  if (hello) {
    let i = 0;
  }

  console.log(i); // ReferenceError: i is not declared
}

Cela résout également notre problème de variable locale à une boucle.

function hello() {
  let obj = { a: 3, b: 6 };

  for (let key in obj) {
    let value = obj[key];

    // key et value sont locales à cette boucle !
  }
}

Alors, quelle différence entre let et const ? const restreint l’affectation : une variable déclarée avec const ne peut être affectée qu’à l’initialisation. C’est dire qu’à aucun autre endroit qu’à l’initialisation de votre variable, vous ne pouvez écrire ma_const = *quelquechose*.

Contrairement à ce qu’on peut penser, const ne rend pas la valeur constante, elle signifie en fait que la variable ne peut pas changer de référence au cours de sa vie.

const ma_const = 3;
// TypeError
ma_const = 4;

const mon_obj = {};
// Ok ! mon_obj ne change pas de référence
mon_obj.hello = 4;

On prendra pour habitude d’utiliser const dès qu’on sait que la variable ne changera pas de valeur, par convention. let est utilisé uniquement quand on utilise des scalaires / variables allant muter (des nombres étant incrémentés, des string allant être modifiées, etc).

let et const sont des modifications simples et rapides à expliquer, mais elles changent fondamentalement le langage 🙂 !

Les nouvelles structures de données, Map et Set

Structures de données usuelles en programmation, les Map et les Set étaient, jusqu’à l’ES6 (2013-2015), inconnues au bataillon.

C koi ?

Map

Une Map est une structure de données associant une clé à une valeur. La structure, utilisant généralement une table de hashage pour stocker les clés, permet de retrouver une valeur rapidement étant donné une clé, et garantit qu’une clé ne puisse être dupliquée dans la structure.

L’opération d’accession (= retrouver la valeur associée à une clé) est généralement d’une complexité approchant la complexité constante : ce qui signifie que peu importe le nombre de clés, la structure mettra le même temps à retrouver votre valeur étant donné une clé donnée.

En Python, un équivalent serait le dict :

un_objet = open('file.txt', 'r')

### La map est ici
# On peut stocker n'importe quoi en clé comme en valeur
my_dict = {
  'key': 'value',
  3: 'hello',
  un_objet: 'valeur associée à l\'objet'
}

Cette structure existe vraiment partout, même dans la STL du C++ :

#include <map>
#include <string>

std::map<int, std::string> number_to_string;
number_to_string[3] = "hello"s;
number_to_string[18] = "eighteen"s;

Set

Un Set est une structure de données permettant de stocker un nombre illimités de valeurs toutes distinctes entre elles.

Le principe d’un set est de garantir l’unicité des valeurs, et de pouvoir tester très rapidement si une valeur est présente ou non (à complexité quasi-constante, idéalement).

De la même manière, un tel objet existait déjà en Python :

allowed_numbers = { 1, 3, 5, 17 }

if 3 in allowed_numbers:
  print('3 is allowed !') # S'affichera

if 18 in allowed_numbers:
  print('18 is also allowed') # Ne s'affichera pas

Comment on faisait, avant ?

En JavaScript, nous avions néanmoins la possibilité de simuler ces deux structures de données pour les string uniquement en utilisant les objets. En effet, les objets de JS sont des conteneurs associant clé => valeur, avec une condition : la clé doit être une string.

L’usage faisait que les objets natifs de JS étaient utilisés en tant que Map ou Set basique.

Voici un example très simple qui encapsulait un objet dans une "classe" Map avant l’ES6 :

// On construit notre map très basique
function Map() {
  this.content = {};
}

Map.prototype.get = function (key) {
  return this.content[key];
};

Map.prototype.set = function (key, value) {
  return this.content[key] = value;
};

Map.prototype.has = function (key) {
  return this.content.hasOwnProperty(key);
};

Map.prototype.delete = function (key) {
  delete this.content[key];
};

Map.prototype.keys = function () {
  var key = '', keys = [];

  for (key in this.content) {
    if (this.has(key)) {
      keys.push(key);
    }
  }

  return keys;
};

Map.prototype.values = function () {
  var key = '', values = [];

  for (key in this.content) {
    if (this.has(key)) {
      values.push(this.get(key));
    }
  }

  return values;
};

Map.prototype.size() = function () {
  var i = 0, key = '';

  for (key in this.content) {
    if (this.content.hasOwnProperty(key)) {
      i++;
    }
  }

  return i;
};

// On se sert de notre map !
var map = new Map();

map.set('hello', 'world');

map.has('hello'); // true
map.get('hello'); // 'world'
map.keys(); // ['hello']
map.values(); // ['world']
map.size(); // 1

Et maintenant, une encapsulation d’un Set qui utilise notre Map :

function Set() {
  this.content = {};
}

Set.prototype.keys = Map.prototype.keys;
Set.prototype.size = Map.prototype.size;
Set.prototype.has = Map.prototype.has;
Set.prototype.delete = Map.prototype.delete;

Set.prototype.add = function (key) {
  this.content[key] = null;
};

// On peut se servir du Set !
var set = new Set();

set.add('hello');
set.has('hello'); // true
// ...

Ces deux types de structures restent néanmois très limités. Pour la Map, les clés sont limités à des string, tandis que pour le Set, les valeurs stockées sont elles aussi limitées au string.

Alors, pour cela, Map et Set natifs à la rescousse !

Les nouveaux Map et Set

Les objets plus haut disposent volontairement de la même API que les nouveaux objets natifs, à la différence près de .keys() et .values().

Les clés / valeurs peuvent être de n’importe quel type dans les nouveaux objets, cependant, lorsqu’on utilise des non-primitives, la vérification de l’existance se réalise à l’aide de la référence de l’objet. Explications.

// Les nouvelles Map ES6
const map = new Map();

map.set('hello', 'world');

const obj1 = { a: 3 };
const obj2 = { a: 3 };

obj1 === obj2; // false, même contenu mais références différentes

map.set(obj1, 'b');
map.get(obj1); // 'b', ça fonctionne !
map.get(obj2); // undefined, obj2 n'est lié à rien

// Les sets fonctionnent comme les maps, mais sans les valeurs
const unique = new Set();
unique.add(obj1);
unique.has(obj1); // true
unique.has(obj2); // false

// Il est possible d'itérer les clés/valeurs
// Ce sont des itérateurs, des objets qui seront abordés un peu plus tard
const values = unique.values();
let current = values.next();

while (!current.done) {
  const value = current.value;

  // Do something with value...

  current = values.next();
}

Malheureusement, il n’est pas possible de personnaliser la manière dont les objets sont comparés pour assurer leur unicité. Les primitives (+ les string) sont comparées par valeur, tandis que les objets sont comparés par référence. Il n’y a pas de système de hash, comme en Java ou Python par exemple.

À la fin de ce résumé, il y aura un petit bonus où l’on construira ensemble une Map + Set capable d’utiliser des hash définis par des utilisateurs sur des objets :).

La fin

Cela en est fini pour cette partie ! Dans la suivante, on discute des nombreuses nouveautés autour des objets, la réelle primitive de JavaScript, apportées depuis 2008. Jetez-y un coup d’oeil !

Article précédent : ModulesArticle suivant : Des objets plus puissants

Laisser un commentaire