Déc 21, 2019 Javascript

Maîtriser Javascript : déstructuration, ensembles et dictionnaires

Après avoir un peu exploré Node.js et ses riches possibilités revenons-en au Javascript de base avec les nouvelles possibilités d’ES6 que nous n’avons pas encore abordées.

Le déstructuration

Avec Javascript nous avons deux types complexes : les tableaux et les objets. Ces deux types présentent des analogies et des différences. Du côté des objets on dispose de couples clé/valeur et on atteint une valeur avec la notation pointée, par exemple objet.cle1. Mais on peut aussi utiliser les crochets avec le même résultat : objet[‘cle1’]. Du côté des tableaux on doit se contenter de clés uniquement numériques et des crochets pour aller chercher les valeurs.

On peut évidement mélanger ces deux types :

const personne = {
  nom: 'Dupont',
  prenom: 'Alfred',
  mensurations: [180, 72] 
}

Pour aller chercher la première mensuration on va écrire :

personne.mensurations[0]

Quand on lit ce genre de code on se pose évidemment des questions quand à la structure de l’objet, quelle en est sa structure ?

On parle de structuration (quand on crée un objet) et de déstructuration (quand on extrait des valeurs d’un objet), et il apparaît un souci quand il y a un manque de symétrie entre les deux.

Dans mon exemple ci-dessus le code serait infiniment plus clair ainsi :

const taille = personne.mensurations[0]

ES6 nous offre de nouvelles possibilités bienvenues pour structurer et déstructurer.

Les tableaux

Prenons ce code :

const valeurs = [10, 50, 8]
const valeur1 = valeurs[0]
const valeur2 = valeurs[1]
const valeur3 = valeurs[2]
console.log(valeur1) // 10
console.log(valeur2) // 50
console.log(valeur3) // 8

Avec les nouvelles possibilités de déstructuration on peut écrire :

const valeurs = [10, 50, 8]
const [valeur1, valeur2, valeur3] = valeurs
console.log(valeur1) // 10
console.log(valeur2) // 50
console.log(valeur3) // 8

Pour déstructurer on utilise les crochets, ainsi on assigne de façon positionnelle des variables.

Que se passe-t-il s’il manque une valeur ?

const valeurs = [10, 50]
const [valeur1, valeur2, valeur3] = valeurs
console.log(valeur1) // 10
console.log(valeur2) // 50
console.log(valeur3) // undefined

Rien de grave, la variable est tout simplement undefined. Mais rien ne nous empêche de définir une valeur par défaut :

const valeurs = [10, 50]
const [valeur1, valeur2, valeur3 = 12] = valeurs
console.log(valeur1) // 10
console.log(valeur2) // 50
console.log(valeur3) // 12

On peut aussi utiliser le paramètre du reste qu’on a déjà vu :

const valeurs = [10, 50, 140]
const [valeur1, ...rest] = valeurs
console.log(valeur1) // 10
console.log(rest) // [50, 140]

On peut aussi déstructurer des tableaux imbriqués mais ça peut vite devenir confus :

const valeurs = [
  [10, 50, 140],
  [52, 28],
 ]
const [[valeur1], [,valeur2]] = valeurs
console.log(valeur1) // 10
console.log(valeur2) // 28

Les objets

Puisqu’on déstructure un tableau avec des crochets [] on va logiquement déstructurer un objet avec des accolades {}.

Prenons un exemple simple :

const identite = {nom: 'Durand', prenom: 'Pierre'}
const nom = identite.nom
const prenom = identite.prenom
console.log(nom)  // Durand
console.log(prenom)  // Pierre

Avec la déstructuration on peut écrire :

const identite = {nom: 'Durand', prenom: 'Pierre'}
const {nom, prenom} = identite;
console.log(nom)  // Durand
console.log(prenom)  // Pierre

Mais ça ne fonctionne que si on garde les mêmes noms que les propriétés :

const identite = {nom: 'Durand', prenom: 'Pierre'}
const {n, prenom} = identite;
console.log(n)  // undefined
console.log(prenom)  // Pierre

Sinon il faut donner le nom explicitement :

const identite = {nom: 'Durand', prenom: 'Pierre'}
const {nom: n, prenom} = identite;
console.log(n)  // Durand
console.log(prenom)  // Pierre

Ce qu’on a vu pour les tableaux reste en général valable pour les objets.

On peut par exemple extraire des objets imbriqués :

const personne = { 
  identite: {nom: 'Durand', prenom: 'Pierre'},
  age: 20
}
const {identite: {nom, prenom}, age} = personne
console.log(nom)  // Durand
console.log(prenom)  // Pierre
console.log(age)  // 20

On peut aussi déstructurer des objets et tableaux imbriqués :

const personne = { 
  identite: {nom: 'Durand', prenom: 'Pierre'},
  data: [180, 20]
}
const {identite: {nom, prenom}, data: [taille, age]} = personne
console.log(prenom)  // Pierre
console.log(age)  // 20

Les ensembles (set)

Un ensemble (set) est une collection de valeurs distinctes. On peut parcourir ces valeurs dans l’ordre et savoir si une valeur particulière est présente.

On crée un ensemble avec cette syntaxe :

const ensemble = new Set()

On peut gérer complètement cet ensemble :

const objet = {nom: 'Durand', prenom: 'Pierre'}
const ensemble = new Set()
ensemble.add('texte')
ensemble.add(12)
ensemble.add(false).add(objet)
console.log(ensemble.size) // 4
console.log(ensemble.has('texte')) // true
ensemble.delete(12)
console.log(ensemble.has(12))
console.log(ensemble.size) // 3
  • On ajoute avec add. On voit que add renvoie l’ensemble, on peut donc chaîner les ajouts
  • On obtient la taille avec size
  • On teste la présence d’une valeur avec has
  • On supprime un élément avec delete

On peut passer un itérable comme paramètre du constructeur :

const ensemble = new Set([1, 'texte', true])
console.log(ensemble.size) // 3
ensemble.forEach(e => console.log(e))
// 1
// "texte"
// true

On voit qu’on peut utiliser forEach pour parcourir les éléments. On voit donc qu’une ensemble se comporte comme un tableau, la seule différence c’est que clé et valeur sont fusionnées.

Comme un ensemble a des valeurs distinctes on peut dédoublonner un tableau, c’est une des raisons de l’existence des sets :

const tableau = [1, 2, 2, 3]
const ensemble = new Set(tableau)
console.log(ensemble.size) // 3
for (let item of ensemble) console.log(item)
// 1
// 2
// 3

On peut passe de Set à tableau et inversement facilement :

const ensemble = new Set([1, 2, 3])
console.log(ensemble.size) // 3
console.log([...ensemble]) // [1,2,3]

Par contre pour les fonctions ensemblistes on ne peut pas utiliser directement les opérateurs -, | ou & comme on le fait en Python. Il faut implémenter des fonctions.

Le test de présence d’un élément dans un ensemble est bien plus rapide que dans un tableau parce qu’un ensemble a une table de hash. J’ai fait ce petit test pour comparer la vélocité :

const a = [...Array(100000).keys()]
const ensemble = new Set(a)
console.time('test1')
ensemble.has(1000000)
console.timeEnd('test1')
 test1: 0.00390625ms

console.time('test2')
a.includes(1000000)
console.timeEnd('test2')
 test2: 0.16796875ms

On voit que la différence est énorme ! Avec un tableau il faut parcourir toutes les valeurs alors que dans un ensemble la fonction de hash calcule directement l’emplacement.

Il existe aussi les WeakSet qui sont comme les sets mais ne peuvent contenir que des objets. D’autre part la référence des objets est faible : les objets disparaissent avec l’ensemble et cet ensemble n’est pas énumérable.

Les dictionnaires (maps)

Le deuxième type de collection introduit par ES6 est l’objet Map. Contrairement aux ensembles on a là des clés et des valeurs qui peuvent être de n’importe quelle nature. On peut se demander si ça ne fait pas double usage avec les objets qui eux aussi on des données sous forme de clés et valeurs, mais on a les différences suivantes :

  • les clés des maps sont ordonnées,
  • les clés des objets sont toujours des chaînes de caractères alors que pour les maps on peut utiliser tous les types,
  • on dispose de size avec les map pour connaître le nombre d’éléments,
  • un map est directement itérable

On crée un dictionnaire avec cette syntaxe :

const map = new Map()

On peut ensuite l’utiliser :

const map = new Map()
map.set('string', 'une chaîne')
map.set(true, 'un booléen').set(11, 'un nombre')
console.log(map.size) // 3
console.log(map.has(11)) // true
console.log(map.get(11)) // "un nombre"
map.set(11, 'on écrase la valeur précédente')
console.log(map.get(11)) // "on écrase la valeur précédente"
map.delete('string')
console.log(map.has('string')) // false
  • On ajoute avec add. On voit que add renvoie l’ensemble, on peut donc chaîner les ajouts
  • On obtient la taille avec size
  • On teste la présence d’une valeur avec has
  • On récupère une valeur avec get sur la clé
  • On supprime un élément avec delete

Comme pour les ensemble on peut envoyer un itérable comme paramètres du constructeur :

const map = new Map([
  ['string', 'une chaîne'],
  [true, 'un booléen'],
  [11, 'un nombre']
]) 
const it1 = map.entries()
console.log(it1.next().value) // ["string", "une chaîne"]
const it2 = map.keys()
console.log(it2.next().value) // "string"
const it3 = map.values()
console.log(it3.next().value) // "une chaîne"
map.clear()
console.log(map.size) // 0

On voit aussi que les méthodes entries, keys et values renvoient un itérable. D’autre part clear permet de vider le dictionnaire.

On peut parcourir un dictionnaire avec for of :

const map = new Map([
  [1, 'un'],
  [2, 'deux'],
  [3, 'trois'],
  [4, 'quatre']
]) 
for (const [clé, valeur] of map) {
  console.log(clé + " = " + valeur);
}
// "1 = un"
// "2 = deux"
// "3 = trois"
// "4 = quatre"
for (const clé of map.keys()) {
  console.log(clé);
}
// 1
// 2
// 3
// 4
for (const clé of map.values()) {
  console.log(clé);
}
// "un"
// "deux"
// "trois"
// "quatre"

On peut évidemment aussi utiliser forEach.

On a vu qu’on peut transformer un tableau en dictionnaire mais on peut aussi faire l’inverse :

const map = new Map([
  [1, 'un'],
  [2, 'deux'],
  [3, 'trois']
]) 
console.log(Array.from(map))
// [[1, "un"], [2, "deux"], [3, "trois"]]

On peut fusionner deux dictionnaires :

const toto = new Map([
  ['voiture', 'Renault'],
  ['animal', 'chien'],
]);
const titi = new Map([
  ['animal', 'chat'],
  ['profession', 'coursier'],
]);
const union = new Map([...toto, ...titi])
union.forEach(function(valeur, clé) {
  console.log(clé + " = " + valeur)
})
// "voiture = Renault"
// "animal = chat"
// "profession = coursier"

On voit que le conflit pour l’animal est résolu en prenant le dernier qui vient écraser le premier.

Il est un peu dommage de ne pas disposer d’un littéral pour définir un dictionnaire. Pour les objets on a les accolades, pour la tableaux les crochets, mais pour les dictionnaires juste le constructeur.

Il existe aussi les WeakMap qui sont comme les maps mais les clés sont forcément  des objets avec une référence faible.

Conclusion

On a vu dans cet article de nouvelles possibilités intéressantes d’ES6. D’une part la déstructuration, d’autres part deux types qui viennent enrichir Javascript en passant outre certaines limitations des objets et tableaux.

Dans le prochain article je développerai la notion d’objet dans Javascript.

 

 

Laisser un commentaire