メヘンニミン

たくさんの言語に触れたい.走り書きで見る整理ブログです.

豆になるJS: イベントリスナーとthisと引数と

JavaScriptに関する初級~中級Tipsをいくつか書いていたのですが, 話題が膨らんじゃったので分離しました.

はじめに

ブラウザで動作するJavaScriptthisを用いた場合, 基本的にはグローバルオブジェクトであるwindowオブジェクトを指します. しかし,たま~~にthisの中身が異なる場合があります.

例えば,Web制作に扱われやすいマウスのイベントリスナー内のお話. イベントリスナーといえば,引数もどう設定するんだ!という疑問も出てくるんじゃないかと思います.

今回の議題を整理しましょう.

  • イベントリスナー内のthisを制御するには?
  • イベントリスナーに引数を与えるには?

thisを理解する

まずは,thisの挙動について整理しましょう.

  • (A) 関数内のthisは,グローバルオブジェクトのままである
this.val = 2;
function get() {
  console.log(this.val); // (A) 関数内なので2を出力
}
get();
  • (B) メソッド内のthisは,そのメソッドが属するオブジェクトを指す
this.val = 2;
var obj = {
  val: 3,
  get: function() {
    console.log(this.val); // (B) メソッド内なので3を出力
    var func = function() {
      console.log(this.val); // (A) 関数内なので2を出力
    };
    func();
  }
};
obj.get();

上記(A)(B)の違いは非常にヤヤコシイ.(B)メソッド内のthisと,メソッド内で宣言された(A)関数のthisの参照先は異なります. つまり,(A)関数内,(B)メソッド内のどちらにthisがあるかが重要です. なお,ES2015から使用できるアロー関数は,定義時のスコープにあるthisを参照するため, 上記では(B)に近い挙動を取ります.

  • (C) コンストラクタから呼び出したthisは,そのコンストラクタが生成したオブジェクトを指す

クラス内のthisはそのクラス(オブジェクト)自身を示す,という言い方になるでしょうか. JavaScriptではprototypeを用いて(擬似的に)クラスを生成していましたが,ES2015ではClass構文を用います.

class Person {
  constructor(name, gender) {
    this.name = name;
    this.gender = gender;
  }
  getName() {
    return this.name;
  }
}

これならわかりやすいですね. クラスの書き方に馴染みがあれば,他の言語を書いている方でも「あ,これか!」となると思います.

本題

さて,ここでマウスのイベントリスナーについて見てみましょう.

this.val = 2;
function myEventListener(e) {
  console.log(this.val) // undefined
}
function func() {
  var name = "Taro";
  var btn = document.getElementById("btn");
  btn.addEventListener("click", myEventListener); // イベントリスナーを設定
}

イベントリスナーとは,とあるクラスの(B)メソッドです.詳細.

イベントリスナーとは : JavaA2Z

したがって,myEventListener内のthisは[object HTMLInputElement], つまりイベントの発生源であるid="btn"の要素を指すことになります.

さぁお題だ.

  • myEventListener内でthis.valをどう参照するか?
  • myEventListener内でfunc内のnameをどう参照するか?

thisを退避させる

最初の解決策は,thisを先に確保しておくことです.

this.val = 2;
var _this = this; // 退避
function myEventListener(e) {
  console.log(_this.val) // 2
}
function func() {
  var name = "Taro";
  var btn = document.getElementById("btn");
  btn.addEventListener("click", myEventListener);
}

_thisにはそのタイミングのthisが確保されているため,どこで使おうともとも参照先は変わりません. この手法自体は,先ほどの関数・メソッドの混同問題でも用いられる有用なテクニックの1つです.

このように,より広いスコープ内で宣言した変数に退避させておくことで, nameについてもうまく参照することができそうです.

無名関数を用いる

イベントリスナーに無名関数やアロー関数を指定する方法があります.

this.val = 2;
function myEventListener(e) {
  console.log(this.val) // 2
}
function func() {
  var name = "Taro";
  var btn = document.getElementById("btn");
  btn.addEventListener("click", function(e){myEventListener(e)}); // 無名関数を使用
}

何故うまくいくのか?この動きは,先ほどの(A)(B)の話につながります.

無名関数function(e)はイベントリスナーとして機能するため, この中で使われるthisは(B)メソッド内のthis,つまりid="btn"の要素を参照します. しかし,myEventListenerはただの(A)関数として実行されるため, myEventListener内のthisはグローバルオブジェクトを指すことになります.

この方法では,myEventListenerにe以外の引数を指定することもできますね.

  btn.addEventListener("click", function(e){myEventListener(e, name)});

call, apply, bindメソッドを用いる

もう少し冗長にならない書き方で,call, apply, bindメソッドでthisを束縛する方法があります.

this.val = 2;
function myEventListener(e) {
  console.log(this.val) // 2
}
function func() {
  var name = "Taro";
  var btn = document.getElementById("btn");
  btn.addEventListener("click", myEventListener.bind(this)); // bindを使用
}

上記は,myEventListener関数について,関数内のthisがグローバルオブジェクトを指すように強制します. 具体的には,myEventListener関数内におけるthisを,func内におけるthisに置き換えます

bindは引数を束縛することもできますが,Eventオブジェクトも残したい場合は,仮引数の順序に注意してください.詳しい動きはリファレンスを参照.

this.val = 2;
function myEventListener(val0, val1, e) { // 順序に注意
  console.log(this.val) // 2
  console.log(val0, val1) // "Taro", "Jiro"
}
function func() {
  var names = ["Taro", "Jiro"];
  var btn = document.getElementById("btn");
  btn.addEventListener("click",
    myEventListener.bind(this, names[0], names[1]) // 引数を与える
  );
}

bindといえば,React.jsのフォームでもよく見かける書き方ですね. イベントリスナー内でthis.setStateを使いたい場合などによく用いられます.

Reactをes6で使う場合のbindの問題 - Qiita

removeEventListenerを使用する場合

ところで,イベントリスナーのやりくりの中では, addEventListenerとremoveEventListenerで指定するリスナーを一致させる必要があります.

btn.addEventListener("click", myEventListener);
btn.removeEventListener("click", myEventListener);

無名関数やbindの書き方でremoveEventListenerを扱いたい場合は,削除すべきリスナーを記憶しておきましょう.

var clickListener = myEventListener.bind(this);
func() {
  btn.addEventListener("click", clickListener);
  btn.removeEventListener("click", clickListener);
}

おわりに

thisの挙動について,下記の書籍を参考にしました.

JavaScriptはいわゆる小ネタが豊富ながら,議論点・注意点も多い言語です.配列とか.未定義とか.

記述の至らぬ点はご指摘ください.