La philosophie de cette page est de rappeler les fonctionnalités essentielles du langage JavaScript sans entrer dans une description exhaustive.
Un peu d'histoire
JavaScript est né d'une collaboration en 1995 entre Netscape (éditeur de Netscape Navigator, navigateur web le plus populaire de l'époque, ancêtre de l'actuel Firefox) et Sun (créateur du langage Java) pour concevoir un langage de script utilisable depuis un client web. Contrairement à ce que l'on peut supposer de son nom, JavaScript est un langage comportant de nombreuses différences avec Java (à part la syntaxe de type C).
JavaScript a été standardisé en 1997 par l'organisme ECMA (norme ECMA-262) ce qui explique que ce langage soit aussi nommé ECMAScript.
JavaScript est aussi utilisé depuis ses origines pour du développement côté serveur :
- En 1995, Netscape Enterprise Server propose l'utilisation de JavaScript
- En 1996, Microsoft implante sa propre version de JavaScript nommée JScript et utilisée par le serveur web Internet Information Services (IIS)
- En 2006, Java 6 SE intègre le moteur JavaScript Rhino permettant d'interpréter du code JavaScript au sein de n'importe quelle application Java
- En 2009, l'utilisation côté serveur de JavaScript est popularisée par le framework Node.js qui repose sur le moteur JavaScript V8 (initialement développé pour le navigateur Chromium)
JavaScript est aussi employé par le framework Electron (basé sur le code de Chromium) utilisable pour la réalisation d'interfaces graphiques ; ce framework est notamment utilisé par l'éditeur Visual Studio Code.
Un langage à prototypes
JavaScript est essentiellement un langage impératif (i.e. utilisant des instructions avec effet de bord). Sa principale spécificité réside dans l'utilisation de prototypes pour la programmation orientée objet. Ce concept est différent de celui de classes utilisés par la majorité des langages orientés objet (tels que SmallTalk, Java et plus tard C# ou Scala). Le langage Self a été l'un des premiers à proposer l'utilisation de prototypes suivis ensuite par Lua, JavaScript, Lisaac...
Les langages utilisant des prototypes permettent à un objet d'hériter des membres d'un autre objet. Il est possible de créer un objet n'héritant pas d'un autre objet (prototype null) ou alors d'obtenir un objet B utilisant le prototype d'un objet A. Depuis ECMAScript 2015, il est possible de changer dynamiquement le prototype d'un objet (i.e. son objet ancêtre). Il existe désormais une syntaxe permettant de définir des classes sans que cela ne modifie le fonctionnement intrinsèque du langage (toujours basé sur des prototypes) : une classe est considérée comme une fonction crééant un objet.
Les objets JavaScript possèdent des membres qui sont appelés propriétés qui peuvent être des variables ou constantes ou alors des fonctions. Ces propriétés s'héritent par prototype. Il est possible d'accéder à l'objet courant auquel est rattaché une fonction avec this. Lorsque l'on accède à un membre d'un objet, on recherche sa présence dans l'objet puis s'il n'est pas trouvé à travers toute la chaîne des prototypes. La recherche d'un membre est donc moins optimisable que pour un langage à classes où les membres sont généralement statiquement définis à la compilation.
Pour en savoir plus sur les prototypes...
La boucle d'événements
La plupart des implantations de JavaScript utilisent une unique thread avec une boucle d'événements. Ces événements concernent des opérations survenant sur l'interface graphique (par exemple un clic sur un élément de l'arbre DOM d'une page HTML), des alarmes programmées avec setTimeout ou alors des événements réseau (résultat d'une requête HTTP par exemple).
Les tâches d'exécution longue (calculs, communications réseau...) ne peuvent être écrites de façon synchrone car elles risqueraient de bloquer la boucle d'événements de la thread principale. Il faut donc :
- Utiliser des APIs asynchrones pour certaines opérations (requêtes HTTP) en communiquant des fonctions de callback à appeler lorsque des événements surviennent
- Utiliser le mécanisme de Promise
- Employer des fonctions asynchrones (async, await)
Un langage intégrable dans des pages HTML
JavaScript est un langage de script qui a été originellement conçu pour interagir avec des pages HTML. Il est ainsi possible d'intégrer du code JavaScript dans une page HTML :
<script type="text/javascript"> let element = document.getElementById("myElement"); let clickNumber = 0; element.addEventListener("click", function(event) { clickNumber++; element.textContent = "Has been clicked " + clickNumber + " times"; }); </script>
Ce script permet d'associer un gestionnaire de clic à l'élément d'identifiant myElement. L'élément affiche le nombre de clics réalisés sur celui-ci.
Il est également possible d'inclure un script provenant d'un fichier externe :
<script src="clicktimes.js"></script>
La syntaxe du langage
JavaScript utilise une syntaxe de type C (comme Java ou C#). Voici les principaux points-clés à connaître sur sa syntaxe :
- Les blocs de code sont délimités par des accolades : { }
- Les blocs de commentaires sont délimités par /* ... */ ; des commentaires sur une ligne sont possible en les préfixant par //
-
Les boucles suivantes sont supportées :
- for (init; condition; post_inst) { ... }
- for (let v of iterable) { ... } : boucle permettant d'itérer sur un container itérable (tableau, Set...)
- for (let v in iterable) { ... } : boucle permettant d'itérer sur le nom des champs d'un objet ; avec un tableau, on obtient la séquence d'indice 1, 2, ..., n-1
- while (cond) { ... }
- do { ... } while (cond)
- La sortie d'une boucle est possible avec break ; le passage à l'itération suivante se fait avec continue
- L'exécution conditionnelle de code est possible avec if (cond) { ... } else { ... }
- La structure switch(expr) { case v1: ...; break; case v2: ...; break; ...; default: ...} est disponible
-
Les opérateurs classiques du C sont disponibles :
- les opérateurs arithmétiques (+, -, *, /, %, ++, --)
- les opérateurs de comparaison (==, !=, >, <, >=, <=)
- l'opérateur ternaire conditionel : condition?expr1:expr2 qui évalue à expr1 si la condition est vrai sinon à expr2
- les opérateurs booléens logiques (&&, ||, !)
- les opérateur binaires (&, |, ~, ^, <<, >>)
Il existe aussi des opérateurs spécifiques à JavaScript :
- === et !== différent des opérateurs d'égalité classiques : il s'agit d'opérateurs d'égalité strictes contrairement à == et != opérateurs d'égalité faibles. L'égalité faible peut réaliser des conversions préalables contrairement à l'égalité stricte échouant si les types des opérandes sont différents. Ainsi 1 == "1" est évalué à true tandis que 1 === "1" est évalué à false. La comparaison d'objets différents retourne toujours false. Ainsi ({a: 1}) == ({a: 1}) retourne false. Par contre deux variables référençant le même objet (créé une seule fois) sont considérées égales.
- >>> est un opérateur existant dans les langages ne proposant que des types signés (comme Java également) permettant de réaliser un décalage à droite de n bits, avec un remplissage par des zéros. Cela permet de considérer un nombre négatif comme une valeur non-signée : la valeur obtenue après décalage est positive. Ainsi -1 >>> 1 === 2147483647 alors que -1 >> 1 === -1. On constate que les opérateurs de décalage considèrent un nombre comme un entier sur 32 bits.
- L'opérateur ** (existant aussi en Python) permet d'élever un nombre à une certaine puissance
Les fonctions
Il est possible de déclarer une fonction globalement. Par exemple, nous pouvons écrire une fonction calculant une factorielle :
function fact(n) { if (n == 0) return 1; else return n * fact(n - 1); }
Nous pouvons aussi ajouter une fonction au prototype d'un objet existant :
function Person(name) { this.name = name; } Person.prototype.getName = function() { return this.name; } let person = new Person("foobar"); console.log(person.getName());
Nous constatons qu'il est possible de définir un objet par une fonction représentant son constructeur (this est une référence vers l'objet courant).
Il est courant également en JavaScript de définir des fonctions retournant une fonction. La fonction retournée capture les variables de son environnement qui lui sont accessibles (closure).
Ecrivons par exemple une fonction retournant une fonction multipliant un entier par v :
function createMultiplier(v) { return function(x) { return x * v; } } let mult2 = createMultiplier(2); console.log("42 * 2 = " + mult2(42));
Il existe également une notation raccourcie pour définir une fonction (fonction fléchée). On peut ainsi réécrire la fonction retournée par createMultiplier :
function createMultiplier(v) { return x => x * v; }
On peut continuer la réécriture en lambaifiant aussi createMultiplier :
const createMultiplier = v => x => x * v;
Une fonction fléchée ne possède pas de référence this : elle ne peut donc pas être utilisée directement comme constructeur d'un objet.
Une fonction peut posséder un rest parameter : ce paramètre peut accueillir un nombre variable d'arguments sous la forme d'un tableau. Voici un exemple avec une fonction retournant un tableau avec l'élément minimum et maximum des arguments (on fixe un premier argument pour être sûr que la fonction est appelée avec au moins un paramètre) :
function minmax(first, ...remaining) { let min = first; let max = first; for (let a of remaining) { if (a < min) min = a; if (a > max) max = a; } return [min, max]; }
ECMAScript 6 a introduit le concept de coroutines existant déjà dans certains langages. Une coroutine est une fonction suspendant on exécution à l'aide de l'instruction yield. Ainsi une coroutine peut implanter un itérateur, ici en générant la suite des factorielles :
function* generateFactSequence() { let value = 1; for (let i = 1; true; i++) { value *= i; yield value; } } let seq = generateFactSequence(); let fact1 = seq.next(); let fact2 = seq.next(); ...
Une coroutine peut aussi recevoir des valeurs (en passant en paramètre la valeur à next et en récupérant la valeur avec yield. Voici une coroutine multipliant les nombres qu'on lui envoie entre eux :
function* multiplyAll() { let result = 1; while (true) result *= yield result; } let m = multiplyAll(); console.log(m.next(2).value); // retourne 1 console.log(m.next(3).value); // retourne 2 console.log(m.next(5).value); // retourne 2*3=6 console.log(m.next(2).value); // retourne 6*5=30
☞ La méthode next d'une co-routine retourne un objet avec deux propriétés :
- value pour la valeur émise par le yield
- done, booléen indiquant si on est arrivé à la fin de l'itération (dans l'exemple précédent done est toujours false car nous utilisons une boucle while (true) sans break)
Les variables
Les variables peuvent être déclarées globalement ou à l'intérieur d'une fonction. Elles peuvent au besoin être capturées par closure.
On utilise principalement deux types de déclaration :
- const v = ... : permet de déclarer une constante non modifiable par la suite (cela peut toutefois désigner un objet mutable mais on ne pourra pas réassigner un autre objet)
- let v = ... : permet de déclarer une variable modifiable
Dans les deux cas, la portée est limitée au bloc courant (la portée peut être globale en cas de déclaration à l'extérieur d'une fonction). var permet aussi de déclarer une variable mais son usage est déprécié car souvent source de problèmes notamment avec les closures.
Depuis ECMAScript 2015, les affectations déstructurantes sur les tableaux et les objets sont supportées ; elles permettent d'affecter plusieurs variables individuelles en une seule instruction depuis un tableau et un objet. Voici quelques exemples :
// Destructuration sur un tableau let i = ["john", "doe']; // un tableau avec deux éléments let [firstName, lastName] = i; console.log(firstName); // John console.log(lastName); // Destructuration partielle let j = [1, 2, 3]; let [a, b] = j; // a=1, b=2 et on perd la troisième valeur // Utilisation d'une variable rest let k = [1, 2, 3]; let [c, ...d] = k; // c = 1 et d = [2, 3] (le reste du tableau) // Omission d'éléments du tableau let l = [1, 2, 3]; let [e, , g] = k; // e = 1, g = 3 et on perd la 2ème valeur omise // Destructuration sur un objet let knuth = { firstName: "Donald", lastName: "Knuth"}; let { firstName: knuthFN, lastName: knuthLN, birthYear: knuthBY = 0 } = knuth; console.log(knuthFN); console.log(knuthLN); console.log(knuthBY); // utilise la valeur par défaut 0 car birthYear n'a pas été trouvé dans l'objet knuth
Les types de données
En JavaScript, les types sont dynamiques : une variable ou champ d'objet n'est pas déclaré avec son type. Une variable modifiable peut être réassignée avec un objet d'un nouveau type. Il est possible de connaître le type d'une expression avec l'opérateur typeof.
Nous présentons ici les principaux types rencontrables :
let i = 10; let f = 10.0; console.log(typeof(i)); // number (les entiers et flottants utilisent un type commun number) console.log(i == f); // true let s = "text"; console.log(typeof(s)); // string let obj = {a: 1, b: 2, c: "some text"}; console.log(typeof(obj)); // object let a = [1,2,3, "foobar"]; console.log(typeof(a)); // object: un tableau est aussi considéré comme object let b = 1 == 1; console.log(typeof(b)); // boolean let n = null; console.log(typeof(n)); // null let u; // valeur sans affectation console.log(typeof(u)); // undefined
Ecrivons une fonction countOfType comptant le nombre d'éléments d'une structure étant d'un type donné. Par exemple countOfType("number", [1,2,"foo", {a: 4}]) retournera 3 car en explorant les structures de données, on trouve 3 nombres.
function countOfType(type, struct) { let count = 0; switch(typeof(struct)) { case "object": for (let value of Object.values(struct)) count += countOfType(type, value); break; default: if (typeof(struct) == type) count += 1; break; } return count; }
Les objets
Un objet peut être défini en JavaScript comme un dictionnaire associant à des clés des valeurs. On peut définir directement un objet ou alors passer par une fonction de construction.
let p1 = {name: "HAL", birthYear: 2001}; // Définition d'un constructeur d'objet Person function Person(name, birthYear) { this.name = name; this.birthYear = birthYear; } let p2 = new Person("HAL", 2001); Object.freeze(p2); // gèle l'objet qui devient immutable p2.name = "IBM"; // pas d'effet ni d'erreur console.log(p2.name); // toujours HAL car l'objet est figé p1["name"] = "IBM"; // changement de nom possible de p1 console.log(p1.name); // accès aussi possible avec la notation p1["name"] // Itérons sur les entrées (couples de clé-valeur) de l'objet for (let [key, value] of Object.entries(p1)) console.log(key + " => " + value);
En JavaScript, un objet peut être utilisé comme une table de hachage ; il existe aussi l'objet Map plus adapté à cet usage.
typeof(obj) retourne "object" quel que soit la nature de l'objet créé. Il est possible d'obtenir le nom de la fonction de construction de l'objet obj (ou de la classe) avec l'expression obj.constructor.name. Si nous reprenons l'exemple précédent, nous obtenons :
console.log(p1.constructor.name); // "Object" est le nom du constructeur des objets initialisés directement avec {...} console.log(p2.constructor.name); // "Person" correspond au nom de la fonction de construction
Nous pouvons aussi construire des objets en ajoutant des champs à d'autres objets par spreading (le spreading agit uniquement sur les champs et pas les méthodes) :
let city = {town: "Champs-sur-Marne", zip: "77420"}; let company = {name: "Boucherie Sanzo", ...city}; console.log(company);
Les tableaux
Un tableau contient une collection d'éléments (pas nécessairement de type homogène). Il est possible de rajouter des éléments au début (unshift) ou à la fin d'un tableau (push), voire de supprimer au début (shift) ou à la fin (pop) du tableau. La méthode splice permet d'insérer un élément dans le tableau. Un tableau peut être rempli avec la même valeur (fill). Son ordre peut être inversé avec la méthode reverse ; les éléments peuvent être triés avec la méthode sort.
Voici un exemple de manipulation de tableau :
let tab = [7, 78, 42, 37, 5, 9, 11, 2]; console.log(tab.length); // quelle est la longueur du tableau ? tab.unshift(1); // rajoute l'élément 1 au début tab.shift(1000); // rajoute l'élément 1000 à la fin tab.splice(0, 1, 45, 47); // remplace le premier élément du tableau en insérant 45 et 47 console.log(tab); tab.reverse(); // inverse l'ordre des éléments let oddTab = tab.filter(i => i % 2 == 1); // retourne un nouveau tableau avec que les éléments impairs console.log(oddTab); // calculons la somme des éléments du tableau const reducer = (accumulator, value) => accumulator + value; let sum = tab.reduce(reducer, 0); // on passe le reducer et la valeur initiale de l'accumulateur // copie du tableau entre deux indices 1 inclus et 3 exclus (soit 2 éléments) let extract = tab.slice(1, 3); console.log(extract); // trions les éléments du tableau en utilisant une copie let sorted = tab.slice(0, tab.length); // copie du tableau entier, on peut aussi utiliser tab.slice() sorted.sort(); console.log(sorted); // [11, 2, 37, 42, 5, 7, 78, 9] // le tri est réalisé en convertissant les éléments en chaînes et en triant ces chaînes par ordre lexicographique (en utilisant les codes Unicode) // ce tri n'est pas adapté pour les number : nous devons alors passant une fonction de comparaison en paramètre let sorted2 = [...tab]; sorted2.sort((a, b) => a - b); console.log(sorted2); // A quelle position est l'élément 42 dans le tableau trié ? console.log(sorted.indexOf(42)); // Si l'élément n'est pas trouvé la valeur -1 est retournée console.log(sorted.indexOf(789)); // Nous pouvons concaténer des tableaux par spreading let array1 = [1, 2, 3]; let array2 = [5, 6, 7]; let conc = [...array1, 4, ...array2]; console.log(conc); // Itérons maintenant sur le tableau for (let element of conc) console.log(element); // L'itération avec for ... in n'a pas d'intérêt car nous obtenons les indices des cellules for (let element in conc) console.log(element); // affiche 0, puis 1, puis 2...
Les chaînes de caractères
Une chaîne de caractère peut être exprimée littéralement entre guillemets ou apostrophes. Depuis ECMAScript 2015, des chaînes de type template avec substitution de variable existent : on les exprime entre backquotes. Des caractères spéciaux peuvent être indiqués précédés d'un caractère \ : \0 (caractère nul), \n (nouvelle ligne), \' (quote), \" (guillemet), \t (tabulation), \uXXXX (caractère unicode avec XXXX une valeur en hexadécimal)...
let name = "Foobar"; let helloMessage = `Hello World ${name}, your name contains ${name.length} characters`; console.log(helloMessage);
Il est possible de manipuler des chaînes de caractères en utilisant certaines méthodes :
let name = "Foobar"; let initial = name.charAt(0); // pour extraire un caractère console.log(`Initial: ${initial}`); let middle = name.slice(1, name.length-1); // pour extraire une sous-chaîne console.log(`Middle: ${middle}`); console.log(`toUpperCase: ${name.toUpperCase()}`); console.log(`toUpperCase: ${name.toLowerCase()}`); let content = " Hello World\n"; console.log(content.trim()); // Enlève les espaces au début et à la fin de la chaîne let buffalo = "buffalo "; console.log(buffalo.repeat(8).trim()); // repète 8 fois buffalo let cat = "Bob is a very nasty nasty cat"; let cat2 = cat.replace("nasty", "nice"); // remplace la 1ere occurrence nasty par nice console.log(cat2);
La plupart des langages de programmation disposent d'un support des expressions régulières : JavaScript n'échappe pas à la règle. Cela permet de rechercher des motifs dans des chaînes et éventuellement de réaliser des substitutions. La création d'une expression régulière s'effectue de deux façons :
- En indiquant le motif recherché entre caractères / : let regexp = /[0-9]+/g (pour recherche une succession de chiffres)
- En utilisant le constructeur de l'objet let regexp = new RegExp("[0-9]+", "g") ; cette deuxième façon s'impose si on construit dynamiquement à l'exécution l'expression régulière
L'option g indiquée lors de la construction de l'expression indique que nous nous intéressons à la recherche de toutes les occurrences et pas uniquement d'une seule.
On peut ensuite utiliser une expression régulière pour rechercher sur une chaîne. Voici des exemples :
let numberRegex = /[0-9]+/g; // succession de chiffres (plusieurs occurrences) let rhyme = "123 nous irons au bois, 456 cueillir des cerises, 789 dans mon panier neuf, 10, 11, 12, elles seront toutes rouges"; let pos = rhyme.search(numberRegex); // trouve la position du commencement de la 1ere occurrence trouvée par la regexp console.log(`First occurrence: ${pos}`); let pos2 = "pas de chiffre".search(numberRegex); console.log(`First occurrence in 2nd string: ${pos2}`); // retourne -1 si l'occurrence n'est pas trouvée let occurs = rhyme.match(numberRegex); // retourne un tableau avec toutes les occurrences trouvées console.log(occurs); let replaced = rhyme.replace(numberRegex, "number"); // remplace toutes les occurrences par la chaîne number console.log(replaced); let split = rhyme.split(numberRegex); // découpe une chaîne en utilisant l'expression régulière comme séparateur console.log(split); // Attention, match se comporte différemment si l'option g n'est pas utilisée // Dans ce cas, une seule occurrence est retournée // avec la possibilité d'exploiter des parenthèses capturantes // Ecrivons une expression cherchant une succession d'au moins un chiffre // en capturant séparemment le 1er et le 2ème chiffre et le reste du nombre let numberRegex2 = /([0-9])([0-9]?)([0-9]*)/; // au moins un chiffre, les chiffres suivants sont optionnels console.log("123 bois 456 cerises".match(numberRegex2)); console.log("1".match(numberRegex2)); // array[0] représente la totalité de l'occurrence trouvée, // array[1], array[2]... le contenu des parenthèses suivantes trouvées
La réalisation d'expressions régulières obéit à une certaine syntaxe : [...] permet d'indiquer un ensemble de caractères à rechercher, ? indique que le caractère (ou groupe) précédent est optionnel, * indique que l'élément précédent doit être trouvé en 0, 1 ou plusieurs exemplaires, avec + il faut au moins un exemplaire de l'élément précédent. Les caractères de signification spéciales ([, ], ?...) peuvent être recherché en les déspécialisant (en indiquant un anti-slash en préfixe). Par exemple, l'expression régulière recherchant tous les points d'interrogation d'un texte est la suivante : let questionMark = /\?/g.
A titre d'exercice, le lecteur pourra écrire une fonction count ajoutée au prototype de String comptant le nombre d'occurrences d'une expression régulière passée en paramètre.
Les nombres
En JavaScript, tous les nombres sont stockés sous la forme de flottants de longueur 64 bits. Ainsi 1 == 1.0 et même 1 === 1.0. Nous avons aussi 0.99999999999999999 === 1. Toute opération arithmétique est valide, sachant que certaines peuvent retourner un résultat Infinity ou NaN suivant la norme IEEE754 sur les flottants (par exemple "foo" / "bar" retourne NaN).
Lorsque des opérations binaires sont réalisées (décalages, ou, et binaire...), les nombres sont convertis en entiers 32 bits signés puis reconvertis en flottants 64 bits une fois l'opération réalisée.
L'objet Math contient des constantes (E, PI...) ainsi que des méthodes utiles pour la manipulation de nombres :
- Des fonctions trigonométriques ({,a}{cos,sin,tan}{,h})
- Des fonctions de conversion entières (ceil, floor, round)
- Des fonctions mathématiques diverses (sqrt, exp, log, pow)
- Les fonctions min et max qui permettent d'obtenir le minimum ou maximum d'un ensemble de nombres
- La fonction random retourne un flottant pseudo-aléatoire dans l'intervalle [0, 1[ (pour des applications non-cryptographiques)
Ainsi nous pouvons par exemple définir une nouvelle fonction membre de Math chargée de tirer un entier aléatoire dans l'intervalle [a, b] :
Math.getRandomInt = function(a, b) { let low = this.ceil(a); let high = this.floor(b); return this.floor(this.random() * (high - low + 1) + low); }
Lançons un dé un million de fois et comptons le nombre de fois que chaque face est obtenue :
let facesNumber = []; for (let i = 0; i < 6; i++) facesNumber.push(0); for (let i = 0; i < 1000000; i++) facesNumber[Math.getRandomInt(1, 6) - 1]++; console.log(facesNumber);
Les fonctions suivantes définies globalement peuvent être utiles :
- parseInt(s) permet de convertir une chaîne s en number entier
- parseFloat(s) convertit la chaîne en number flottant
- isNaN(n) permet de savoir si un nombre a un valeur NaN (nous avons toujours NaN != NaN et même NaN !== NaN)
- isFinite(n) permet de savoir si un nombre est fini (non infini et pas NaN)
- eval(s) permet d'évaluer une expression exprimée en code JavaScript (par exemple eval("10 / 2 + 3")). Son usage est à proscrire si les chaînes proviennent de sources externes car eval ne se contente pas d'évaluer des expressions arithmétiques mais peut potentiellement exécuter n'importe quel code JavaScript (ce qui présente un risque de sécurité).
Une proposition a été réalisée pour intégrer dans la prochaine version d'ECMAScript un nouveau type de base BigInt pour représenter des entiers de longueur arbitraire. Les navigateurs les plus récents supportent ce nouveau type : les entiers sont suffixés par le caractère n. Essayons par exemple de calculer 2 à une puissance élevée :
let i = 2 ** 1024; console.log(i); // Infinity (nombre trop grand pour être représenté sur un flottant de 64 bits) let j = 2n ** 1024n; console.log(j); // 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216n
Les interactions avec le document HTML (DOM)
JavaScript est essentiellement destiné à interagir avec l'arbre DOM d'une page HTML. Il est possible d'enregister un listener d'événement avec la méthode addEventListener appelée sur un élément de l'arbre DOM.
Il est possible de réagir aux événements de souris suivants sur des éléments de l'arbre DOM de la page (on appelle element.addEventListener("eventName", event => { ... })) ; la plupart des listeners de souris reçoivent un MouseEvent (avec champs offsetX et offsetY indiquant les coordonnées locales de l'événement) :
- onclick : clic gauche ; un clic est un événement composé de deux éléments primitifs onmousedown et onmouseup
- oncontextmenu : clic droit contextuel
- ondblclick : double clic
- onmouseenter, onmouseleave : entrée et sortie de la souris d'un élément
- onmouseover, onmouseout : entrée et sortie d'un élément ou l'un de ses enfants
- onmousemove : déplacement de la souris sur un élément
- onwheel : la roulette de la souris est utilisée (WheelEvent avec champs deltaX et deltaY)
- ontouchstart, ontouchmove, ontouchend : début, déplacement et fin d'un événement de toucher sur l'écran (applicable aux écrans tactiles) ; événement communiqué sous la forme d'un objet TouchEvent
Des événements peuvent être déclenchés selon le cycle de vie d'un élément :
- onload : lorsque l'élément est complétement chargé ; on enregistre généralement un listener sur onload de l'élément body de la page pour exécuter du code une fois que la page est complètement chargée
- onresize : cet événement est disponible uniquement sur l'objet global window et survient lorsque la fenêtre du navigateur est redimensionnée
- onunload : cet événement survient lorsque l'élément est supprimé (navigation vers une autre page)
- onerror : cet événement est appelé lorsque le chargement de données pour l'élément pose problème (pour un élément img, audio et video) ; cela est souvent dû au fait que la ressource à charger n'est pas accessible
- onscroll : cet évément disponible pour window est lié à un déplacement de l'ascenseur de la fenêtre
- ononline, onoffline : ces événements sont appelés sur l'élément lorsque le navigateur passe en mode en ligne ou hors ligne (perte de connectivité réseau).
L'objet global window
window est un objet représentant la fenêtre d'affichage du navigateur web. Il possède des champs permettant d'obtenir des informations sur la page affichée :
- document permet d'obtenir le document affiché (et ensuite de parcourir son arbre DOM)
- history représente l'historique de consultation de pages (la méthode go(-1) simule un retour, go(1) un forward)
- location indique l'adresse du document chargé
let handle = window.setTimeout(f, delay) permet d'injecter dans la boucle d'événements l'exécution de la fonction f après un délai spécifié en millisecondes. clearTimeout(handle) est la méthode antagoniste permettant d'annuler une fonction planifiée. Il existe aussi des fonctions window.setInterval et window.clearInterval s'utilisant de façon analogue afin de planifier une fonction s'exécutant de façon périodique (et de l'annuler). Si l'on souhaite planifier l'exécution d'une fonction pendant un temps libre de la thread principale, on utilisera plutôt window.requestIdleCallback.
window propose des méthodes affichant des boîtes de dialogue modales interrompant l'utilisateur (il ne faut pas en abuser car cela réduit la fluidité de la navigation) :
- window.alert("message") affiche une boîte avec la chaîne "message"
- let value = window.confirm("do you want to continue?") affiche une boîte posant une question à l'utilisateur qui a la possiblité de valider (value est true) ou d'annuler (value est false)
- let result = window.prompt("enter your birth year", "2000") demande à l'utilisateur son année de naissance avec une valeur par défaut affichée à 2000. result contient la valeur entrée ou null si le dialogue a été annulé
Voici un exemple implantant un compte à rebours avec des boîtes modales pour demander le temps et l'informer de la fin du décompte. Entre-temps, un élément <div> de la page HTML est mis à jour périodiquement avec l'indication du temps restant.
<html> <body> <h1>Countdown</h1> <div id="remainingTime"></div> <input type="button" id="startButton" value="Start the countdown" /> <script> /** Stored handles for setTimeout and setInterval functions */ let countdownEndHandle = null; let refreshHandle = null; /** Start the countdown for a given time in millis */ function startCountdown(t) { let startTime = performance.now(); // in millis let deadline = startTime + t; let remainingTimeDiv = document.getElementById("remainingTime"); refreshHandle = window.setInterval( () => { remainingTime.textContent = Math.floor((deadline - performance.now()) / 1000) + " s"; }, 500); countdownEndHandle = window.setTimeout(() => { window.clearInterval(refreshHandle); window.alert("End of countdown!"); }, t); } let startButton = document.getElementById("startButton"); startButton.addEventListener("click", event => { // cancel a possible previous countdown if (countdownEndHandle) { window.clearTimeout(countdownEndHandle); window.clearInterval(refreshHandle); countdownEndHandle = null; refreshHandle = null; } let time = window.prompt("Number of seconds for the countdown?", "60"); if (! isFinite(time / 1)) { if (window.confirm("Invalid time, do you want to try again?")) startButton.click(); // simulate a new click } else { // valid time, we start the countdown startCountdown(time * 1000); } }); </script> </body> </html>
Les manipulations de l'arbre DOM
Le code JavaScript d'une page HTNL a vocation de récupérer des informations sur l'arbre DOM de celle-ci et/ou de le modifier. Nous présentons ici les principales méthodes utiles pour cela.
Sélection d'éléments de l'arbre DOM
La racine du document HTML est accessible avec la variable globale document. A partir de document, il est possible :
- de récupérer un élément par son identifiant : document.getElementById("identifier")
- de récupérer un tableau d'éléments avec document.querySelectorAll(selector) ou une seule occurrence d'élément avec document.querySelector(selector) ; selector est une chaîne spécifiant un sélecteur CSS (par exemple div.important pour tous les éléments div de classe CSS important) ; ces méthodes peuvent aussi être appelées depuis un élément DOM interne pour restreindre la recherche au sous-arbre sous cet élément.
Informations sur un nœud
Un nœud comporte diverses propriétés que l'on peut consulter pour avoir des informations à son sujet :
- node.parentNode permet de connaître le nœud parent
- node.childNodes permet d'obtenir une collection des nœuds enfants
- node.previousSibling permet d'obtenir le nœud frère précédant le nœud
- node.nodeName retourne le nom du nœud (div, span, p, input...)
- node.textContent indique le texte à l'intérieur du nœud (cette propriété est modifiable)
- node.attributes retourne les attributs du nœud (il s'agit d'une map pouvant être modifiée)
Création d'un élément DOM
Il est possible de créer programmatiquement un nouvel élément DOM avec document.createElement ; par exemple ici nous crééons une liste d'éléments :
function createElementList(...items) { let root = document.createElement("ol"); for (let item of items) { let item = document.createElement("li"); // création d'un list item item.textContent = item; root.appendChild(item); } return root; }
Quelquefois cloner un nœud avec son sous-arbre est plus simple que de le recréer : on peut utiliser à cet effet la méthode cloneNode().
Il est possible aussi de créer un nouvel élément DOM de type texte avec la méthode document.createTextNode("Texte pour le nœud").
Plutôt que de créer un nœud ex-nihilo programmatiquement, il peut être intéressant d'intégrer le code à utiliser dans le document HTML et de le cloner sur demande. On utilise pour cela la balise <template> qui set à définir des modèles d'arbres à créer :
<template id="complexContent"> Very complex content... </template>
On peut ensuite cloner le contenu du template :
function createComplexContent() { return document.getElementById("complexContent").content.cloneNode(true); }
Modifications de l'arbre DOM
Différentes méthodes permettent de modifier l'arbre DOM à sa convenance :
- parent.appendChild(child) pour ajouter un nœud enfant à la fin des enfants de parent
- parent.insertBefore(child, previousSibling) pour ajouter un nœud enfant avant un nœud spécifié
- parent.removeChild(child) permet de supprimer un nœud enfant
- parent.replaceChild(newChild, oldChild) permet de remplacer un nœud enfant par un autre
Exemple : supprimer tous les enfants d'un nœud et les remplacer par d'autres nœuds (en ajoutant la fonction dans le prototype de Node) :
function replaceChildren(parentNode, ...newChildren) { while (parentNode.firstChild != null) parentNode.removeChild(parentNode.firstChild); for (let child of newChildren) parentNode.appendChild(child); } Node.prototype.replaceChildren = replaceChildren;
Les exceptions
Tout comme les langages C++ ou Java, JavaScript propose un mécanisme d'exceptions interrompant le flux d'exécution normal des instructions. Il est possible de lever une exception avec throw suivi d'une valeur (string, object...). Les exceptions peuvent être capturées dans un bloc catch ; un bloc finally est utilisable afin d'exécuter du code à la fin qu'il y ait exception ou non.
Il faut avoir à l'esprit que certaines opérations qui leveraient une exception pour certains langages ne le font pas en JavaScript. C'est le cas de toutes les opérations arithmétiques qui travaillent sur des flottants et peuvent retourner des valeurs NaN. L'accès à des champs inexistants d'un objet retourne la valeur undefined. document.getElementById("foo") retourne null si l'élément d'identifiant foo n'existe pas. L'accès à un champ de la valeur null provoquera la levée d'une exception. Écrivons par exemple une méthode modifiant le contenu d'un nœud DOM d'identifiant donné :
function modifyNodeContent(identifier, newContent) { try { document.getDocumentById(identifier).textContent = newContent; } catch (e) { console.log(`The element identified ${identifier} does not exist (raised exception: ${e})`); } finally { console.log("End of execution of the modifyNodeContent function"); } }
Il est toutefois souvent préférable de vérifier en amont les conditions susceptibles de lever une exception plutôt que d'utiliser un catch. Dans le cas exposé ici, il aurait été plus judicieux de vérifier si getDocumentById() retournait null.
Généralement lorsque l'interpréteur JavaScript lève une exception interne, un objet est créé avec un champ message explicatif. Le champ name de cet objet peut prendre les valeurs suivantes :
- EvalError si on fait appel à la fonction eval et qu'une erreur était présente dans l'expression à évaluer
- SyntaxError en cas d'erreur de syntaxe (oubli de fermeture de parenthèse ou d'accolade par exemple)
- TypeError en cas d'erreur de type (appel de méthode n'existant pas sur un objet par exemple)
- ReferenceError lorsqu'on tente d'accéder à une variable non-déclarée
On constate que la plupart de ces erreurs ne sont pas destinée à être capturées (le code doit être corrigé) et sont liées à la nature dynamique du langage JavaScript : ces erreurs auraient lieu à la compilation avec un langage à typage statique.
Dans certaines circonstances, il peut être utile de lever soi-même une erreur. Ecrivons par exemple une fonction retournant le nombre de jours dans un mois :
function getNumberfOfDays(year, month) { if (year < 1582) throw new Error("This function only supports the Gregorian area"); if (month < 1 || month > 12) throw new Error("Invalid month"); let leapYear = year % 4 == 0 && (year % 400 == 0 || year % 100 != 0); switch (month) { case 2: return leapYear?29:28; case 1: case 3: case 5: case 7: case 8: case 10: case 12: return 31; default: return 30; } }
Cette fonction vérifie si l'année est bien comprise dans l'ère du calendrier grégorien et le mois compris entre 1 et 12. Toutefois aucune vérification n'est faite pour savoir si year et month sont bien des nombres. Si ce n'est pas le cas, les opérations arithmétiques évalueront avec NaN ; aucune erreur ne sera retournée et il est probable que la fonction retourne 30 (cas default) si month est invalide. En tant que développeur, il serait trop fastidieux de pallier à l'absence de vérification de type de JavaScript. Il peut alors être intéressant d'utiliser un langage introduisant un système de typage statique tout en restant rétro-compatible avec JavaScript : TypeScript répond à cette problématique.