イベントハンドラの this と event.target, +α

対応その2: event.targetを使う対応その4 : this を使う について

上記サイトで書いてあることが間違っているというわけじゃないんだけど、少し補足したい。

event.target

その Event オブジェクトには発火元となった要素を target プロパティとして保持しています.

合っています。

が、on* や addEventListener した要素と target は必ずしも一致しない点には注意した方が良いと思います。

<ul id="list">
  <li>foo <b>hoge</b></li>
  <li>bar <b>hoge</b></li>
</ul>
var list = document.getElementById("list");
for (var i = 0, len = list.children.length; i<len; ++i) {
  var li = list.children[i];
  li.onclick = function (event) {
    event.target.innerHTML = "HOGEHOGE";
  };
}

こんな場合、<b>hoge</b>部分をクリックすると、target は発火元となった要素なので、b要素となります。li要素にはなりません。

イベントハンドラの this

合っていると思います。

が、正直どう書いて良いか分からないのですが、仕様が見つけられませんでした。
invoke - DOM Standard側では発見したのですが、DOM Level 2 Event Model にも Document Object Model (DOM) Level 3 Events Specification にもイベントハンドラ実行時の this がどうあるべきかは書かれていないように思われます。*1

習慣的に(?) this はハンドラを設置したオブジェクトになるようブラウザで統一されているから、WHATWGにまとめられたと解釈しておきます。

まぁ、ともかく、this はハンドラを設置したオブジェクトになるので、文句なしに正しいでしょう。

event.currentTarget

ハンドラ実行時の挙動が invoke - DOM Standard に書かれているわけですが、

  • 3. eventオブジェクトのcurrentTargetを設置したオブジェクトにする
  • 4.6. コールバックの this を currentTarget にする
  • 4.7. コールバックを実行

となっています。

よって、this を event.currentTarget にしても問題なく動きそう。ブラウザ各実装が仕様に基づいているかまでは知りませんけどね :P

クロージャについて

+αとして最初の問題提起のコードに戻って、クロージャについて書いてみたいとおもいます。
というのも、何故か削除されてしまっていますが、当初は以下の様な事が書かれていました。

これは, for 内で定義する関数がクロージャされていない為に起きる現象です.

うまく解釈すれば間違っていないのですが、微妙な書き方なので、補足したいと思います。

var buttons = document.getElementsByTagName("button");

for (var i=0,len=buttons.length; i<len; ++i) {
    var button = buttons[i];
    
    button.onclick = function() {
        alert(button.innerHTML);
    };
}

のonclickに設置した関数オブジェクトはクロージャであるか、ですが、立派にクロージャです。

さて、「JavaScriptにはブロックスコープはなく、var による変数宣言は関数スコープだ」というのは耳にタコができるほど聞くでしょう。
上記コードにおけるvar button = buttons[i]の場合は、関数内ではないのでグローバルになり、少し説明しにくいので全体を関数内のコードとして説明したいと思います。

function setButtonHandler() {
  var buttons = document.getElementsByTagName("button");

  for (var i=0,len=buttons.length; i<len; ++i) {
    var button = buttons[i];
    
    button.onclick = function() {
        alert(button.innerHTML);
    };
  }
}

setButtonHandler 内のvar変数宣言、 buttons, i, len, button は関数スコープとなります。ECMAScript 5th ではこのスコープをレキシカル環境と呼んでいるのですが、このレキシカル環境にこれらの変数があるよという情報が蓄積されます。
そして、onclickのfunctionオブジェクトが生成されるとき、新たなレキシカル環境がつくられ、親となるスコープ(ここではsetButtonHandlerのレキシカル環境)を parent として保持します。
onclickの関数内で変数を得る時、まずは自分のレキシカル環境から探し、なければ、親へ問い合わせて最終的にはグローバルにあるレキシカル環境に辿り着く、という俗に言うスコープチェーンの出来上がりです。
functionオブジェクトが生成されるときと書いたように、定義時にこのチェーンは出来上がっていることになり、functionオブジェクトを何処へ持って行っても変化することはありません。立派にクロージャですね?

実際のコードの動きを追ってみましょう。

  1. buttons, i, len, button を setButtonHandlerのレキシカル環境に設置
  2. buttons = document.getElementsByTagName("button")
  3. for 文
    1. i = 0
      • button = buttons[0] // setButtonHandlerのレキシカル環境内のbuttonの値を更新
      • button.onclick = function(){ alert(button.innerHTML); }
    2. i = 1
      • button = buttons[1] // setButtonHandlerのレキシカル環境内のbuttonの値を更新
      • button.onclick = function(){ alert(button.innerHTML); }
    3. ...

さて、clickイベントが発生しました。0 番目のボタンで発生したとしましょう。

  1. function(){ alert(button.innerHTML); }が実行される
  2. alertという名前の関数を実行しているぞ。まだ実行せずに、引数の値を評価しよう
  3. buttonという名のオブジェクトが必要だ
    1. 自分の関数(onclickのハンドラ)のレキシカル環境には存在しない
    2. 親のレキシカル環境(setButtonHandler)に問い合わせ
      • この時、for文は既に回りきっている
      • buttonという変数名の先には buttons[buttons.length -1]の値が入っていることになり、それを返す
    3. 返ってきた値のinnerHTMLプロパティを取得
    4. alertという名のオブジェクトを(自分の環境にない、親に問い合わせ、更に問い合わせて)グローバルからwindow.alertを得る
    5. alert(返ってきた値)の実行

と、意図とは違った結果がalert関数に渡ることになります。

let 宣言によるブロックスコープ

これは, for 内で定義する関数がクロージャされていない為に起きる現象です.

これをfor文のブロック内で定義する変数が、ブロックスコープとして保持されない為に起きる現象と捉えてみましょう。

ということで、ECMAScript 6th から使用出来る let 宣言の登場です。

function setButtonHandler() {
  var buttons = document.getElementsByTagName("button");

  for (var i=0,len=buttons.length; i<len; ++i) {
    let button = buttons[i];
    
    button.onclick = function() {
        alert(button.innerHTML);
    };
  }
}

一箇所だけ、var button = buttons[i]let button = buttons[i]に変更しました。
こうすると、for分のブロック({...}部分)に毎回新たなレキシカル環境が作られます。

var 宣言の時の clickハンドラのスコープチェーンは clickハンドラ→setButtonHandler→グローバルでしたが、let の場合ブロックスコープが発生するため、clickハンドラ→for文のブロック→setButtonHandler→グローバルとなります。

同じようにコードの動きを追うと

  1. buttons, i, len を setButtonHandlerのレキシカル環境に設置
  2. buttons = document.getElementsByTagName("button")
  3. for 文
    1. i = 0
      • 新たなレキシカル環境を作る
      • button = buttons[0] // for文のレキシカル環境内のbuttonの値を更新
      • button.onclick = function(){ alert(button.innerHTML); }
    2. i = 1
      • 新たなレキシカル環境を作る
      • button = buttons[1] // for文のレキシカル環境内のbuttonの値を更新
      • button.onclick = function(){ alert(button.innerHTML); }
    3. ...

for文のブロックに入る毎にレキシカル環境が作られて、そこにbuttonの値が入ることになり、それぞれのclickハンドラが毎回異なるレキシカル環境を親として持つようになります。clickイベント時にめでたくそれぞれのボタンを参照できるようになるわけです。

途中で書くのが面倒になって脈絡なく駆け足になってしまいました。反省

ともかく、ECMAScript 6th たいへん楽しみですね!

そんじゃーね。

*1:書かれている箇所が分かる人は教えて下さい