吉本集の個人ブログ
Web制作の技術について書いています。たまに日記も書きます。

【JS入門】イベントについて Vol.2(スコープ、引数、イベント伝播を止める)

2018年2月11日 / category : javascript

イベントハンドラのスコープについて

イベントハンドラのスコープについて解説します。まずは、次のコードを見てみます。

sample.js
var message = 'お腹減った。';
function debug(){
	console.log('this : ', this); // this : Window {...}
	console.log('this.message : ', this.message); // this.message : お腹減った。
};
debug();

コンソール画面

this : Window {...}
this.message : お腹減った。

まずはthisの復習ですが、過去の記事「【JS入門】コンストラクタ関数について」で、「thisは関数内で使用される特殊なキーワードで、関数が実行された際にその関数が属しているオブジェクトを指します。」と解説しました。上の結果は想像できたと思いますが、関数debugの中のthisは、その関数が属しているオブジェクトが、トップレベルであるWindowオブジェクトに属している(Windowオブジェクトで定義している)ため、thisはWindowオブジェクトを指します。また、変数messageもWindowオブジェクトで定義しているので、this.messageで参照することができます。

では、次のコードではどうでしょうか。

sample.js
var message = 'お腹減った。';
function debug(){
	console.log('this : ', this); // <button id="btn">ボタン</button>
	console.log('this.message : ', this.message); // undefined
};
let btn = document.getElementById('btn');
btn.addEventListener('click', debug);

コンソール画面

this : <button id="btn">ボタン</button>
this.message : undefined

ボタンをクリックすると関数debugが実行されます。先ほどとthisの参照が変わり、this.messageがundefinedになりました。これは、関数debugが属しているオブジェクトが先ほどのトップレベル(Windowオブジェクト)からbuttonオブジェクト(DOMオブジェクト)に変わったためです。
thisについては、次回以降の記事でまとめて整理して解説したいと思いますので、イベントハンドラのスコープの解説に関しては、ここまでとします。

イベントハンドラの引数について

イベントハンドラには、引数を渡すことができます。次のコードを見てみます。

sample.js
let btn = document.getElementById('btn');
btn.addEventListener('click', function(e){ console.log(e); });

コンソール画面

MouseEvent {...}

上のコードは、イベントハンドラに無名関数を定義したコードです。第1引数に引数eが設定され、コンソールで出力しています。コンソールを見ると、MouseEventオブジェクトが出力されました。これはインターフェースですので、以降MouseEventインターフェースと呼びます。

sample.js
console.log( MouseEvent.__proto__ ); // UIEvent() { [native code] }
console.log( UIEvent.__proto__ ); // Event() { [native code] }
console.log( Event.__proto__ ); // () { [native code] }

MouseEventインターフェースを調べると上のように、UIEventインターフェースを継承し、さらにEventインターフェースを継承したインターフェースということがわかりました。
イベント系のインターフェースは他にも、キーボードに対するユーザー動作のKeyboardEventインターフェースや、マウスホイールに対するユーザー動作のWheelEventインターフェースなどがあります。

sample.js
function debug(e){
	console.log(e.target); // <button id="btn">ボタン</button>
	console.log(e.target === this); // true
	console.log( 'ローカルx座標', e.clientX ); // 34
	console.log( 'ローカルy座標', e.clientY ); // 16
	console.log( 'グローバルx座標', e.screenX ); // 34
	console.log( 'グローバルy座標', e.screenY ); // 137
};
let btn = document.getElementById('btn');
btn.addEventListener('click', debug);

上のコードは、先ほど無名関数で定義したイベントハンドラを関数debugに置き換え、MouseEventインターフェースが保持しているプロパティをいくつか出力しています。プロパティtargetは、クリックしたイベントターゲット(オブジェクト)が参照されます。この場合は、button要素になりますので、thisが参照するオブジェクトと同じ参照になります。その他、マウスの座標系を参照するプロパティをいくつか定義されています。

イベント伝播(イベントバブリング)を止める

前回の記事「イベントについて Vol.1(イベントハンドラの登録、イベント伝播)」で、イベント伝播(イベントバブリング)について解説しましたが、これを先ほどのイベントハンドラの引数で渡しているMouseEventオブジェクトで定義されているメソッドを使用することで、イベント伝播(イベントバブリング)を止めることができます。まずは次のコードを見てみます。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>タイトル</title>
</head>
<body>
<div id="inner">
<p><button id="btn">ボタン</button></p>
</div>
<script src="./js/sample.js"></script>
</body>
</html>
sample.js
let inner = document.getElementById('inner');
let btn = document.getElementById('btn');

function debug(e){
	console.log('クリックしました。');
	console.log('target', e.target);
	console.log('this', this);
	console.log(e.target === this);
}

inner.addEventListener('click', debug);
btn.addEventListener('click', debug);

コンソール画面

クリックしました。
target : <button id=​"btn">ボタン​</button>
this : <button id=​"btn">ボタン​</button>
true
クリックしました。
target : <button id=​"btn">ボタン​</button>
this : <div id=​"inner">…​</div>
false

メソッドaddEventListenerの第3引数の設定がありませんので、キャプチャフェーズの設定は無しの状態です。そのため、イベントのバブリングは最初にターゲットフェーズが発生し、次にバブリングフェーズが発生しますので、最初にid属性にbtnを持つbutton要素、次にid属性にinnerを持つdiv要素の順にイベントハンドラが実行されます。コンソール画面を確認すると、その通りの順で出力されています。これは前回の記事「イベントについて Vol.1(イベントハンドラの登録、イベント伝播)」で解説しました。イベントハンドラの引数にはMouseEventオブジェクトが設定されており、プロパティtargetにはイベントを発生させたイベントターゲットが参照されるので、button要素(DOMオブジェクト)が参照されることになります。最初はbutton要素で設定したイベントハンドラが実行されますので、プロパティtargetとthisは同じ参照になります。そのため、比較はtrueになります。次はdiv要素で設定したイベントハンドラが実行されますので、プロパティtargetとthisが参照しているオブジェクトは異なります。

このイベント伝播(イベントバブリング)を止め、button要素のイベントハンドラのみを実行させる場合は、次のようなコードにします。

sample.js
let inner = document.getElementById('inner');
let btn = document.getElementById('btn');

function debug(e){
	e.stopPropagation();
	console.log('クリックしました。');
	console.log('target : ', e.target);
	console.log('this : ', this);
	console.log(e.target === this);
}

inner.addEventListener('click', debug);
btn.addEventListener('click', debug);

コンソール画面

クリックしました。
target : <button id=​"btn">ボタン​</button>
this : <button id=​"btn">ボタン​</button>
true

メソッドstopPropagationの実行を追加しました。このメソッドは、MouseEventインターフェースが継承しているEventインターフェースで定義されているメソッドです。このメソッドを実行することで、イベント伝播(イベントバブリング)を止めることができます。

ブラウザが持っているデフォルトのイベントをキャンセルする

Eventインターフェースで定義されているメソッドpreventDefaultを使用することで、ブラウザが持っているデフォルトのイベントをキャンセルすることができます。次のコードを見てみます。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>タイトル</title>
</head>
<body>
<div id="inner">
<a href="http://yahoo.co.jp" id="link">リンクテキスト</a>
</div>
<script src="./js/sample.js"></script>
</body>
</html>
sample.js
let inner = document.getElementById('inner');
let link = document.getElementById('link');

function prevent(e){
	e.preventDefault();
}

function debug(){
	console.log('クリックしました。');
}

inner.addEventListener('click', debug);
link.addEventListener('click', prevent);

コンソール画面

クリックしました。

リンクテキストをクリックしても、a要素のhref属性で設定しているURLに遷移しません。これはa要素に対しメソッドaddEventListenerでクリックした際にEventインターフェースのメソッドpreventDefaultが実行されるイベントハンドラを登録したためです。a要素をクリックするとhref属性に設定したURLにページが遷移するというのがブラウザが持つデフォルトのイベントです。これがメソッドpreventDefaultによってキャンセルされました。ただし、イベント伝播(イベントバブリング)は止まっていません。コンソールを見ると、div要素に設定したイベントハンドラが実行されています。

同じ要素に登録した他のイベントを止める

メソッドstopPropagationはイベント伝播(イベントバブリング)を止めるメソッドで、メソッドpreventDefaultはブラウザの持っているイベントをキャンセルするメソッドですが、これらのメソッドではもし同じ要素に複数のイベントハンドラを登録していた場合、止めることはできません。次のコードを見てみます。

sample.js
let inner = document.getElementById('inner');
let link = document.getElementById('link');

function prevent(e){
	e.preventDefault();
	e.stopPropagation();
}

function debug(value){
	console.log(value);
}

inner.addEventListener('click', function(){ debug('div要素をクリックしました。') });
link.addEventListener('click', prevent);
link.addEventListener('click', function(){ debug('a要素をクリックしました。') });

コンソール画面

a要素をクリックしました。

関数preventで、メソッドpreventDefaultを実行しているためa要素のページ遷移のイベントはキャンセルされています。また、メソッドstopPropagationも実行されているのでdiv要素で登録したイベントハンドラも実行されません。ただ、a要素に登録したもう1つのイベントハンドラが実行されるため、コンソールに「a要素をクリックしました。」が出ます。これを止めるには別のメソッドstopImmediatePropagationを使用する必要があります。次のコードを見てみます。

sample.js
let inner = document.getElementById('inner');
let link = document.getElementById('link');

function prevent(e){
	e.preventDefault();
	e.stopImmediatePropagation();
}

function debug(value){
	console.log(value);
}

inner.addEventListener('click', function(){ debug('div要素をクリックしました。') });
link.addEventListener('click', prevent);
link.addEventListener('click', function(){ debug('a要素をクリックしました。') });

上のコードでは、メソッドstopImmediatePropagationを実行しています。このメソッドはイベント伝播(イベントバブリング)も止めますので、メソッドstopPropagationを実行する必要はありません。