Qu’est-ce que le DOM ?
Lorsque le navigateur lit votre HTML, il ne conserve pas une simple chaîne de texte. Il construit un arbre d’objets appelé le Document Object Model. Chaque élément devient un nœud avec des propriétés et des méthodes, chaque attribut est accessible et chaque fragment de texte a sa place. Le DOM est cet arbre, et il est dynamique : modifiez-le et la page se met à jour.
C’est pourquoi JavaScript et HTML semblent connectés. Votre HTML décrit la structure, le navigateur la transforme en objets, et JavaScript manipule ces objets. L’objet document est le point d’entrée de tout l’arbre.
L’arbre de nœuds
Le DOM est hiérarchique. Le document est la racine, <html> est son enfant, et tout le reste est suspendu en dessous selon une relation parent-enfant. Un nœud peut avoir plusieurs enfants, mais un seul parent.
document
└── html
├── head
│ └── title
└── body
├── h1
└── p
Il existe plusieurs types de nœuds. Les nœuds d’élément représentent les balises. Les nœuds de texte contiennent les caractères à l’intérieur des éléments. Les nœuds de commentaire représentent les commentaires. La plupart du temps, vous travaillez avec des éléments, mais savoir que les nœuds de texte existent permet de comprendre les surprises liées aux espaces blancs et pourquoi childNodes peut inclure plus d’éléments que prévu.
Sélectionner des éléments
Avant de pouvoir modifier quoi que ce soit, vous devez d’abord le trouver. L’outil indispensable aujourd’hui est querySelector, qui accepte n’importe quel sélecteur CSS.
// select.js
const title = document.querySelector("h1");
const firstCard = document.querySelector(".card");
const allLinks = document.querySelectorAll("a[target='_blank']");
const byId = document.getElementById("main");
allLinks.forEach((link) => link.classList.add("external"));
querySelector renvoie le premier élément correspondant ou null. querySelectorAll renvoie une NodeList statique de toutes les correspondances, laquelle supporte forEach mais ne se met pas à jour lorsque le DOM change. Les anciennes méthodes getElementById, getElementsByClassName et getElementsByTagName fonctionnent toujours ; les deux dernières renvoient des collections dynamiques (live) qui se mettent à jour automatiquement.
Lire et écrire du contenu
Une fois que vous avez un élément, vous pouvez lire ou remplacer son contenu.
// content.js
const heading = document.querySelector("#title");
heading.textContent; // read the visible text
heading.textContent = "Hello"; // replace it, escaped as text
heading.innerHTML = "<em>Hi</em>"; // parses HTML
Utilisez textContent dès que la valeur provient d’un utilisateur, d’une API ou de toute source que vous ne contrôlez pas entièrement. Cela traite tout comme du texte brut et ne peut pas injecter de balisage. innerHTML analyse une chaîne de caractères comme du HTML, ce qui est pratique pour des templates de confiance, mais représente un risque sérieux de cross-site scripting avec des entrées non fiables. Si vous devez construire du balisage à partir de données, préférez la création d’éléments et la définition de textContent pour chacun d’eux.
Attributs, classes et styles
Les attributs sont liés à l’élément et peuvent être lus ou modifiés directement.
// attrs.js
const link = document.querySelector("a");
link.getAttribute("href");
link.setAttribute("href", "/docs");
link.dataset.userId = "42"; // data-user-id="42"
link.classList.add("active");
link.classList.toggle("open");
link.classList.remove("hidden");
link.style.color = "tomato";
classList est la méthode la plus propre pour gérer les classes — cela évite la concaténation de chaînes de caractères et les doublons de classes accidentels. La propriété style permet de définir des styles inline propriété par propriété ; pour tout ce qui est thématique, privilégiez le basculement d’une classe et laissez CSS décider.
Création, insertion et suppression de nœuds
Vous créez de nouveaux nœuds avec document.createElement et vous les placez à l’aide des méthodes d’insertion.
// build.js
const item = document.createElement("li");
item.textContent = "New task";
item.classList.add("task");
const list = document.querySelector("#todos");
list.append(item); // add at the end
list.prepend(item); // add at the start
item.remove(); // detach from the tree
append accepte plusieurs nœuds et même des chaînes de caractères, tandis que insertBefore permet un placement précis. Lorsque vous devez insérer de nombreux nœuds, construisez-les hors du document et attachez-les en une seule fois :
// fragment.js
const fragment = document.createDocumentFragment();
for (const name of names) {
const li = document.createElement("li");
li.textContent = name;
fragment.append(li);
}
list.append(fragment);
Un DocumentFragment est un conteneur léger qui ne fait pas partie de la page. L’attacher déplace tous ses enfants en une seule opération, ce qui déclenche beaucoup moins de calculs de mise en page (layout) que de les ajouter un par un.
Écouter les événements
Les événements permettent à votre code de répondre aux actions de l’utilisateur et du navigateur. Attachez un écouteur avec addEventListener.
// events.js
const button = document.querySelector("#save");
button.addEventListener("click", (event) => {
event.preventDefault();
console.log("clicked", event.target);
});
L’objet d’événement contient des informations utiles : target est l’élément qui a déclenché l’événement, currentTarget est l’élément auquel l’écouteur est attaché, et preventDefault() annule le comportement par défaut du navigateur. Les événements courants incluent click, input, change, submit, keydown, scroll et DOMContentLoaded.
Délégation d’événements
Au lieu d’attacher un écouteur à chaque enfant, attachez-en un seul au parent et inspectez event.target. Comme les événements remontent (bubble) de la cible vers ses ancêtres, le parent les voit tous.
// delegate.js
const list = document.querySelector("#todos");
list.addEventListener("click", (event) => {
const button = event.target.closest("button.delete");
if (!button) return;
button.closest("li").remove();
});
C’est plus efficace et, surtout, cela continue de fonctionner pour les éléments ajoutés après l’installation de l’écouteur. Pour les listes et les tableaux dynamiques, la délégation est le pattern par défaut.
Parcourir l’arbre
Il arrive que vous ayez besoin de passer d’un nœud à ses parents ou enfants.
// traverse.js
const item = document.querySelector(".item");
item.parentElement; // nearest ancestor element
item.children; // child elements only
item.firstElementChild; // first child element
item.nextElementSibling; // next sibling element
item.closest(".card"); // nearest ancestor matching a selector
Les variantes Element ignorent les nœuds de texte et de commentaire, ce qui est presque toujours le comportement souhaité. closest est particulièrement utile dans les gestionnaires d’événements, comme le montre l’exemple sur la délégation.
Performance et bonnes pratiques
- Regroupez vos lectures et écritures. La lecture de propriétés de mise en page comme
offsetHeightforce le navigateur à recalculer ; alterner lectures et écritures provoque des reflows répétés. - Construisez hors document. Utilisez un
DocumentFragmentou un élément hors écran, puis attachez-le une seule fois. - Mettez vos recherches en cache. Stockez les éléments dans des variables au lieu de lancer des requêtes dans une boucle.
- Privilégiez les classes aux styles en ligne. Basculez une classe et gardez la présentation dans le CSS.
- Utilisez
textContentpar défaut. N’utilisezinnerHTMLqu’avec du contenu fiable et assaini. - Déléguez les événements pour les listes et tout élément dynamique.
- Supprimez les écouteurs dont vous n’avez plus besoin, surtout sur les pages à longue durée de vie, pour éviter les fuites de mémoire.
Erreurs courantes
- Interroger un élément avant qu’il n’existe, puis se demander pourquoi il est
null. - Utiliser
innerHTMLavec des entrées utilisateur et créer une faille XSS. - Attacher un écouteur à chaque ligne au lieu de déléguer l’événement au tableau.
- Lire et modifier des propriétés de mise en page (layout) dans une même boucle.
- Confondre
event.targetetevent.currentTarget. - Supposer que
querySelectorAllest dynamique alors qu’il s’agit d’un instantané statique.
Et après ?
Le DOM est le point de convergence de tout ce que vous avez appris. Combinez-le avec la syntaxe ES6+ pour un code plus propre, alimentez-le en données via l’ Fetch API et assurez sa robustesse grâce à la gestion des erreurs. Le moyen le plus rapide de assimiler tout cela est de reconstruire un petit projet — une liste de tâches, une galerie filtrable ou une recherche en temps réel — entièrement en JavaScript vanilla.