Types et structures de données JavaScript

Les langages de programmation disposent de structures de données natives. Selon les langages, les structures mises à disposition peuvent être différentes. Dans cet article, on listera les structures de données natives en JavaScript. On détaillera leurs propriétés et les façons de les utiliser voire de les combiner. Dans certains cas, on comparera ces structures avec celles d'autres langages.

Un typage dynamique

JavaScript est un langage dont le typage est faible et dynamique. Cela signifie qu'il n'est pas nécessaire de déclarer le type d'une variable avant de l'utiliser. Le type de la variable sera automatiquement déterminé lorsque le programme sera exécuté. Cela signifie également que la même variable pourra avoir différents types au cours de son existence :

js
let toto = 42; // toto est un nombre
toto = "truc"; // toto est désormais une chaîne de caractères
toto = true; // toto est désormais un booléen

Les types de données JavaScript

L'ensemble des types disponible en JavaScript se compose des valeurs primitives et des objets.

Les valeurs primitives

Tous les types, sauf les objets, définissent des valeurs immuables (qu'on ne peut modifier). Ainsi, contrairement au C, les chaînes de caractères sont immuables en JavaScript. Les valeurs immuables pour chacun de ces types sont appelées « valeurs primitives ».

Le type booléen

Un booléen représente le résultat d'une assertion logique et peut avoir deux valeurs : true (pour le vrai logique) et false (pour le faux logique) (voir Boolean pour plus de détails sur la représentation objet de ce type).

Le type nul

Le type nul ne possède qu'une valeur : null. Voir null et la page du glossaire pour plus d'informations.

Le type indéfini

Une variable à laquelle on n'a pas affecté de valeur vaudra undefined. Voir undefined et la page du glossaire pour plus d'informations.

Les types numériques

ECMAScript possède deux types numériques natifs : Number et BigInt, ainsi que la valeur spéciale NaN.

Le type nombre

Le type Number est géré pour représenter les nombres : les nombres flottants à précision double, représentés sur 64 bits, selon le format IEEE 754. Cette représentation permet de stocker des nombres décimaux entre 2^-1074 et 2^1024, mais ne permet de représenter des entiers de façon sûre qu'au sein de l'intervalle allant de -(2^53 − 1) à 2^53 − 1. Les valeurs en dehors de l'intervalle compris entre Number.MIN_VALUE et Number.MAX_VALUE sont automatiquement converties en +Infinity ou -Infinity, qui se comporteront de façon analogue à l'infini mathématique (voir la page sur Number.POSITIVE_INFINITY pour les détails et les quelques différences).

Note : Vous pouvez vérifier si un nombre est un nombre entier représentable de façon exacte avec une représentation en nombre flottant à double précision avec la méthode Number.isSafeInteger(). En dehors de l'intervalle entre Number.MIN_SAFE_INTEGER et Number.MAX_SAFE_INTEGER, JavaScript ne peut plus représenter un entier de façon exacte et ce sera une approximation avec un nombre flottant à double précision.

Pour le type Number, il n'y a qu'un seul nombre qui possède plusieurs représentations : 0 qui est représenté comme -0 et +0 (avec 0 étant un synonyme pour +0). En pratique, il n'y a presque pas de différences entre ces représentations et +0 === -0 vaut true. Toutefois, on pourra remarquer la nuance lors de la division par zéro :

js
> 42 / +0
Infinity
> 42 / -0
-Infinity

Dans la plupart des cas, un nombre représente sa propre valeur et JavaScript fournit des opérateurs binaires.

Note : Bien que les opérateurs binaires puissent être utilisés afin de représenter plusieurs valeurs booléennes avec un seul nombre en utilisant un masque de bits, c'est généralement une mauvaise pratique. En effet, JavaScript fournit d'autres moyens pour représenter un ensemble de valeurs booléennes comme les tableaux ou l'utilisation de propriétés nommées pour stocker ces valeurs. L'utilisation d'un masque de bit dégrade également la lisibilité, la clarté et la maintenabilité du code.

Il peut être nécessaire d'utiliser de telles techniques dans des environnements extrêmement contraints, pour gérer des limites de stockage local ou lorsque chaque bit transmis sur le réseau compte. Cette technique devrait uniquement être considérée comme dernière mesure pour réduire la taille.

Le type BigInt

Le type BigInt est un type numérique qui permet de représenter des entiers avec une précision arbitraire. Avec ce type, on peut donc manipuler des entiers plus grands que ceux représentables avec Number.

Pour créer un grand entier, on ajoutera un n après l'entier ou on appellera le constructeur BigInt.

On peut connaître la valeur la plus grande qui peut être incrémentée et représentée avec le type Number en utilisant la constante Number.MAX_SAFE_INTEGER. Avec les grands entiers, on peut manipuler des nombres qui vont au-delà de Number.MAX_SAFE_INTEGER.

Dans l'exemple qui suit, on voit le résultat obtenu lorsqu'on incrémente la valeur de Number.MAX_SAFE_INTEGER :

js
// BigInt
> const x = BigInt(Number.MAX_SAFE_INTEGER);
9007199254740991n
> x + 1n === x + 2n; // 9007199254740992n === 9007199254740993n
false

// Number
> Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // 9007199254740992 === 9007199254740992
true

À l'instar des nombres classiques, on peut utiliser les opérateurs +, *, -, ** et %. Un grand entier ne sera pas strictement égal à un nombre mais on pourra avoir une égalité faible.

Un grand entier se comportera comme un nombre lorsqu'il est converti en booléen avec if, ||, &&, Boolean et !.

Il n'est pas possible d'utiliser des grands entiers et des nombres de façon interchangeable. Une exception TypeError sera déclenchée en cas d'incompatibilité.

NaN

NaN (pour Not A Number en anglais, qui signifie « qui n'est pas un nombre ») est utilisée lorsque le résultat d'une opération arithmétique ne peut pas être exprimée comme un nombre. Il s'agit également de la seule valeur JavaScript qui n'est pas égale à elle-même (du fait de la norme IEEE 754).

Le type chaîne de caractères (String)

Ce type JavaScript est utilisé afin de représenter des données de texte. C'est un ensemble d'« éléments » de valeurs entières non-signées représentées sur 16 bits. Chaque élément occupe une position au sein de cette chaîne de caractères. Le premier élément est situé à l'indice 0, le deuxième à l'indice 1 et ainsi de suite. La longueur d'une chaîne de caractères correspond au nombre d'éléments qu'elle contient.

À la différence d'autres langages (comme le C), les chaînes de caractères JavaScript sont immuables. Cela signifie qu'une fois une chaîne créée, il est impossible de la modifier.

En revanche, il est toujours possible de créer une autre chaîne basée sur la première grâce à des opérations. Par exemple :

  • Un fragment de la chaîne originelle en sélectionnant certaines lettres ou en utilisant String.substr().
  • Une concaténation de deux chaînes de caractères en utilisant l'opérateur de concaténation (+) ou String.concat().

Attention à ne pas utiliser les chaînes pour tout et n'importe quoi !

Ça peut être tentant de vouloir utiliser des chaînes afin de représenter des données complexes. En revanche, les avantages de cette méthode ne sont que très superficiels :

  • On peut facilement construire des chaînes complexes grâce à la concaténation.
  • On peut déboguer rapidement le contenu des chaînes de caractères.
  • Les chaînes de caractères sont utilisées à de multiples endroits dans beaucoup d'API (champs de saisie, valeurs en stockage local, réponses XMLHttpRequest avec responseText, etc.).

En utilisant des conventions, il peut être possible de représenter n'importe quelle donnée sous forme d'une chaîne de caractères, en revanche cela n'est souvent pas la meilleure façon. Par exemple, avec un séparateur, on pourrait émuler le comportement d'un tableau en « interdisant » que ce séparateur soit utilisé pour éléments, etc. On pourrait ensuite définir un caractère d'échappement, qui serait à son tour inutilisable dans les chaînes : toutes ces pseudo-conventions entraîneront de lourdes conséquences en termes de maintenance.

En résumé, les chaînes doivent être utilisées pour les données de texte. Pour des données plus complexes, utilisez une abstraction adéquate et analysez/parsez les chaînes que vous recevez d'autres API.

Le type symbole

Un symbole est une valeur primitive unique et immuable pouvant être utilisée comme clé pour propriété d'un objet (voir ci-après). Dans d'autres langages de programmation, les symboles sont appelés atomes.

Pour plus de détails, voir les pages du glossaire et de Symbol JavaScript.

Les objets

En informatique, un objet est une valeur conservée en mémoire à laquelle on fait référence grâce à un identifiant.

Propriétés

En JavaScript, les objets peuvent être considérés comme des collections de propriétés. En utilisant un littéral objet, il est possible d'initialiser un ensemble limité de propriétés ; d'autres propriétés peuvent ensuite être ajoutées et/ou retirées. Les valeurs des propriétés peuvent être de n'importe quel type, y compris des objets. Cela permet de construire des structures de données complexes. Les propriétés sont identifiées grâce à une « clé ». Une clé peut être une chaîne de caractères ou un symbole.

Il existe deux types de propriétés qui ont certains attributs : des propriétés de données (data property) et des propriétés d'accesseur.

Note : Chaque propriété est décrite par des attributs correspondants. Ceux-ci sont utilisés par le moteur JavaScript et ne peuvent pas être manipulés depuis le code. Pour les identifier, les attributs sont indiqués entre double crochets.

Voir la page Object.defineProperty() pour en savoir plus.

Propriétés de données

Elles associent une clé avec une valeur et possèdent les attributs suivants :

Attributs d'une propriété de donnée
Attribut Type Description Valeur par défaut
[[Value]] N'importe quel type JavaScript La valeur obtenue lorsqu'on accède à la propriété. undefined
[[Writable]] Booléen Si cet attribut vaut false, l'attribut [[Value]] de la propriété ne pourra pas être changé. false
[[Enumerable]] Booléen

Si cet attribut vaut true, la propriété sera énumérée dans les boucles for…in. Voir aussi Rattachement et caractère énumérable des propriétés.

false
[[Configurable]] Booléen Si cet attribut vaut false, la propriété ne peut pas être supprimée, ne peut pas être changée en propriété d'accesseur et les attributs en dehors de [[Value]] et [[Writable]] ne pourront pas être changés. false
Attribut Type Description
Read-only Booléen État symétrique pour l'attribut ES5 [[Writable]].
DontEnum Booléen État symétrique pour l'attribut ES5 [[Enumerable]].
DontDelete Booléen État symétrique pour l'attribut ES5 [[Configurable]].

Propriétés d'accesseur

Ces propriétés associent une clé avec une ou deux fonctions accesseur et mutateur (respectivement get et set) qui permettent de récupérer ou d'enregistrer une valeur.

Note : Il est important de noter qu'on parle de propriété d'accesseur et pas de méthode. On peut donner des accesseurs semblables à ceux d'une classe à un objet en utilisant une fonction comme valeur d'une propriété mais ça ne fait pas de l'objet une classe.

Elles possèdent les attributs suivants :

Attribut Type Description Valeur par défaut
[[Get]] Un objet Function ou undefined La fonction qui est appelée sans argument afin de récupérer la valeur de la propriété quand on souhaite y accéder. Voir aussi la page sur get. undefined
[[Set]] Un objet Function ou undefined La fonction, appelée avec un argument qui contient la valeur qu'on souhaite affecter à la valeur et qui est exécutée à chaque fois qu'on souhaite modifier la valeur. Voir aussi la page sur set. undefined
[[Enumerable]] Booléen S'il vaut true, la propriété sera listée dans les boucles for…in. false
[[Configurable]] Booléen S'il vaut false, la propriété ne pourra pas être supprimée et ne pourra pas être transformée en une propriété de données. false

Les objets « normaux » et les fonctions

Un objet JavaScript est un ensemble de correspondances entre des clés et des valeurs. Les clés sont représentées par des chaînes ou des symboles (Symbol). Les valeurs peuvent être de n'importe quel type. Grâce à cela, les objets peuvent, naturellement, être utilisés comme tables de hachage.

Les fonctions sont des objets classiques à la seule différence qu'on peut les appeler.

Les dates

Lorsqu'on souhaite représenter des dates, il est tout indiqué d'utiliser le type utilitaire natif Date de JavaScript.

Les collections indexées : les tableaux (Arrays) et les tableaux typés (Typed Arrays)

Les tableaux (ou Arrays en anglais) sont des objets natifs qui permettent d'organiser des valeurs numérotées et qui ont une relation particulière avec la propriété length.

De plus, les tableaux héritent de Array.prototype qui permet de bénéficier de plusieurs méthodes pour manipuler les tableaux. Par exemple, indexOf() qui permet de rechercher une valeur dans le tableau ou push() qui permet d'ajouter un élément au tableau. Les tableaux sont donc indiqués quand on souhaite représenter des listes de valeurs ou d'objets.

Les tableaux typés (Typed Arrays en anglais) ont été ajoutés avec ECMAScript 2015 et offrent une vue sous forme d'un tableau pour manipuler des tampons de données binaires. Le tableau qui suit illustre les types de données équivalents en C :

Type Intervalle Taille (exprimée en octets) Description Type Web IDL Type équivalent en C
Int8Array -128 à 127 1 Entier signé en complément à deux sur 8 bits. byte int8_t
Uint8Array 0 à 255 1 Entier non signé sur 8 bits. octet uint8_t
Uint8ClampedArray 0 à 255 1 Entier non signé sur 8 bits (compris entre 0 et 255). octet uint8_t
Int16Array -32768 à 32767 2 Entier signé en complément à deux sur 16 bits. short int16_t
Uint16Array 0 à 65535 2 Entier non signé sur 16 bits. unsigned short uint16_t
Int32Array -2147483648 à 2147483647 4 Entier signé en complément à deux sur 32 bits. long int32_t
Uint32Array 0 à 4294967295 4 Entier non signé sur 32 bits. unsigned long uint32_t
Float32Array 1.2x10^-38 à 3.4x10^38 4 Nombre flottant sur 32 bits selon la représentation IEEE (7 chiffres significatifs). unrestricted float float
Float64Array 5.0x10^-324 à 1.8x10^308 8 Nombre flottant sur 64 bits selon la représentation IEEE (16 chiffres significatifs). unrestricted double double
BigInt64Array -2^63 à 2^63-1 8 Nombre entier signé sur 64 bits en complément à deux. bigint int64_t (signed long long)
BigUint64Array 0 à 2^64-1 8 Nombre entier non signé sur 64 bits. bigint uint64_t (unsigned long long)

Les collections avec clés : Map, Set, WeakMap, WeakSet

Ces structures de données utilisent des clés pour référencer des objets. Elles ont été introduites avec ECMAScript 2015. Set et WeakSet représentent des ensembles d'objets, Map et WeakMap associent une valeur à un objet.

Il est possible d'énumérer les valeurs contenues dans un objet Map mais pas dans un objet WeakMap. Les WeakMap quant à eux permettent certaines optimisations dans la gestion de la mémoire et le travail du ramasse-miettes.

Il est possible d'implémenter des objets Map et Set grâce à ECMAScript 5. Cependant, comme les objets ne peuvent pas être comparés (avec une relation d'ordre par exemple), la complexité obtenue pour rechercher un élément serait nécessairement linéaire. Les implémentations natives (y compris celle des WeakMap) permettent d'obtenir des performances logarithmiques voire constantes.

Généralement, si on voulait lier des données à un nœud DOM, on pouvait utiliser les attributs data-* ou définir les propriétés à un même l'objet. Malheureusement, cela rendait les données disponibles à n'importe quel script fonctionnant dans le même contexte. Les objets Map et WeakMap permettent de gérer plus simplement une liaison « privée » entre des données et un objet.

Les données structurées : JSON

JSON (JavaScript Object Notation) est un format d'échange de données léger, dérivé de JavaScript et utilisé par plusieurs langages de programmation. JSON permet ainsi de construire des structures de données universelles pouvant être échangées entre programmes.

Pour plus d'informations, voir la page du glossaire et la page sur JSON.

Les autres objets de la bibliothèque standard

JavaScript possède une bibliothèque standard d'objets natifs. Veuillez lire la référence pour en savoir plus sur ces objets.

Déterminer le type des objets grâce à l'opérateur typeof

L'opérateur typeof peut vous aider à déterminer le type d'une variable. Pour plus d'informations et sur les cas particuliers, voir la page de référence sur cet opérateur.

Voir aussi