image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

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 :

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 :

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 :

Il existe aussi des opérateurs spécifiques à JavaScript :

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 :

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 :

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 :

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 :

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 :

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) :

Des événements peuvent être déclenchés selon le cycle de vie d'un élément :

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 :

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) :

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 :

Informations sur un nœud

Un nœud comporte diverses propriétés que l'on peut consulter pour avoir des informations à son sujet :

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 :

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 :

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.