JavaScript中級者への道!クリックされたら動くの巻

JavaScript
JavaScript

3月は個人的にJavaScript集中強化月間としてお勉強していきます。
今回はJavaScriptで画面イベントをトリガーに処理を実行する方法をまとめていきます。

こちらのJavaScriptロードマップも併せてご覧ください。

目指せ中級!JavaScript学習ロードマップ(非webエンジニア向け)
当ブログの筆者のinashunです。 私はJavaをメインの技術としてお仕事をしているのですが、最近お仕事でJavaScriptを扱う機会が増えてきました。 これまでも扱う機会は多々あったのですが、すべて「既存ソースの改修」か「実装...

イベントとは

イベントとは、Webページ上で発生する様々な動作の総称です。例えば「ページが読み込まれた」「ボタンがクリックされた」「部品にカーソルが乗った」「スクロールされた」などです。
JavaScriptでは、とある要素でとあるイベントが発生したという情報を受け取って、その時に任意の処理を行うというプログラムを作成することができます。

ブラウザ上で動作するJavaScriptの処理は、このようなイベントをトリガーとして動く場合が多いです。
ここをマスターすると、一気にJavaScriptでできることが増えていきます。頑張って覚えていきましょう!

JavaScriptでイベントを拾う

JavaScriptでのイベント処理を身に付けるうえで重要になる3つの登場人物(人物??)がいます。
それは「要素」「イベント」「処理」の3つです。
要素はそのイベントが発生する画面上のボタンなどの部品のことです。
イベントは画面上で起こるイベントそのものです。「カーソルが乗る」とか「クリック」などです。
処理はイベント発生時に実行したいプログラムのことを指します。

2パターンの方法でイベント処理の実装方法を紹介しますが、どちらもこの3つの要素を意識して作成する必要があります。逆に言うと、この3つの要素でイベント処理が成り立っていることが理解できれば、どの方法でも容易にイベント処理を実装することができます!

早速イベント処理の実装方法をお勉強していきましょう!

イベントハンドラープロパティ

こちらは、イベントが発生する要素のノードオブジェクトのプロパティに、イベント発生時の処理を関数として持たせる方法です。
下記の例は、ボタンがクリックされるたびに「あ」の字が増えて、ランダムで色が変わるというプログラムです。

<body>
    <button>Push Me!</button>
    <p>あ</p>
</body>
// 要素を取得
const btn = document.querySelector('button');
const p = document.querySelector('p');

// ボタンクリック時の実行関数
const random = (number) => {
    return Math.floor(Math.random() * (number+1));
};

const addStrA = (element) => {
    element.innerText += "あ";
};

const onclickFunc = () => {
    p.style.color = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
    addStrA(p);
};

// イベントハンドラープロパティに処理を格納
btn.onclick = onclickFunc;
  • よかったらコピペして動かしてみてください!とてもしょーもないです!

この例では、ボタンのノードオブジェクトbtnonclickイベントハンドラープロパティに、クリック時に実行させたい処理であるonclickFunc関数を設定しています。
最初に紹介した3人の登場人物に当てはめると、要素がbtn、イベントがonclick、処理がonclickFuncとなります。

addEventListener

続いてはaddEventListener()というメソッドを使って同様の処理を実装する方法を紹介します。こちらの記法の方が新しくより多くのことができるようになっています。ですが、おおよその目的と考えることはイベントハンドラープロパティでの書き方と同じなので、覚えるのはさほど難しくないと思います。

イベントハンドラープロパティで作成した例と同様の動きをするプログラムのサンプルです。

<body>
    <button>Push Me!</button>
    <p>あ</p>
</body>
// 要素を取得
const btn = document.querySelector('button');
const p = document.querySelector('p');

// ボタンクリック時の実行関数
const random = (number) => {
    return Math.floor(Math.random() * (number+1));
};

const addStrA = (element) => {
    element.innerText += "あ";
};

const onclickFunc = () => {
    p.style.color = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
    addStrA(p);
};

// メソッドを利用して属性にイベントリスナーを設定
btn.addEventListener("click", onclickFunc);

一番最後の属性に処理を設定する書き方のみが変化しています。属性.addEventListener("イベント", 処理);という記法で設定ができます。

イベントハンドラープロパティを用いた方法との主な大きな違いは、

  • イベントリスナーを削除するremoveEventListener()メソッドと併用して、設定した処理を削除することができる
  • 同じ属性の同じイベントに対して、複数の処理を設定することができる
    (イベントハンドラーだと上書きされてしまう)

といったところになります。(細かい点では他にもいろいろ違いがあります)
拡張性を考えると、こちらのaddEventListener()を使うことを基本として考えて問題なさそうですね。

  • IE8以前ではaddEventListener()は互換性がなく動きません。もし、それらをターゲットブラウザとしている場合はイベントハンドラープロパティを利用することになります。とりあえずご愁傷様です。。。

イベント一覧

イベントハンドラープロパティおよびaddEventListener()にてよく使いそうなイベントの書き方を一覧で紹介します。ここに記載するのはすべてではないのであしからず。。。

イベント名(addEventListener)イベントハンドラープロパティどんなイベント?
loadonloadページ全体が読み込まれたときに発生
(DOMContentLoadedイベントと使い分ける)
beforeunloadonbeforeunload要素がアンロードされる直前に発生
focusonfocusある要素にフォーカスがあたったときに発生
bluronblurある要素がフォーカスを失ったときに発生
submitonsubmitformのsubmitボタンが押下されたときに発生
clickonclickある要素がクリックされたときに発生
mouseoveronmouseoverある要素の上にカーソルが移動したときに発生
mouseoutonmouseoutある要素の上からカーソルが移動したときに発生
keydownonkeydown任意のキーが押下されたときに発生
参考

イベントリファレンス | MDN
ほぼすべてのイベントが載っていますが、リンクが切れまくってるので注意です。。。

JavaScriptの関数はオブジェクト

イベントの拾い方にて「イベント発生時に実行したい処理を関数にして変数に詰めてプロパティとして渡す」といったことを上記の例では行っていました。
この挙動をちゃんと理解するためには、JavaScriptでの「関数」というものの扱いについて理解しておかなければいけません。以前別の記事でも簡単に触れたのですが、改めておさらいしておきます。

JavaScriptの基礎の文法をまとめる
案件でWebアプリの構成の検討のためにサンプルアプリを作っていたのですが、Javaのソースならさらさらっと書けるけど、JavaScriptとなると「文法がよくわからん」となって初歩的なことを調べることが多かったです。Webアプリエンジニアと...

↑その時の記事です

といっても結論は見出しの通りなのです。JavaScriptでは1つの関数1つのオブジェクトとして扱われます。なので、関数単体で変数に格納して使うことができます。

function hoge() {
    console.log("hogeeeeeeeeeeeeee");
}

const fuga = hoge;
fuga(); // hogeeeeeeeeeeeeee

他のオブジェクト指向の言語、例えばJavaだと「属性+処理=クラス」でこのクラス単位でオブジェクトとして扱われるので、処理だけがオブジェクトになって自由にあっちに行ったりこっちに行ったりするというのは違和感につながりやすいのかなと思います。(実際に私も最初はナニソレイミワカンナイ!ってなってました。。)
ですが受け入れてください!このことを理解するだけでもJavaScriptが一気に扱いやすくなります。

覚えることは単純です。1つは「JavaScriptにおける関数はオブジェクトであること」、もう1つは「関数オブジェクトを格納した変数に()をつけて呼び出すとその関数が実行される」ということです。
後者について簡単な例を紹介します。

function add(a, b) {
    return a + b;
}

const func = add;       // ()がなければ関数オブジェクトを代入
const num = add(3, 5);  // ()があれば関数を実行して戻り値を代入

console.log(func);      // function add(a, b) ...
console.log(num);       // 8

このように、()をつけるかつけないかによって関数を実行するかどうかの挙動が変化します。ちなみに、数字や文字列などの関数ではないオブジェクトの変数に対して()をつけてしまうと、実行時に「これは関数じゃないから実行できないよ!」というエラーが発生します。

オブジェクト内の関数

複数のデータをまとめたオブジェクトを作成することができますが、その中にも関数を含めることができます。JavaScriptでは、オブジェクトのプロパティとして設定されている関数のことをメソッドと呼びます。
簡単な例を下記に示します。

const obj = {
    str : "hoge",
    method1 : function () {
        console.log(this.str);
    }
};

obj.method1();  // hoge

objオブジェクトの中に、method1メソッドを定義しています。呼び出す際は、obj.method1();のようにオブジェクト名とメソッド名をドットで繋げて呼び出しています。

メソッドには短縮記法を用いることができます。下記の例の通りです。

const obj = {
    str : "hoge",
    method1 () {
        console.log(this.str);
    }
};

obj.method1();

functionキーワードがなくなり、メソッド名()という形式で定義できます。より直感的で読みやすく書きやすいですね。

アロー関数

上記の例では、function hoge() {というように、function句を利用した関数の宣言の方法を用いていました。
JavaScriptのES6というバージョンから、新たにアロー関数という記法が使えるようになりました。こちらの記法だと「定義した関数を名前付きの変数に代入して使う」という感覚を持ちやすくてよいです。他にも利点はいろいろありますが、とりあえずモダンだからという理由でこちらを基本的に使っていくことをオススメします。

下記の例にて記法を紹介します。

const add = (a, b) => {
    return a + b;
};

上の例のfunction add(a, b) {をアロー関数に置き換えるとこのようになります。アロー=>が用いられていることがわかりますね。

コールバック関数

JavaScriptの関数の特性が活かされている仕組みが、いわゆるコールバック関数というものです。ざっくり説明すると、「他の関数に渡して実行してもらうための関数」がコールバック関数です。
関数はオブジェクトなので、変数に格納できるのと同じように、別の関数の引数として渡すことができます。引数で受け取った関数は、そのまま()をつけて呼び出すことで実行することができます。
引数で関数を受け取り処理の中でそれを実行する関数を高階関数と呼びます。

const callback = (str) => {
    console.log(str);
}

const higherOrderFunction = (func) => {
    func("One");
    func("Two");
    func("Trhee");
}

higherOrderFunction(callback); // One Two Three

callback()は引数に文字列を受け取る関数です。higherOrderFunction()は引数に関数を受け取る高階関数です。受け取った関数を処理の中で3回呼び出しています。higherOrderFunction()を呼び出す際に引数にcallbackを渡しています。これにより、3回のコールバック関数呼び出しの引数それぞれがconsole.log()によってログ出力されます。

このようなコールバック関数は、非同期処理に使われることが多いです。例えばAjaxを用いてサーバと通信する関数なんかだと、通信が完了した後に、受け取ったデータを画面に反映する処理をコールバック関数としてAjax関数に渡して実行してもらう、なんていう処理を行うことがよくあります。

関数はちゃんと変数に詰めておこう

コールバック関数を定義するときに、下記のように無名関数を引数の位置にそのまま定義して渡すことができます。

const higherOrderFunction = (func) => {
    func("One");
    func("Two");
    func("Trhee");
}

higherOrderFunction((str) => {
    console.log(str);
}); // One Two Three

先ほどよりもスマートに書けた感じがしますね。世の中的にもこの書き方が主流なのかなと思っています。
これは個人的な見解なのですが、この書き方はオススメしません!
これには3つの理由があります。

  1. 括弧(){}が増えて(とくにJavaScriptに熟練していない人から見て)理解しづらいコードになる。特にお尻の});なんてJavaエンジニアから見たら「何事?!」ってなる。
    先に関数が変数に入っていれば、そのことだけ受け入れられたらやりたいことは理解できる。
  2. 例えば、イベントリスナーを削除するremoveEventListener()メソッドは、addEventListener()したときに無名関数だと使えない、といった無名関数だからこその不都合があったりする。
  3. そもそも「無名」ってのがもうナンセンス、オブジェクト指向言語のくせにプログラムにわかりやすい名前をつけるチャンスをみすみす失っている。関数に適切な名前を付けてあげれば、それだけで可読性が向上するし、再利用の可能性を広げる。

書くときは無名関数の方が楽かもしれないけど、可読性ではちゃんと名前がついた関数を作成するほうに軍配が上がると思います。あなたが楽かどうかはどうでもいいのです!最優先すべきは可読性であることは不変の原則なので、ちゃんと名前を付けて関数を定義し、その名前を引数として渡してあげるようにしましょう。

高階関数を作るときの注意点

引数として関数オブジェクトを受け取る高階関数を作成して、例えばその開発プロジェクト内で共通的に使用するといった場合は、必ずドキュメントを整備しましょう!
引数として渡す関数はどのようなタイミングで呼び出されるのか、呼び出されるときの引数は何が渡されるのか、どんなことをするために関数を渡すべきなのかをドキュメントにわかりやすく記してその関数を使用する開発者が見える範囲に公開します。使用する側は、このドキュメントを参照することで高階関数の内部のロジックを意識することなく開発を行うことができます。
ドキュメント整備は開発中は面倒だと感じることが多いと思いますが、後からその効果をどんどん発揮していきます。どうか面倒だという気持ちをグッとこらえて、ちまちまとドキュメント整備しましょう。

JavaScriptの「this」

ここまで、JavaScriptのイベント操作、そしてそれを実現する重要な要素であるJavaScriptの関数オブジェクトのことについて学んできました。
これらの要素に深くかかわるもう一つ重要な要素である、JavaScriptのthisキーワードについて簡単にまとめたいと思います。

JavaScriptのthisは他の言語と違い独特な動きをします。さわりだけでも理解しておくとコードが読みやすくなるのではないかと思います。

関数(function)の中のthis

thisの挙動は、主にアロー関数とそれ以外全般で分かれます。ここではまずアロー関数以外の全般の関数(メソッドも含む)のthisの挙動について紹介します。(以降、対象の関数のことをすべてひっくるめて関数と呼びます)

関数におけるthisの値はベースオブジェクトとなります。ベースオブジェクトとは、「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことを指します。

例を挙げると、obj.method()のようにメソッドを呼び出した場合、methodの中で使われているthisにはobjが格納されることになります。
一方で、func()というようにオブジェクトに含まれていない関数をそのまま呼び出したときには、thisundefinedとなります。ひとつ左が何もないのでそうなると考えると納得ですね。

const obj = {
    str : "hoge",
    method1 : function () {
        console.log(this.str);
    }
};

obj.method1();  // hoge

オブジェクト内の関数の項での実装例ですが、この中でthisが使われています。
この場合は、thisの中にはobjオブジェクトが格納されていることになるので、そのオブジェクト内のstrプロパティの値をログに出力している、と読むことができますね。

ここまでであれば、thisは他の言語と何ら変わらない便利なキーワードのように思えますが、いくつかの問題点を抱えています。ここからはその問題点とその対策を見ていきましょう。

問題点① メソッドを変数に代入

問題点の一つは、thisを含んだメソッドが別の変数に代入されて呼び出される場合です。
たとえオブジェクトの中のメソッドであろうと、JavaScriptでは関数はオブジェクトでありそれのみで取り出すことができたりするので、こういった問題が発生します。

const obj = {
    str : "hoge",
    method1 : function () {
        console.log(this.str);
    }
};

const func = obj.method1;
func();  // undefined

上記の例では、objオブジェクトのmethod1メソッドを変数funcに代入して呼び出しています。funcに入っている関数内ではthisを使用していますが、呼び出した際にはベースオブジェクトがない状態なので、その部分はundefinedとなってしまい、意図したとおりの動作となりません。

この問題の最も有効な解決策は「メソッドはメソッドとして使う」ことです。そうであればこのような問題は起こりません。ですが、どうしてもという場合には、this値を指定して関数を呼ぶことができるメソッドを使用することができます。

call・apply・bindメソッド

これらのメソッドは、thisの値を指定して関数を呼び出すためのものです。

callメソッドは、thisの値を指定して即時に関数を実行します。

const obj = {
    str : "hoge",
    method1 : function () {
        console.log(this.str);
    }
};

const func = obj.method1;
func.call(obj);  // hoge

関数オブジェクトのメソッドとして呼び出します。func.call(obj);というように、引数にthisとして認識してほしい値を渡します。他にその関数自体の引数がある場合は、func.call(obj, "hoge", fuga);というように、thisを指定する引数の後ろに順番を合わせて設定します。(可変長引数となっています)
もしthisが不要な場合は、慣例的にnullを引数として渡します。

applyメソッドはcallメソッドとほとんど同様の動きをします。違いは、関数自体の引数の渡し方が、callメソッドであれば可変長引数だったのが、applyメソッドだと配列で渡すようになります。

bindメソッドは、thisの値や引数を束縛した新しい関数を取得するために使用します。

const obj = {
    str : "hoge",
    method1 : function () {
        console.log(this.str);
    }
};

const func = obj.method1.bind(obj);
func();  // hoge

既にobjオブジェクトがバインド済みの状態で変数funcに関数が格納されるため、それをそのまま呼び出してもthisがちゃんと働いている状態となります。
関数を使いまわそうとするときには有効な選択肢になると思います。

ちなみに、thisではない引数に関しては、バインドをしてもしなくてもどちらでも構いません。バインドされている分の引数は実行の呼び出し時には設定する必要はありませんし、バインドされていない分の引数は実行時に設定することで動作します。かなり都合よく動くので意外と出番はあるかもしれません。

問題点② コールバック関数の中のthis

コールバック関数の中でthisを使用した場合、想定したとおりの結果とならない場合があります。

const obj = {
    str : "hoge",
    method1 (func) {
        func("fuga");
    },
    callback () {
        this.method1(function (argStr) {
            console.log(this.str + argStr);
        });
    }
};

obj.callback();   // undefinedfuga

hogefugaという結果を期待したいところですね。コールバック関数内で記述されているthis.strは、objオブジェクトのstrプロパティを狙って書かれているものですが、その意図通りには動きません。
何故なら、コールバック関数の呼び出し時(func("fuga");の部分)にはドットの左側、所謂ベースオブジェクトがない状態だからです。ベースオブジェクトがない状態でのthisundefinedになります。

いくつかの対処法があります。まず一つはthisを変数に代入する方法です。

const obj = {
    str : "hoge",
    method1 (func) {
        func("fuga");
    },
    callback () {
        const that = this;
        this.method1(function (argStr) {
            console.log(that.str + argStr);
        });
    }
};

obj.callback();    // hogefuga

明示的にthisを変数に詰める場合は慣例的にconst that = this;というようにthatという変数名にします。このようにthisの値を、同じ値を表す別の変数に入れてしまってそちらを使うという方法です。

他の対処法として、先ほど紹介したbindメソッドを使用することもできます。

const obj = {
    str : "hoge",
    method1 (func) {
        func("fuga");
    },
    callback () {
        this.method1(function (argStr) {
            console.log(this.str + argStr);
        }.bind(this));
    }
};

obj.callback();     // hogefuga

コールバック関数の定義の後ろに.bind(this)を付与することで、コールバック関数の中のthisの値を束縛した状態で高階関数に渡しています。こちらの方法の方がスマートな感じがしますね。
また、例えばArray#mapメソッドなどの引数にコールバック関数を取る高階関数の中には、そのメソッドの引数としてthisの値を受け取れるようになっているメソッドも存在します。bindメソッドを使う要件がある場合には、先に高階関数の方にthisを受け取る引数がないか確認してみるといいかもしれません。

アロー関数の中のthis

実はもう一つ、コールバック関数のthis問題をスマートに解決してくれる方法があります。

const obj = {
    str : "hoge",
    method1 (func) {
        func("fuga");
    },
    callback () {
        this.method1((argStr) => {
            console.log(this.str + argStr);
        });
    }
};

obj.callback();     // hogefuga

そうです!コールバック関数をアロー関数で書くことで、thisの値が変化してしまうことを意識することなく使用することができるようになります!

アロー関数の中でのthisの値は、「アロー関数自身の外側のスコープに定義されたもっとも近い関数のthisの値」となります。上記の例でみると、それに該当するのはcallbackメソッドとなります。このメソッドを呼び出すとしたらobj.callback();と呼び出すことになるので、thisの値はobjオブジェクトになります。

重要なことは、アロー関数のthisはコードが書かれたときにもう決まっている、言い換えると通常のfunctionのように実行中にthisの値が変化したりしないということです。ですから、たとえコールバック関数として高階関数に渡されて実行されたとしても、thisの値は想定通りのものであり続けるので、安心してthisを使うことができます

ちなみに、アロー関数ではcallメソッドやbindメソッドを使用しても、thisの値を変えることはできません。これらのメソッドは、その関数オブジェクト自体のthisの値をコントロールしようとするのに対し、アロー関数はそれ自体はthisの値を持たないからです。(自分で持たないから一つ外側の関数のthisを参照するような動きになります。)

まとめ

この記事では、JavaScriptでイベントを拾って処理を実行する方法を学び、それを実現するために知っておきたいコールバック関数等のJavaScriptの関数についても勉強しました。さらにはちょっと理解が難しいJavaScriptのthisキーワードについても簡単に学習しました。
かなり内容が盛りだくさんになってしまいましたが、ここを乗り越えればほぼJavaScript中級を名乗れるのではないでしょうか。

この記事で覚えておきたいこと
  • イベント処理を理解する大事な要素は「要素」「イベント」「処理
  • イベント処理を実装する方法は「イベントハンドラープロパティ」「addEventListener
  • JavaScriptの関数オブジェクトだから関数単体で変数に入れられる!引数として関数を渡せる!
  • 引数で関数を受け取る関数は高階関数!引数として渡されて実行される関数はコールバック関数
  • オブジェクトのメソッドthisはそのオブジェクトを指す!
  • コールバック関数はアロー関数で定義するとthisの値が勝手に変化しない!
  • メソッドでない通常の関数ではthis使わないようにしよう!

参考

JavaScriptの「コールバック関数」とは一体なんなのか
JavaScript超基礎講座!イベント処理を学ぶ
イベントへの入門 – web開発を学ぶ | MDN
this – JavaScript | MDN
関数とthis・JavaScript Primer #jsprimer

コメント

タイトルとURLをコピーしました