読者です 読者をやめる 読者になる 読者になる

JavaScript でオブジェクトのメンバであるメソッドをイベントハンドラにしたときに非 DOM の this を取りたい

ちょっとエントリのタイトルが微妙だが、まず次の HTML + JavaScript コードをご覧いただきたい。(前後の HTML コードをカットしている)

<script type="text/javascript">
function SampleA(name){
  this.name = name;
}
SampleA.prototype.button_click = function(){
  alert("This is " + this.name);
}

function init(){
  var s1 = new SampleA("s1");
  document.getElementById("s1button").onclick = s1.button_click;
}
</script>
</head>
<body onload="init()">
<p><button id="s1button" name="s1ButtonName">s1 ボタン</button></p>
</body>

上記を実行して「s1 ボタン」をクリックすると、「This is s1ButtonName」とアラートが表示される。
これはイベントでは this がイベント送出の DOM オブジェクトとなるからだ。button 要素の name 属性が this.name となる。s1.button_click を呼び出していても、this.name が "s1" とはならない。

こういうコーディングをしたい場合、今まで SampleA に当たるものがシングルトンだった。そのため、this の代わりに SampleA.name とベタに定義してしまっても問題がなかった。

ということで、複数インスタンスを作りたい場合どうすれば良いかというのが本題である。
あるオブジェクトのメソッドの中でイベントを無名関数で定義するならば、その中でその外のスコープの this を参照 したいときは with やローカル変数に一旦代入することで拾えるようになる。
例えば、このようなな感じで書くだろう。

var sample = {
  name: "名前",
  display: function(){
    var that = this;
    $("s1button").click(function(){
        alert(that.name);
      });
  }
};

結局、この方法を使うしかないだろう。では、解決方法に。
要は、外側のスコープで参照できるように定義しておけばよいので、次のように書ける。

<script type="text/javascript">
function SampleA(name){
  this.name = name;
  var me = this;
  this.button_click = function(){
    alert("This is " + me.name);
  }
}

function init(){
  var s1 = new SampleA("s1");
  document.getElementById("s1button").onclick = s1.button_click;
  
  var s2 = new SampleA("s2");
  document.getElementById("s2button").onclick = s2.button_click;
}
</script>
</head>
<body onload="init()">
<p><button id="s1button">s1 ボタン</button></p>
<p><button id="s2button">s2 ボタン</button></p>
</body>

(cyokodog さんのご指摘により5行目を修正 at 2009/01/11 13:35)
prototype の定義が上書きされてしまうのでうまく動きませんでした。
SampleA.prototype.button_click = function(){
this.button_click = function(){

このようにコンストラクタの中でメソッドを定義してしまう。上記の「s1 ボタン」をクリックすれば「This is s1」、「s2 ボタン」をクリックすれば「This is s2」とアラートが表示される。SampleA のそれぞれのインスタンスの name が参照されていることがわかる。
ただ、これが上手いやり方かどうかはあまり自信が無い。モアベターな書き方があったら教えてください。
(サンプルコードの内容がモデルとビューがくっついていて…という話ではないです。)

(追記 at 2009/01/11 14:55)
このエントリーを書いたのは、ある div 要素にマウスがホバーしたパネルを表示する処理をコーディングしていて、それのホバー時の処理を疑似クラス化したかったのが切っ掛けだった。
cyokodog さんのコメントを参考に上手く定義できるようになった。DOM 要素の this とメソッドの所有者の this どちらも参照したかった。ラップして、DOM 要素の this を sender として渡すように変更した。

// jQuery を使ってます。
function HoverPanel(){
  var me = this;
  me._onhoverin = function(e){
      me.onhoverin(this, e);
    };
  me._onhoverout = function(e){
      me.onhoverout(this, e);
    };
}
HoverPanel.prototype = {
  bind: function($target){
    $target.hover(this._onhoverin, this._onhoverout);
  },
  unbind: function($target){
    $target.unbind('mouseenter').unbind('mouseleave');
  },
  onhoverin: function(sender, evt){
    // ホバーしたときの処理
  },
  onhoverout: function(sender, evt){
    // ホバーが解除したときの処理
  }
};