「JavaScriptコードが実行されるタイミング」について、JavaScript初学者向けに解説します。 解説は、次の3つに分類されます。
- HTMLがパース⇒実行される過程
- JavaScriptコードがパース⇒実行される過程
- JavaScriptで使われるDOMのイベントモデル
初学者向けですが、次節の専門用語が頻出する為、プログラミング初学者には理解が難しいかもしれません。
用語 | 説明 |
---|---|
ダウンロード(download) | ファイルをサーバからクライアントPCにコピーすること |
パース(parse) | ある文法に沿った文字列を解析し、プログラムで扱えるデータ集合に変換すること |
実行(execute) | パースして出来た「プログラムで扱えるデータ集合」を実行すること。通常はパース後に即実行しますが、 script[defer] のように特定のタイミングまで実行を待機させる場合があります。 |
各言語の役割を簡単に説明しておきます。
言語名 | 種類 | 役割 |
---|---|---|
HTML | マークアップ言語 | 「構造」を表す(※DOMと密接な関係有) |
CSS | スタイルシート言語 | 「装飾(デザイン)」を表す |
JavaScript | スクリプト言語 | 「振舞い」を表す |
JavaScriptは「振舞い」を表すわけですが、実はJavaScriptは単体では全く機能しません。 JavaScriptからHTML(※厳密にはDOM)やCSSを間接的に操作する事でWebブラウザにWeb制作者が望む「振舞い」をさせる事が出来ます。 従って、JavaScriptを扱うためにはHTML/CSSについても予め知っておく必要があります。
WebブラウザはWebサーバからhtml/css/jsファイルをダウンロードし、ダウンロードした文字列をパース処理後にコードを評価→実行します。 JavaScriptの実行タイミングを知るにはまず、HTMLパーサの動きを知る必要があります。
次のHTMLをパースする場合を考えてみましょう。 (JTMLパーサの動きは「基本的な考え方」は正しいと思いますが、細かく読み解けてはいない為、厳密な定義を知りたい場合は上述のリンク先を参照して下さい)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Parsing HTML document Sample</title>
<link rel="stylesheet" type="text/css" href="foo.css"/>
<script src="bar.js"></script>
</head>
<body>
<p id="target">target</p>
</body>
</html>
HTMLパーサは上から順番にパース処理を実行していきます。
<!DOCTYPE html>
をパース⇒実行 (DOCTYPE宣言)<html>
をパース⇒実行 (開始タグ)<head>
をパース⇒実行 (開始タグ)- ...
外部CSS、外部スクリプト等の外部ファイルが存在する場合は、パース後にダウンロード処理を挟む必要がある為、もう少し、複雑化します。
<link rel="stylesheet" type="text/css" href="foo.css"/>
をパース⇒実行 (空要素)foo.css
をダウンロード (※ダウンロード開始後に後続処理を開始する。並列処理。)<script src="bar.js">
をパース⇒実行 (開始タグ)bar.js
をダウンロード (※ダウンロード完了するまで 6. 以降の処理を停止する。直列処理。)</script>
をパース⇒実行 (終了タグ)</head>
をパース⇒実行 (終了タグ) ※bar.jsのダウンロード完了後にこのステップは実行される- ...
このように、外部スクリプトはHTMLパーサの処理を中断させる為、レンダリング速度に影響が出ます。
その為、最近では </body>
手前に外部スクリプトを置く方法が推奨される事が多いようです。
(※実際には </body>
手前ではタイミングが遅い場合もあるので、厳密にはケースバイケースですが、パース処理を強制的に停止する性質は覚えておくと良いでしょう)
前節で説明した通り、script要素で外部スクリプトを読み込む場合、jsファイルがダウンロード完了するまで後続のパース処理が停止してしまいます。 HTML Standard (通称HTML5) ではこの問題への対策として、script要素に新しい属性が定義されました。
開始タグ | 同期/非同期 | 実行タイミング |
---|---|---|
<script> | 同期 | ダウンロード完了後 |
<script async> | 非同期 | ダウンロード完了後 |
<script defer> | 非同期 | DOM構築完了後 |
<script type="module"> | 非同期 | DOM構築完了後 |
<script type="module" async> | 非同期 | ダウンロード完了後 |
- 非同期は外部スクリプトのダウンロード時にパース処理を停止しない事を意味しており、パース処理とダウンロード処理を並列処理できます
- ただし、いずれもコード実行時にはパース処理が停止します(※詳細は「4.12.1 script要素 - HTML Standard 日本語訳」を参照して下さい)。
HTMLパーサがHTMLタグをパース⇒実行した時、全てのHTMLソースはDOMと呼ばれるオブジェクトに変換し、Webブラウザによって管理されます。 たまに、「JavaScriptでHTMLソースを書き換える方法はありますか」という質問を受ける事がありますが、WebブラウザにとってはHTML文書を読み込み完了した時点で、DOMオブジェクトに変換してしまっている為、元のHTMLソースを操作する術がありません。 全てはDOMによって管理されます。
簡単な事例として、次のHTMLソースをWebブラウザに読み込ませてみましょう。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>DOM Sample</title>
<script>
console.log(document.getElementById('target')); // null
</script>
</head>
<body>
<p id="target">target</p>
<script>
console.log(document.getElementById('target')); // <p id="target">target</p>
</script>
</body>
</html>
JavaScriptはWebブラウザ付属の「開発者ツール(Developer Tools)」によってデバッグする事が出来、console.log()
は開発者ツールのコンソールに出力する関数になります。
コンソールの確認方法はWebブラウザによって異なりますが、Google Chrome では [Ctrl] + [Shift] + [J] キーを押せば、コンソールが表示されます。
コンソールには、次のように表示されます。
null
<p id="target">target</p>
1行目は、<head>
内のスクリプトコードで、コード実行時には <p id="target">target</p>
がまだパースれていない為に、null
を返しています。
(getElementById
は対応する要素が見つからなかった場合に null
を返します)
2行目は、</body>
手前のスクリプトコードで、コード実行時に <p id="target">target</p>
のパース処理が完了している為に、要素ノードを取得出来ています。
こうしてみると、2行目の方法で全てのコードを書きたくなるかもしれませんが、実は1行目のタイミングでも該当要素ノードを得る方法があり、それは次節で紹介します。
DOMにはイベントモデルという概念があり、特定のタイミングでイベントが発火し、対応するイベントハンドラ関数が実行されます。Webブラウザは「DOM Standard」「UI Events」仕様に則り、イベントモデルを実装しています。
各イベントが実行されるタイミングになる事を**発火(fire)**と呼びます。イベントが発火する時、対応するイベントハンドラ関数が実行されるわけですが、addEventListener
によってイベントハンドラ関数を設定することが出来ます。
- EventTarget.addEventListener() - Web API インターフェイス | MDN
- 2.6. インタフェース EventTarget - DOM Standard 日本語訳
引数 | 説明 |
---|---|
第一引数(type) | イベントタイプ名を指定します(例: "click", "DOMContentLoaded") |
第二引数(listener) | イベント発火時に実行されるイベントハンドラ関数(listener関数)を指定します(※1) |
第三引数(options) | キャプチャフェイズ(Capture Phase)を捕捉するか否かの真偽値を指定します(既定値は false ) |
※1 仕様としてはリスナー関数が正しいですが、旧来のイベントモデルを覚えている人にとっては「イベントハンドラ関数」の方が通りがいいようです。ただし、第二引数にlistenerオブジェクトを指定した場合には「イベントハンドラオブジェクト」と呼称するのは正しくありません。
コードは次のようになり、addEventLIstener
は対象のDOMノードオブジェクトのメソッドとして実行します。
node.addEventListener(type, listener, useCapture);
前節のHTMLを addEventListener
を使用したコードに書き換えてみましょう。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>addEventListener Sample</title>
<script>
function handleDOMContentLoaded (event) {
console.log(event.type, document.getElementById('target')); // "DOMContentLoaded" <p id="target">target</p>
}
document.addEventListener('DOMContentLoaded', handleDOMContentLoaded, false);
</script>
</head>
<body>
<p id="target">target</p>
<script>
console.log('</body>', document.getElementById('target')); // "</body>" <p id="target">target</p>
</script>
</body>
</html>
コンソールには下記が出力されます。
"</body>" <p id="target">target</p>
"DOMContentLoaded" <p id="target">target</p>
1行目は </body>
手前で実行されるコードです。
2行目は addEventListener
の第二引数に指定された関数コードです。
この関数は「DOMContentLoadedイベントハンドラ関数」と呼べるもので、DOMContentLoadedの発火タイミングで実行されます。DOMContentLoadedは「DOM構築完了のタイミング」で実行されます(全てのHTMLソースをパース⇒実行完了したタイミング)。スクリプトコードが実行された時点では </body>
等のパースすべきHTMLタグが残っている為、2行目のコードが先に実行されるわけです。
JavaScriptは情報量がトップクラスに多いですが、誤情報も多いと感じています。
現在では、Webブラウザ別の動作の差は小さくなっていますが、特定のブラウザで動作しなかった場合の切り分けが難しい、という根本的な問題があります。 ある機能がJavaScriptコードで実装されている時、それが動いている理由は3通り考えられます。
- Web標準の機能で動作している
- 特定のWebブラウザの独自拡張でどうしている
- 特定のWebブラウザのバグで偶然動作している
例えば、あるコードがIEで動作し、Firefoxで動作しなかったとしましょう。 考えられる原因は4通りありますが、想像できるでしょうか。
原因 | 対策 |
---|---|
(A) Firefoxが当該機能を実装していない | Firefoxが実装している機能で実装します(機能検出による分岐) |
(B) IEの独自拡張機能を使っており、Firefoxが独自拡張に未対応 | Web標準仕様の機能を使って実装します |
(C) Firefoxのバグで動作しない | バグの発生条件を特定し、回避手段を探ります。もしくは、別の機能で代替できないか検討します。 |
(D) IEのバグを利用して偶然動作している(Firefoxの挙動が正しい) | Firefoxの挙動に合わせてコードを修正する。IEのバグは回避手段を探るか、別の機能で代替する。 |
これを正確に切り分ける為には、関数/プロパティ単位で次の手順を踏む必要があります。
切り分け手順 | 結果 |
---|---|
(1) 当該関数はWeb標準仕様書に記載されているか | Yesなら (2) へ。No なら (3) へ。 |
(2) 当該関数はFirefoxで実装されているか | Yes なら Firefoxの実装バグ。No なら Firefox で未実装。 |
(3) 当該関数はブラウザベンダーのサイトでIEの独自拡張と記載されているか | Yes なら IE の独自拡張。No なら IE のバグで動作している。 |
これを切り分けるのは、Web標準仕様書とブラウザベンダーのサイトで資料をあたる必要がある為、一般人には難易度が高いでしょう。ですので、通常は「動いたら正」「動かなかったらバグ」と考え、誤情報を流布してしまいます。
原則として、Web標準で規定される仕様は全て、後方互換性を保つように設計されています。
- 新しい機能を既定しても、古い機能を削除しません
- 古い機能を改定する場合は、今までの古い実装でも機能するほうに挙動的には同じとします
なぜなら、クライアントPCを持つユーザが常に最新のブラウザを使ってくれるとは限らないからです。 これは新しい機能が仕様規定された場合に「新しい機能」と「古い機能」が混在する事を意味しています。 古い機能が必ずしもダメなわけではありませんが、中には一般に非推奨とされる機能も後方互換性の為だけに残し、その情報を公開しているサイトもあります。
[typeof null === 'object'
なので、null
はオブジェクトだと思っているdocument.formName
で form 要素ノードを参照しているeval("1")
で Number 型に変換している
[typeof null === 'object'
は歴史的な経緯があって残っていますが、個人的には後方互換性を排除しても良いから撤廃してほしかったところです…。
Webブラウザは Internet Explorer, Microsoft Edge, Google Chrome, Firefox...のように様々であり、各ブラウザで多少の動作の違いがあります。そこでW3CやWHATWG等の標準化団体がWeb標準仕様を策定し、ブラウザベンダーがその仕様に準拠させる事で動作を統一させようという動きがあります。Web仕様書の信頼性は最も高いといえます。
JavaScriptでコードを書く時にも標準仕様に準拠したコードを書けば、ブラウザの違いを意識する負担が減るわけですが、参照すべき標準仕様が多数ある為、全てを一度に網羅するのは難しいでしょう。
ですので、初めは下記から始める事を推奨します。
Webブラウザの互換性情報は下記が役立ちます。
- Can I use...
- ECMAScript 6 compatibility table
- JavaScript | MDN ※各種メソッド/プロパティのページを参照。日本語版は情報が古い事が多々ある為、英語推奨。
Mozilla, Microsoft等のブラウザベンダーのサイトはWebブラウザを制作しているだけに個人サイトよりも信頼性が高いといえます。
なお、MDNは基本的には情報を信頼できますが、日本語版は情報が古い事が多く、英語版に切り替えると最新の情報が手に入ることもあります。
「躓きやすいポイント」で述べた通り、個人サイトは誤情報が最も多い為、私がお勧めできるサイトは2つしかありません。
Google Chromeで実装された最新の機能を紹介しています。 対応するECMAScript仕様が存在したなら、ES2015のようにバージョン番号を添えて紹介してくれます。
ECMAScriptから始まって、DOMまでWeb標準仕様のみを使って、コードの書き方を解説しています。 ES5, ES2015 というように仕様のバージョン番号別に解説があるのもGood 最新の機能は古いブラウザでは使えない事が多い為、そうした配慮があるのは助かります。
お勧めポイントは次の通り。
- プログラミング初心者をターゲットにしていると思われるとても丁寧な解説をしている書籍です。
- DOM APIの中でも汎用的に使える関数/プロパティを解説しています(table要素ノード独自のAPI等、用途が限られるAPIまでは網羅していません)
- ECMAScript(JavaScriptの基本仕様)も30P程、説明されており、文法/制御構文/オブジェクトと一通りの説明があります。
- HTMLはXHTML1.1当時のものなので古いですが、説明の根幹となるのはDOMなので、問題はないと思います。
- 「出しゃばらないJavaScript」のような推奨される作法も解説されています
- 文章による説明が大半を占めており、必要な部分だけ拾い読みをしたり、索引から辞書的に引く用途には向きません
他言語習得者にとっては説明が平易すぎて退屈に感じる部分もあるかもしれませんが、コーディング上の良い作法を学べたので、私は損をしたとは思いませんでした。
お勧めポイントは次の通り。
- 表と図を上手く使い、短い説明文で簡潔に解説しています
- 説明が簡素なので、どちらかといえば他言語習得者向けです(プログラミング初心者は辛いかも)
- DOM APIの中でも汎用的に使える関数/プロパティを解説しています(table要素ノード独自のAPI等、用途が限られるAPIまでは網羅していません)
- 目次に各APIの機能的説明(例: ノードを削除する)がある為、やりたいことから関数名を調べたい時に有用です
- ECMAScriptには17ページ程、紙面を消費しており、こちらもシンプルな説明にまとまっています
目次性が高いのが最大の特徴です。機能からAPIを引ける為、DOMの基礎学習時に何度も引きました。
お勧めポイントは次の通り。
- 840ページの大ボリューム
- 基本的に仕様に忠実で正確性の高さが特徴です
- 子細にわたって説明するタイプで、プログラミング初心者が読めば、文章量の多さに挫折するかもしれません
- ECMAScript/DOM/XMLHttpRequest/Web Strage/jQueryを網羅しています
- ECMAScriptの説明に全22章中11章の紙面を割いており、特にJavaScriptの基本仕様を知る場合に有用です
- 次点でDOM仕様が4章分の紙面を消費しています
実は私が所有しているのは『JavaScript 第5版』ですが、私が購入した当時はJavaScript参考書として必ず、名前があがる程、有名な書籍でした。 『JavaScript 第6版』でも2012年08月 発行なので、情報はだいぶ、古くなっていますが、基本的な仕様はECMAScript/DOM共に変わっていない為、基礎学習の範囲であれば、まだ通用すると思います。 正確で細かい部分まで仕様を知りたい場合にお勧めです。
2018/05/06現在のECMAScript正式版は「ECMAScript 2017 (ECMAScript 8)」なので、情報が古いですが、ECMAScript仕様の日本語訳が欲しい時にはお勧めです。 一次情報の日本語訳の為、基本的に間違いがありません。