Q1defer の無い head の script で document.querySelector("#total") の値は?
ブラウザで JavaScript が動く仕組み
ブラウザが HTML から DOM を作る流れと、link と script を処理する順番、DOM の完成を待つ defer 属性、DevTools のコンソールでの確認、練習問題のコンソールで結果を文字で確かめる方法を扱います。
JavaScript で画面の文字を書き換えるコードを書いても、読み込ませる場所によって動いたり動かなかったりします。動かないときも画面にはエラーが出ないため、コードの誤りなのか、実行する順番の問題なのかを見分けられません。
この記事では、ブラウザが HTML を読み込み、JavaScript を実行する順番を扱います。
この記事のコードは HTML として開く例
練習問題のコンソールでは HTML を読み込む順番を再現できないため、この記事には練習問題がありません。コードは index.html と app.js を同じフォルダに置き、Chrome で index.html を開いたときの動きを示す例で、結果は行末のコメントに書いてあります。コンソールでの確かめ方は最後の章で扱います。
HTML から要素の入れ子を作る — DOM ツリー
注文確認ページの「計算中…」を、app.js から「3,300 円」に書き換えたいとします。このとき JavaScript が書き換えるのは index.html ファイルの文字ではありません。ブラウザが HTML を読んで組み立てた、別のデータを操作します。
HTML の 要素(<p>〜</p> のようにタグで区切った部品)を、ブラウザは 1 つずつオブジェクトにし、その親子関係の集まりを DOM(Document Object Model。DOM ツリーとも呼び、入口は document)と呼びます。
タグの中の id="total" や defer を属性と呼び、id 属性にはページ内で重ならない名前を書きます。
<!-- ---- index.html(注文確認ページ) ---- -->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>注文の確認</title>
<link rel="stylesheet" href="style.css"> <!-- 見た目を決める CSS -->
</head>
<body>
<h1>注文の確認</h1>
<ul id="items"> <!-- li 2 つを子に持つ -->
<li>マグカップ × 2</li>
<li>コースター × 1</li>
</ul>
<p id="total">計算中…</p> <!-- app.js が書き換えたい要素 -->
</body>
</html>
meta— 文字コードは UTF-8title— 中身は「注文の確認」link— style.css を指す
- 中身は「注文の確認」
li— 中身は「マグカップ × 2」li— 中身は「コースター × 1」
- 中身は「計算中…」
- app.js が書き換えたい要素
app.js は document.querySelector("#total")(# の後ろの id を持つ要素を探し、無ければ null を返す)で p 要素を取り出し、textContent(要素の中の文字)を書き換えます。p 要素は body の子なので、見つかるかどうかは body が DOM に入っているかで決まります。
HTML を上から読む — link と script の順番
合計を書き換える app.js を、CSS と並べて <head> で読み込んだとします。index.html には <p id="total"> が確かに書いてあるのに、ページを開くと表示は「計算中…」のままで、app.js の書き換えが効いていません。
HTML の解析(ブラウザが HTML を先頭から読み、見つけた要素を順に DOM へ追加する処理)の途中で <link> を見つけると、CSS を取りに行きながら解析を続けます。<script src> を見つけたときは解析を止め、JavaScript を取得して実行し終えてから続きを読みます。
// ---- index.html(head で app.js を読み込む) ----
// <head>
// <link rel="stylesheet" href="style.css"> ← 解析は続ける
// <script src="app.js"></script> ← 解析を止めて実行
// </head>
// <body>
// <p id="total">計算中…</p> ← app.js の後で DOM に入る
// </body>
// ---- app.js ----
const total = document.querySelector("#total"); // null(まだ DOM に無い)
total.textContent = "3,300 円";
// TypeError: Cannot set properties of null (setting 'textContent')
書き換えが効かなかった原因は、実行した時点で p 要素がまだ DOM に無かったことです。次の章では、app.js の 2 行は変えずに、実行するタイミングを DOM の完成後へ移します。
DOM の完成を待って実行する — defer 属性
<script> を </body> の直前へ移せば、p 要素を読んだ後に実行されるので #total は見つかります。ただし動くかどうかが置き場所で決まるため、読み込む行を CSS と一緒に <head> へまとめ直すと、同じ null がまた返ります。
defer 属性(HTML の解析を止めずに JavaScript を取得し、DOM が完成してから実行させる <script> の指定)は <script defer src="app.js"></script> と書き、CSS の <link> と同じく <head> に置けます。
// ---- index.html(defer を付けて head で読み込む) ----
// <head>
// <link rel="stylesheet" href="style.css">
// <script defer src="app.js"></script> ← 取得を始め、解析は続ける
// </head>
// <body>
// <p id="total">計算中…</p> ← </html> まで読み終えてから app.js を実行
// </body>
// ---- app.js(前の章と同じ 2 行+確認の 1 行) ----
const total = document.querySelector("#total"); // p 要素
total.textContent = "3,300 円"; // 画面の表示が変わる
console.log(total.textContent); // 3,300 円
defer を付けた script が複数あるときは、取得が終わった順ではなく、HTML に書いた順に実行されます。下の表は、script の書き方ごとに、app.js を実行するときと #total が見つかるかをまとめたものです。
| script の書き方 | app.js を実行するとき | #total |
|---|---|---|
| head に <script src="app.js"> | 解析を止めて、その場で実行 | 見つからない(null) |
| </body> の直前に <script src="app.js"> | p 要素を読んだ後に実行 | 見つかる |
| head に <script defer src="app.js"> | DOM が完成してから実行 | 見つかる |
| head の <script defer> の中に直接コードを書く | src が無いと defer は効かず、その場で実行 | 見つからない(null) |
| head に <script type="module" src="app.js"> | defer を付けなくても DOM の完成後に実行 | 見つかる(index.html を直接開くと読み込めない) |
止まった行と DOM を調べる — DevTools
defer を付ける前のページでは、app.js が TypeError で止まっても、画面には何も表示されませんでした。どのファイルの何行目で止まったのかが分からないと、表示が「計算中…」のまま変わらない原因を探せません。
DevTools(ブラウザに付いている、ページの DOM やエラーを調べるための画面)の Console タブは、Chrome なら Windows で Ctrl + Shift + J、Mac で Command + Option + J を押すと開きます。ここに console.log の出力と、止まったエラーのファイル名と行番号が並びます。
// ---- Console タブに出たエラー(右端の app.js:2 を押すとその行が開く) ----
// Uncaught TypeError: Cannot set properties of null (setting 'textContent')
// at app.js:2:19 ← 2 行目の 19 文字目で止まった
// ---- 読み込み後に、Console の > の後ろへ入力して Enter ----
document.querySelector("#total"); // <p id="total">計算中…</p>
document.querySelector("#total").textContent; // '計算中…'
// ---- app.js の 1 行目の後に足す行(Console には入力しない) ----
console.log("実行時の total:", total); // 実行時の total: null
Console に入力した式は、入力した時点の DOM に対して実行されます。app.js の中の値を確かめたいときは、疑わしい行の前に console.log を書き足し、実行した時点の値を Console タブに出します。
結果を文字で確かめる — innerHTML と outerHTML
この先の記事の演習では、要素を探して書き換えたり追加したりします。ところが練習問題のコンソールには画面が無く、再読み込みするまではすべての練習問題が 1 つの見えないページを共有するため、前に実行したときの要素が残ったまま次の実行が始まります。
innerHTML(要素の中身を HTML の文字列で読み書きするプロパティ)に代入すると、中身がまるごと置き換わります。演習の先頭で document.body(body 要素)の innerHTML に代入して前の要素を消し、結果は outerHTML(要素自身のタグを含む HTML の文字列)を出力して確かめます。
// ① body の中身をまるごと置き換える(前の実行で残った要素はここで消える)
document.body.innerHTML = `<p id="total">計算中…</p>`;
// ② 要素を探して、中の文字を書き換える
const total = document.querySelector("#total");
total.textContent = "3,300 円";
// ③ 画面は見えないので、文字列にして出力する
console.log(total.textContent); // 3,300 円
console.log(total.outerHTML); // <p id="total">3,300 円</p>
console.log(document.body.innerHTML); // <p id="total">3,300 円</p>
- 結果は
console.logの出力だけが文字で返る
document.bodyの中身 — 次の実行まで残る
const total— この実行の中だけで使えるdocument.body.innerHTMLに p を代入する
const total— 前回とは別の変数として作られる- 先頭の代入で、1 回目の p が消える
コンソールのコードは実行のたびに新しい関数の中で動くので、const total は前回とは別の変数として作られ、再宣言のエラーになりません。また、ページの読み込みが終わった後に実行されるため、defer を考える必要もありません。
要素をそのまま出力すると {} になる
練習問題のコンソールは画面を持たず、出力を文字に変換して返します。要素をそのまま console.log に渡すと中身の見えない {} と表示され、書き換えが効いたかを判断できません。要素は outerHTML か textContent を出力して確かめます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2<script defer src="app.js"> の app.js が実行されるのはいつですか?
Q3document.body.innerHTML = '<p>計算中…</p>'; を 2 回実行すると、p 要素はいくつですか?