イベントハンドラを登録するコードの記述位置に関する注意と対処方法

要素のプロパティに対してイベントハンドラを登録したり、 addEventListener メソッドを使ってイベントリスナーを登録するための JavaScript のコードが、 HTML ページの中で対象の要素が記述されている位置よりも前にある場合にはエラーが発生する可能性があります。その為、ヘッダー部分で JavaScript のコードを読みこむ場合には注意が必要です。ここではイベントハンドラを登録するコードの記述位置に関する注意と対象方法について解説します。

(Last modified: )

コードの記述する位置によってエラーが発生する理由

ここでは例として addEventListener を使ってイベントリスナーを登録する場合で解説していきます。プロパティを使ってイベントハンドラを登録する場合も同じです。

これまでは addEventListener メソッドを使って click イベントのイベントリスナーを登録するには次のように記述していました。

<input type="button" value="button" id="xxx">

<script>
    let button = document.getElementById('xxx');
    button.addEventListener('click', function(){
        console.log('Hello');
    });
</script>

では input タグと script タグの順序を入れ替えるとどうなるでしょうか。

<script>
    let button = document.getElementById('xxx');
    button.addEventListener('click', function(){
        console.log('Hello');
    });
</script>

<input type="button" value="button" id="xxx">

すると HTML ページへアクセスした時点で次のようなエラーが表示されます。

>> TypeError: Cannot read property 'addEventListener' of null

エラーとなる理由は次のとおりです。

HTML ページはファイルのダウンロードが終わったあと、 HTML ページに記述されている内容をページの先頭から順にパース(解析)して DOM ツリーを構築します。そして HTML ページの中に <script> タグがあると、パースをいったん停止して <script> タグの中の JavaScript のコードが実行されます。(詳しくは「JavaScriptのコードをHTMLファイルのどこに記述するべきなのか」を参照されてください)。

<script> タグの中では最初に getElementById メソッドを使って id 属性が 'xxx' の要素ノードを取得しようとしますが、この要素は <script> よりも後に記述されているため、まだパースが行われておらず見つけることができません。その結果、変数 button には null が格納されます。

そして次の文で addEventListener メソッドを実行しようとしますが、ターゲットである変数 button には null が格納されているため先ほどのエラーが発生します。

これは外部のファイルに JavaScript のコードを記述し、そのファイルを読み込んでいる場合でも同じです。 <script> タグでファイルの読み込みを行っている場合もパースをいったん停止してファイルの読み込みを行い、読み込みが完了したあとで JavaScript のコードが実行されるため、まだパースが行われていない input タグを見つけられずにターゲットが null になってしまいます。

<script src="./main.js"></script>

<input type="button" value="button" id="xxx">
main.js
let button = document.getElementById('xxx');
button.addEventListener('click', function(){
    console.log('Hello');
});

このように JavaScript のコードを記述する位置によってはエラーが発生してしまいます。このエラーを回避する 3 つの方法をこのあと解説します。

JavaScriptのコードをHTMLページの末尾に記述する

一つ目の方法はこれまでのサンプルでも行ってきたようにターゲットとなる要素を HTML ページで先に記述し、それよりもあとで <script> を記述することです。

先ほども解説したように HTML ページのパースはページの先頭から順に行っていきます。要素の記述はページ内の表示される位置に記述する必要がありますので、 JavaScript のコードを要素よりもあとへ、例えば HTML ページの末尾である </body> の直前に記述すれば先ほどのようなエラーは起こりません。

JavaScript のコードの記述位置に特に制限がなければ一番簡単な方法です。

サンプルコード

次のサンプルを見てください。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>サンプル</title>
</head>
<body>

<p>ボタンをクリックしてください。</p>

<input type="button" value="button" id="xxx">

<p>ログに文字列が出力されます。</p>

<!-- JavaScriptのコードは末尾に記述する -->
<script src="./main.js"></script>

</body>
</html>
main.js
let button = document.getElementById('xxx');
button.addEventListener('click', function(){
    console.log('Hello');
});

ボタンをクリックすると 'Hello' をコンソールに出力します。

JavaScriptのコードをHTMLページの末尾に記述する(1)

DOMContentLoadedイベントを利用する

ここからはターゲットなる要素よりも前に <script> タグを記述したい場合の方法です。例えば <head> タグ内に <script> タグを記述したい場合に利用できます。

エラーが発生する理由がターゲットの要素がまだパースされていない状態で参照しようするためなので、 JavaScript のコードは先に記述してあっても実行するのをターゲットの要素のパースが終わって DOM ツリーが完成したあとまで伸ばすことでエラーが発生されないようにすることができます。

HTML ページのパースが完了し DOM ツリーが完成すると document.DOMContentLoaded というイベントが発生します。このイベントのイベントリスナーとして HTML ページ内の要素に対するイベント処理を行う JavaScript のコードを記述することで、 JavaScript のコードの実行を DOM ツリーが完成したあとまで延期することができます。

<head>
<script>
    document.addEventListener('DOMContentLoaded', function(){
        let button = document.getElementById('xxx');
        button.addEventListener('click', function(){
            console.log('Thank you');
        });
    });
</script>
</head>

<body>
    <input type="button" value="button" id="xxx">
</body>

DOMContentLoaded イベントが発生すると、イベントリスナーとして登録したコードが実行され、ターゲットの要素の取得と click イベントへのイベントリスナーの登録が実行されます。

サンプルコード

次のサンプルを見てください。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>サンプル</title>

<script src="./main.js"></script>

</head>
<body>

<p>ボタンをクリックしてください。</p>

<input type="button" value="button" id="xxx">

<p>ログに文字列が出力されます。</p>

</body>
</html>
main.js
document.addEventListener('DOMContentLoaded', function(){
    let button = document.getElementById('xxx');
    button.addEventListener('click', function(){
        console.log('Thank you');
    });
});

ボタンをクリックすると 'Thank you' をコンソールに出力します。

DOMContentLoadedイベントを利用する(1)

loadイベントを利用する

DOMContentLoaded イベントは document オブジェクトで発生します。 document オブジェクトには ondomcontentloaded プロパティが存在しないため、プロパティを使ったイベントハンドラの設定を行う方法は使用できません。 addEventListener メソッドが使用できない場合には、代わりに window オブジェクトで発生する load イベントを使用してください。

DOM ツリーの構築が完了すると DOMContentLoaded イベントが発生しますが、そのあとで画像やスタイルシートなどすべてのリソースの読み込みが完了すると load イベントが発生します。順番としては先に DOMContentLoaded イベントが発生し、そのあとで load イベントが発生します。 load イベントが発生した時点では DOM ツリーはすでに完成していますので、どの要素も参照することができます。

window オブジェクトの onload プロパティに対するイベントハンドラとして HTML ページ内の要素に対するイベント処理を行う JavaScript のコードを記述することで、 JavaScript のコードの実行を load イベントが発生したあとまで延期することができます。

<head>
<script>
    window.onload = function(){
        let button = document.getElementById('xxx');
        button.onclick = function(){
            console.log('How are you?');
        };
    };
</script>
</head>

<body>
    <input type="button" value="button" id="xxx">
</body>

load イベントが発生すると、イベントハンドラとして登録したコードが実行され、ターゲットの要素の取得と click イベントへのイベントハンドラの登録が実行されます。

サンプルコード

次のサンプルを見てください。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>サンプル</title>

<script src="./main.js"></script>

</head>
<body>

<p>ボタンをクリックしてください。</p>

<input type="button" value="button" id="xxx">

<p>ログに文字列が出力されます。</p>

</body>
</html>
main.js
window.onload = function(){
    let button = document.getElementById('xxx');
    button.onclick = function(){
        console.log('How are you?');
    };
};

ボタンをクリックすると 'How are you?' をコンソールに出力します。

loadイベントを利用する(1)

scriptタグでdefer属性を設定する

最後の方法は script タグで defer 属性を設定する方法です。この方法は JavaScript のコードを記述した外部のファイルを読み込む場合にだけ利用できます。

<head>
<script defer src="./main.js"></script>
</head>

<body>
    <input type="button" value="button" id="xxx">
</body>
main.js
let button = document.getElementById('xxx');
button.addEventListener('click', function(){
    console.log('Bye');
});

defer 属性を設定された script タグで読み込まれた外部の JavaScript のファイルに記述されたコードは、すぐに実行されるのではなく DOM ツリーの構築が完了し DOMContentLoaded イベントが発生する直前に実行されます。

コードが実行されるときには DOM ツリーが完成しておりどの要素も参照できますので、特別なコードを追加することなくイベントリスナーやイベントハンドラの登録を行うこととができます。

※ defer 属性について詳しくは「JavaScriptの外部ファイルを非同期で読み込む(async属性,defer属性)」を参照されてください。

サンプルコード

次のサンプルを見てください。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>サンプル</title>

<script defer src="./main.js"></script>

</head>
<body>

<p>ボタンをクリックしてください。</p>

<input type="button" value="button" id="xxx">

<p>ログに文字列が出力されます。</p>

</body>
</html>
main.js
let button = document.getElementById('xxx');
button.addEventListener('click', function(){
    console.log('Bye');
});

ボタンをクリックすると 'Bye' をコンソールに出力します。

scriptタグでdefer属性を設定する(1)

-- --

イベントハンドラを登録するコードの記述位置に関する注意と対象方法について解説します。

( Written by Tatsuo Ikura )

Profile
profile_img

著者 / TATSUO IKURA

プログラミングや開発環境構築の解説サイトを運営しています。