Question 1Dans un script chargé dans head sans defer, que renvoie document.querySelector("#total") ?
Comment JavaScript s'exécute dans le navigateur
Comment le navigateur construit le DOM à partir du HTML, dans quel ordre il traite link et script, l'attribut defer, la console des DevTools et la vérification des résultats en texte.
Du code JavaScript qui modifie un texte de la page peut marcher ou non selon l'endroit où tu le charges. Quand il ne marche pas, aucune erreur ne s'affiche sur la page : impossible de savoir si le code lui-même est faux ou si le problème vient de l'ordre d'exécution.
Cet article explique dans quel ordre le navigateur charge le HTML et exécute le JavaScript.
Le code de cet article s'ouvre sous forme de page HTML
La console des exercices ne peut pas reproduire l'ordre de chargement du HTML : cet article n'a donc pas d'exercices. Chaque exemple montre ce qui se passe quand tu places index.html et app.js dans le même dossier et que tu ouvres index.html dans Chrome ; les résultats sont indiqués en commentaire en fin de ligne. La façon de vérifier dans la console est expliquée dans la dernière section.
Des éléments imbriqués construits à partir du HTML — l'arbre DOM
Imagine qu'app.js doive remplacer le texte « Calcul en cours… » d'une page de confirmation de commande par « 3 300 yens ». Ce que JavaScript modifie ici, ce n'est pas le texte du fichier index.html, mais une autre structure de données, que le navigateur a construite en lisant le HTML.
Le navigateur transforme chaque élément HTML (un morceau de la page délimité par des balises, comme <p>…</p>) en objet, et l'ensemble de leurs relations parent-enfant s'appelle le DOM (Document Object Model, aussi appelé arbre DOM ; on y accède par document).
Les informations ajoutées dans une balise, comme id="total" ou defer, s'appellent des attributs. Un id doit être unique : aucun autre élément de la page ne peut porter le même.
<!-- ---- index.html (page de confirmation de commande) ---- -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Confirmation de commande</title>
<link rel="stylesheet" href="style.css"> <!-- le CSS qui définit l'apparence -->
</head>
<body>
<h1>Confirmation de commande</h1>
<ul id="items"> <!-- contient deux li (ses enfants) -->
<li>Mug × 2</li>
<li>Dessous de verre × 1</li>
</ul>
<p id="total">Calcul en cours…</p> <!-- l'élément qu'app.js veut modifier -->
</body>
</html>
meta— l'encodage est UTF-8title— contient « Confirmation de commande »link— pointe vers style.css
- Contient « Confirmation de commande »
li— contient « Mug × 2 »li— contient « Dessous de verre × 1 »
- Contient « Calcul en cours… »
- L'élément qu'app.js veut modifier
app.js récupère l'élément p avec document.querySelector("#total") (qui cherche l'élément dont l'id est écrit après le #, et renvoie null s'il n'y en a pas), puis modifie son textContent (le texte contenu dans l'élément). Comme l'élément p est un enfant de body, il n'est trouvé que si body est déjà dans le DOM.
Le HTML se lit de haut en bas — l'ordre de link et script
Suppose que tu charges app.js, qui met à jour le total, dans <head>, à côté du CSS. index.html contient bien <p id="total">, et pourtant la page affiche toujours « Calcul en cours… » : la modification d'app.js n'a aucun effet.
Pendant l'analyse du HTML (le navigateur lit le HTML depuis le début et ajoute au DOM, dans l'ordre, chaque élément qu'il rencontre), quand il tombe sur un <link>, il continue l'analyse tout en récupérant le CSS. Quand il tombe sur un <script src>, il suspend l'analyse, récupère le JavaScript et l'exécute jusqu'au bout avant de lire la suite.
// ---- index.html (app.js chargé dans head) ----
// <head>
// <link rel="stylesheet" href="style.css"> ← l'analyse continue
// <script src="app.js"></script> ← l'analyse s'arrête pour l'exécuter
// </head>
// <body>
// <p id="total">Calcul en cours…</p> ← ajouté au DOM après app.js
// </body>
// ---- app.js ----
const total = document.querySelector("#total"); // null (pas encore dans le DOM)
total.textContent = "3 300 yens";
// TypeError: Cannot set properties of null (setting 'textContent')
La modification n'a pas marché parce que l'élément p n'était pas encore dans le DOM au moment où app.js s'est exécuté. Dans la section suivante, on garde les deux lignes d'app.js telles quelles et on repousse simplement leur exécution à la fin de la construction du DOM.
Attendre que le DOM soit complet — l'attribut defer
Si tu déplaces le <script> juste avant </body>, il s'exécute après la lecture de l'élément p, et #total est trouvé. Mais le bon fonctionnement dépend alors de l'emplacement de la balise : si un jour tu regroupes la ligne de chargement avec le CSS dans <head>, tu obtiens de nouveau null.
L'attribut defer (une option de <script> qui récupère le JavaScript sans suspendre l'analyse du HTML et ne l'exécute qu'une fois le DOM complet) s'écrit <script defer src="app.js"></script>, et il peut aller dans <head>, comme le <link> du CSS.
// ---- index.html (chargé dans head avec defer) ----
// <head>
// <link rel="stylesheet" href="style.css">
// <script defer src="app.js"></script> ← la récupération commence, l'analyse continue
// </head>
// <body>
// <p id="total">Calcul en cours…</p> ← app.js s'exécute une fois tout lu jusqu'à </html>
// </body>
// ---- app.js (les 2 mêmes lignes qu'avant, plus 1 pour vérifier) ----
const total = document.querySelector("#total"); // l'élément p
total.textContent = "3 300 yens"; // le texte affiché change
console.log(total.textContent); // 3 300 yens
Quand plusieurs scripts ont defer, ils s'exécutent dans l'ordre où ils apparaissent dans le HTML, et non dans l'ordre où leur récupération se termine. Le tableau ci-dessous indique, pour chaque façon d'écrire le script, quand app.js s'exécute et si #total est trouvé.
| Balise script | Exécution d'app.js | #total |
|---|---|---|
| <script src="app.js"> dans head | Tout de suite, analyse suspendue | Introuvable (null) |
| <script src="app.js"> avant </body> | Après la lecture du p | Trouvé |
| <script defer src="app.js"> dans head | Une fois le DOM complet | Trouvé |
| Script inline avec defer (head) | Tout de suite (defer ignoré sans src) | Introuvable (null) |
| <script type="module" src="app.js"> dans head | Après le DOM, même sans defer | Trouvé (sauf en fichier local) |
Trouver la ligne fautive et inspecter le DOM — DevTools
Dans la version sans defer, app.js levait une TypeError, mais rien ne l'indiquait sur la page. Sans savoir dans quel fichier et à quelle ligne l'erreur s'est produite, tu ne peux pas trouver pourquoi la page reste bloquée sur « Calcul en cours… ».
Les DevTools (les outils de développement intégrés au navigateur, qui servent à inspecter le DOM et les erreurs d'une page) ont un onglet Console, qui s'ouvre dans Chrome avec Ctrl + Maj + J sous Windows ou Cmd + Option + J sur Mac. On y trouve la sortie de console.log, ainsi que chaque erreur avec le nom du fichier et le numéro de ligne où elle a été levée.
// ---- Erreur affichée dans l'onglet Console (clique sur app.js:2 à droite pour ouvrir la ligne) ----
// Uncaught TypeError: Cannot set properties of null (setting 'textContent')
// at app.js:2:19 ← levée à la ligne 2, colonne 19
// ---- Une fois la page chargée, tape ceci à l'invite > de la Console, puis appuie sur Entrée ----
document.querySelector("#total"); // <p id="total">Calcul en cours…</p>
document.querySelector("#total").textContent; // 'Calcul en cours…'
// ---- Ligne à ajouter après la ligne 1 d'app.js (à ne pas taper dans la Console) ----
console.log("total à l'exécution :", total); // total à l'exécution : null
Une expression tapée dans la Console s'exécute sur le DOM tel qu'il est à cet instant. Pour vérifier une valeur à l'intérieur d'app.js, ajoute un console.log avant la ligne suspecte : la valeur au moment de l'exécution s'affichera dans l'onglet Console.
Vérifier les résultats sous forme de texte — innerHTML et outerHTML
Dans les exercices des articles suivants, tu vas chercher, modifier et ajouter des éléments. Mais la console des exercices n'a pas d'écran, et jusqu'au prochain rechargement, tous les exercices partagent une seule page invisible : chaque exécution commence donc avec les éléments laissés par la précédente.
Affecter une valeur à innerHTML (une propriété qui lit et écrit le contenu d'un élément sous forme de chaîne HTML) remplace tout son contenu. Chaque exercice commence par une affectation à l'innerHTML de document.body (l'élément body) pour effacer les éléments précédents, et tu vérifies le résultat en affichant outerHTML (une chaîne HTML qui inclut les balises de l'élément lui-même).
// ① Remplacer tout le contenu de body (les éléments restés de l'exécution précédente disparaissent ici)
document.body.innerHTML = `<p id="total">Calcul en cours…</p>`;
// ② Trouver l'élément et modifier son texte
const total = document.querySelector("#total");
total.textContent = "3 300 yens";
// ③ La page n'est pas visible : afficher les résultats sous forme de chaînes
console.log(total.textContent); // 3 300 yens
console.log(total.outerHTML); // <p id="total">3 300 yens</p>
console.log(document.body.innerHTML); // <p id="total">3 300 yens</p>
- Seule la sortie de
console.logrevient, sous forme de texte
- Contenu de
document.body— conservé jusqu'à l'exécution suivante
const total— utilisable uniquement dans cette exécution- Affecte un p à
document.body.innerHTML
const total— une nouvelle variable, distincte de la précédente- L'affectation du début efface le p de la 1re exécution
Le code de la console s'exécute à chaque fois dans une nouvelle fonction : const total est donc une variable distincte de celle de l'exécution précédente, et tu n'obtiens pas d'erreur de redéclaration. Il s'exécute aussi après la fin du chargement de la page, donc tu n'as pas à te soucier de defer.
Afficher un élément directement donne {}
La console des exercices n'a pas de page à montrer : elle convertit la sortie en texte avant de la renvoyer. Si tu passes un élément tel quel à console.log, il s'affiche sous la forme {}, sans rien de visible à l'intérieur, et tu ne peux pas savoir si ta modification a marché. Pour vérifier un élément, affiche son outerHTML ou son textContent.
Vérification des connaissances
Répondez à chaque question une par une.
Question 2Avec <script defer src="app.js">, quand app.js s'exécute-t-il ?
Question 3Si tu exécutes deux fois document.body.innerHTML = '<p>Calcul en cours…</p>';, combien y a-t-il d'éléments p ?