• ベストアンサー

Javascriptのオブジェクト指向プログラミングとイベント、thisの扱い

Javascriptを勉強中です。 オブジェクト指向プログラミングを習得しようと努力しております。 あるHTML要素(例ではelm)にクリックイベントを付加する際、オブジェクト内のmyFuncを呼ぶのに以下のようにthis.myFuncとすると、thisがHTML要素となるためにエラーが出ます。 elm.addEventListener("click", this.myFunc, false); これを回避する目的で elm.addEventListener("click", (function(that) { return function() { that.myFunc(); } } )(this), false); とオブジェクト内からクロージャを使って定義することで解決することは分かったのですが、このイベントを削除するのに、 elm.removeEventListener("click", (function(that) { return function() { that.myFunc(); } } )(this), false); や elm.removeEventListener("click", function() { that.myFunc(); }, false); としても除去することが出来ません。この場合はどのようにイベントを削除することが出来るのでしょうか? そもそもクロージャを使った定義部分に問題があるのでしょうか? どうぞ教えていただきますようお願いいたします。

質問者が選んだベストアンサー

  • ベストアンサー
  • think49
  • ベストアンサー率59% (285/482)
回答No.11

#2,9です。#9の補足を読みました。 > 今回1つのオブジェクトで全てを定義しようとしたのは、別の方の回答にも書いたのですが、そのような例を見かけたからです。 差し支えなければ、その例を掲示してもらえないでしょうか? highslide.js を読んでみましたが、 document.onmousedown = hs.mouseDownHandler; のように宣言しており、thisは見あたりませんでした。 > Javascriptは複数プログラムの共存が頻繁で、関数名の衝突が起きやすく、それを避ける保険として1つのオブジェクトにするという発想もあるかと思います。 関数名の衝突を避けるために一つのオブジェクトに、という発想にはちょっと違和感を感じますね…。 この発想だと一つのスクリプトにまとめるために、別々の機能を一つのオブジェクトにまとめてしまうこともあるように思います。 例えば、匿名関数で括ってみてはどうでしょうか?(#9 のコードも匿名関数で括られています) ---- (function(){ var hoge = { start:function(){ ; }, stop:function(){ ; }, end:function(){ ; } }; var foo = { start:function(){ ; }, stop:function(){ ; }, end:function(){ ; } }; hoge.start(); foo.start(); })(); ---- とか、外部スクリプトにすることも踏まえて --- window./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'load', function (evt) { // 処理 },false); --- とか。 任意のスクリプトで呼び出せるようにするならば、ローカルなオブジェクトでは問題がありますが、最終的にグローバルオブジェクトとなっていればいいはずです。 --- (function(){ function a(){ ; } function b(){ ; } function d(){ ; } window.drag = { start:a, stop:b, end:c } // グローバルなオブジェクト初期化 })(); --- google-code-prettify も同じような組み方をしているので、参考になるかもしれません。 http://code.google.com/p/google-code-prettify/ # 蛇足ですが、メモリリークに関しては「循環参照 javascript」「javascript クロージャ メモリリーク」などで検索すれば関連情報がHITします。

life_span
質問者

お礼

たびたびご回答ありがとうございます。 あれからオライリーのGood Partsという本を読んで少しだけ理解を深めました。 私がトライしていたのは「グローバル変数を1つだけにする」ことと、「prototypeでメモリを節約する」ことですが、それぞれのサンプルを無理矢理接着させて両立させようとしていたことが、そもそもの間違いであったようです。(highslide.jsは前者のサンプルです) 仰る通り、グローバル変数の削減には色々と方法があるのですね。盲目的に1つのオブジェクトに閉じ込めようとしていました。 頂戴した情報は、よく参考にさせていただきます。 変な質問に繰り返しご回答頂きましてありがとうございました。

すると、全ての回答が全文表示されます。

その他の回答 (10)

  • Chaire
  • ベストアンサー率60% (79/130)
回答No.10

ドラッグコードでいちいち (add|remove)EventListener する必要はありません。フラグ管理だけで十分です。 ただし、No.2 へのコメントを見る限り、ご質問のコードはメモリリークパターンである可能性が高いので、ページ破棄時の removeEventListener は必須となります。 var f = (function(that) { return function(e) { that.myFunc(); }; })(this); elm.addEventListener("click", f, false); のようにし、この f をどこかに保持しておいて下さい(cf. No.1、No.3)。可能であればメモリリークパターンを避け、全体として No.9 のように書くべきです。そうすれば、そもそも removeEventListener は不要です。下記の例も、それを前提に書いています。 オブジェクト名を接頭辞代わりに使ったり、prototype をメソッド置き場のように使うのは、JavaScript のオブジェクト指向とは無関係です。 this 値がズレて困るというのは、本来異なるデータを一緒くたにしている可能性があります。そこで、No.8 の後半では、イベントを監視する Listener と、イベントを処理する Handler の 2 つのデータを分離し、両者の間で this 値の調整を行いました。これらは本来、this 値を共有しない異なるオブジェクトだと判断したからです。 var HogeDragEvent = new function() {  //  function DragEvent() { ; }  (function() {   this.startDrag = function(e) { /* mousedown */; };   this.moveDrag = function(e) { /* mousemove */; };   this.stopDrag = function(e) { /* mouseup*/; };  }).call(DragEvent.prototype);  //  function HogeDragEvent() { DragEvent.apply(this, arguments); }  HogeDragEvent.prototype = new DragEvent;  HogeDragEvent.prototype.constructor = HogeDragEvent;  (function() {   this.moveDrag = function(e) { /* override */; };  }).call(HogeDragEvent.prototype);  HogeDragEvent.ready = function() { return new HogeDragEventListener; };  //  function HogeDragEventListener() {   var f = function(e) { return h[e.type](e); };   var h = new HogeDragEventHandlers(new HogeDragEvent, f);   return f;  };  //  function HogeDragEventHandlers(drag, caller) {   this.drag = drag;   this.caller = caller;  }  HogeDragEventHandlers.prototype = {   mousedown: function(e) { return this.drag.startDrag(e); },   mousemove: function(e) { return this.drag.moveDrag(e); },   mouseup: function(e) { return this.drag.stopDrag(e); },   unload: function(e) {    var d = document;    var w = d.defaultView;    d.removeEventListener('mousedown', this.caller, false);    d.removeEventListener('mousemove', this.caller, false);    d.removeEventListener('mouseup', this.caller, false);    w.removeEventListener('unload', this.caller, false);   },   ready: function(e) {    var d = document;    var w = d.defaultView;    d.addEventListener('mousedown', this.caller, false);    d.addEventListener('mousemove', this.caller, false);    d.addEventListener('mouseup', this.caller, false);    w.addEventListener('unload', this.caller, false);   }  };  this.ready = HogeDragEvent.ready; }; HogeDragEvent.ready(); HogeDragEvent.ready(); かなりイイカゲンですが、ここでは 4 種類のインタフェース(もどき)を定義しました。つまり、少なくとも 4 種類の異なるオブジェクトクラスが存在すると、私が判断したということです。また、名前の衝突を避けるために全体を関数の中に入れ、外部公開する API は HogeDragEvent.ready だけにしています。 とは言え、複数のドラッグイベントが同時に生じるケースは少なく(マルチデバイス対応を目指すなら別ですが)、大抵はイベントリスナ 1 つあれば事足ります。であれば、上記の HogeDragEventHandlers と HogeDragEventListener はコンストラクタである必要がありません。極端な話、 var HogeDragEvent = { }; HogeDragEvent.Listener = function(e) {  return HogeDragEvent.Handler[e.type](e); }; HogeDragEvent.Handler = {  startDrag: function(e) { ...; },  moveDrag: function(e) { ...; },  stopDrag: function(e) { ...; },  mousedown: function(e) { ...; },  mousemove: function(e) { ...; },  mouseup: function(e) { ...; },  ready: function(e) { ...; } }; これで十分という可能性もあります。 しかし、コンストラクタなしでは、万が一、マルチデバイス対応が必要になったときに全部書き直しになるのでしょうか。必ずしもそうではありません。ECMAScript5 の Object.create を使えば任意の Object をプロトタイプとする Object を生成できます(Object.create は ECMAScript3 の範囲内でもある程度は実装できます)。 var createListener = function(handler) {  return function(e) {   return handler[e.type](e);  }; }; var myHandler1 = Object.create(HogeDragEvent.Handler); var myHandler2 = Object.create(HogeDragEvent.Handler); var myListener1 = createListener(myHandler1); var myListener2 = createListener(myHandler2); 実のところ、コンストラクタも new も、JavaScript ではさほど重要ではありません。それらは少しだけ手間を減らしてくれるというだけです。「Object さえあれば良い」のが、JavaScript におけるオブジェクト指向です。 繰り返しますが、this 値がズレて困るなら、それは本来別のオブジェクトに属するものを、無理矢理単一のオブジェクトに閉じ込めた結果かもしれません。一度、this 値に応じてメソッドを分類・整理してみて下さい。

life_span
質問者

お礼

たびたびご回答ありがとうございます。 何度も読み返してようやくなんとなく理解できました。貴殿に比べると私はその程度のレベルです。 「グローバル変数の撲滅(1個だけにする)」と、「prototypeによるメモリ節約」の2つ試みを混同していたことが良く分かりました。サンプルはそれが可能であることを表していただけたのかと思いますが、それには無理があることは分かりました。勉強し直します。 「thisに応じて分類・整理」ですね。 ありがとうございました。

すると、全ての回答が全文表示されます。
  • think49
  • ベストアンサー率59% (285/482)
回答No.9

#2 です。 私とは設計からして違いますね。私はfunctionとイベントリスナは分けて定義してます。 mousemoveイベントは経験がないので、clickイベントで例を出しますが。 <script type="text/javascript"> (function(){ var func = { hello: function(){ alert('hello'); }, world: function(){ alert('world'); } }; document.addEventListener('click',function(evt){ var t = evt.target; if(t.id == 'hello'){ func.hello(); t.id = 'world'; } else if(t.id == 'world'){ func.world(); t.id = ''; } },false); })(); </script> </head> <body> <p id="hello">hello, world!</p> document全体をフックしてイベント定義しています。 この手法の素晴らしいところは id,class属性 など条件を整えることで挙動を簡単に変更できることです。(id属性を削除してイベント削除とか) document.getElementById('Test').addEventListener() ではJavaScriptで新しく定義された <div id="Test"> を認識できませんが、上のやり方なら認識できます。 ただ、このやり方は全体(documentとかwindowとか)をフックすることを基本としているので、それが出来ないイベントもあるかもしれません。 そういう場合は、addEventListenerで各々イベント定義します。 <script type="text/javascript"> (function(){ // 関数 var foo = { hello: function(evt){ alert('hello'); }, world: function(evt){ alert('world'); } }; // イベント document.getElementById('Test1').addEventListener('click', foo.hello, false); document.getElementById('Test2').addEventListener('click', foo.world, false); })(); </script> ポイントはイベントと関数を分けて考えることです。 例文を見ていると、全てをDragオブジェクトで定義しようとしているように見えますが、 上のようにルートでイベント定義するか、「イベント定義用のオブジェクト」と「関数用のオブジェクト」を別々に用意すれば、 容易に解決できるのではないかと思います。 「イベントと関数をまとめて一元化」から「イベントの一元化」と「関数の一元化」に分ける考え方ですね。

life_span
質問者

お礼

再びご回答ありがとうございます。 お礼が遅くなりまして申し訳ございません。 document全体をListenをする方法は初見でした。面白いですね。いつかどこかで使わせて頂きます。 設計思想は、貴殿がお考えが正しいものと私も思います。 今回1つのオブジェクトで全てを定義しようとしたのは、別の方の回答にも書いたのですが、そのような例を見かけたからです。 Javascriptは複数プログラムの共存が頻繁で、関数名の衝突が起きやすく、それを避ける保険として1つのオブジェクトにするという発想もあるかと思います。しかし、メリットをスポイルしすぎるのでは意味がありません。そのあたりの見極めが相変わらず分かりません。引き続き勉強します。皆様ありがとうございました。

すると、全ての回答が全文表示されます。
  • Chaire
  • ベストアンサー率60% (79/130)
回答No.8

状況によりますが、質問文のクロージャはブラウザがメモリリークする原因になる可能性があります(IE7 以下、Firefox 1.5 以下、Safari 4 以下)。基本的に、不用意なクロージャを避けるべきです。 addEventListener を使える今どきの実装であれば、イベントリスナとして handleEvent メソッドを持つ Object を渡せます。 elem.addEventListener('click', {  handleEvent: function(e) {   this.myFunc();  },  myFunc: function() {   ...;  } }, false); もともと NN6 による DOM2-Events の独自解釈ですが、直接 Function を渡すより扱いやすいでしょう。もちろん、コンストラクタ由来の Object でも構いません。 function HogeListener(a) {  this.useCapture = a; } HogeListener.prototype = {  handleEvent: function(e) {   this.myFunc(e);  },  myFunc: function(e) {   e.currentTarget.removeEventListener(e.type, this, this.useCapture);  } }; elem.addEventListener('click', new HogeListener(false), false); 特に理由がなければ、この方法をお勧めします。 IE および Opera 8 以下、Safari 2 以下を相手にするなら、下記では this 値を調節しつつ関連イベント全般を監視する myListener と、イベントタイプに応じて処理を行う myHandlers の二本立てにしています。こうすれば、イベントリスナの登録・解除は myListener だけ見れば済みますし、関連するイベント処理をまとめることもできます。 var myHandlers = {  'click': function(e) {   return this.myFunc(e);  },  'mousedown': function(e) {   return this.myFunc(e);  },  myFunc: function(e) {   e.currentTarget.removeEventListener(e.type, myListener, false);  } }; var myListener = function(e) {  return myProcessor[e.type](e); }; elem.addEventListener('click', myListener, false); elem.addEventListener('mousedown', myListener, false); ちなみに、IE には event.currentTarget に相当するものがないので何らかの工夫が必要ですが、そもそも IE で currentTarget 相当が必要な場合には、HTML のイベント属性として書く方がいろんな意味で無難です。 上記は一例に過ぎません。しかし、どのような方法を採用するにせよ、イベントハンドラ関数は必ずイベントオブジェクトを第一引数にとるよう定義して下さい。これは DOM-Events の約束事です。このイベントオブジェクトによって、イベント発生時の文脈を調べ、処理を振り分けるのがイベントハンドラの役割です。

life_span
質問者

お礼

ご回答ありがとうございます。 知らなかった世界に触れられた気がします。大変勉強になりました。 イベントをまとめる方法、イベントオブジェクトのルールはまったく知りませんでした。 今回、コンストラクタを除く全スクリプトがvar Obj.prototype = {}の中に入っているエレガントなサンプルがあり、これを真似ようと勉強をはじめたのがきっかけでした。 ご提示頂いた内容を無理矢理、 Obj.prototype = { "myHandlers" : {"click":略}, "myListener" : function() { 略 }, 略 }; のようにして試したのですが、そもそもmyListenerを呼ぶ時点でthisの問題がクロージャ頼みになってしまい、解決出来ませんでした。 Obj.prototype = {}内でイベントにthis.myFuncを登録・削除する際は elm.addEventListener("click", Obj.prototype.myFunc, false); elm.removeEventListener("click", Obj.prototype.myFunc, false); とするのが適当なのでしょうか。 highslide.jsなどを参考にするとそう思えてくるのですが。

すると、全ての回答が全文表示されます。
回答No.7

#3~#6です。さくじょできないなら、thisでよべてもいみがありませんね。 また「無駄に回答が多い質問」のぶんるいにしてしまった!ちと反省。 やっぱりなまえつけて、arguments.calleeとでけす? //@cc_on @set @V = (@_jscript_version >= 5.5) /*@if (@V) attachEvent ('on' + @else@*/ addEventListener (/*@end@*/  'load', function () {   /*@if (@V) detachEvent ('on' + @else@*/ removeEventListener (/*@end@*/    'load', arguments.callee, false);   handler1.call(this, e);   handler2.call(this, e);      document./*@if(@V) attachEvent('on'+ @else@*/ addEventListener(/*@end@*/ 'click', test, false);  }, false); /*@if(@V) attachEvent ('on' + @else@*/ addEventListener (/*@end@*/  'unload', function () {   /*@if(@V) detachEvent ('on' + @else@*/ removeEventListener (/*@end@*/    'unload', arguments.callee, false);   document./*@if(@V) attachEvent ('on' + @else@*/ addEventListener (/*@end@*/ 'click', test, false);     }, false); function test(evt) {  var e = evt./*@if(@F) srcElement @else@*/ target /*@end@*/;  alert(e.tagName); }

life_span
質問者

お礼

あらためまして、ご回答ありがとうございました。 No.2の方の補足に書いたのですが、やはりarguments.calleeを使えるのは「あるイベントを付加した無名関数内からそのイベントを消す」という制約があるように思います。(私の勘違いがありますか??) がしかし、大変参考になりました。 重ねてありがとうございました。

すると、全ての回答が全文表示されます。
回答No.6

こりなやつだと、おもってくれ^^; //@cc_on @set @V = (@_jscript_version >= 5.5) var X = { 'test':'abc' }; X.add = (function( handler1, handler2) {  return function ( ) {   document./*@if( @V ) attachEvent ('on' + @else@*/ addEventListener (/*@end@*/    'click', (function(that){ return function(evt) { handler1.call(that ,evt); }; })(this), false);  }; })(  function () { alert(this.test); }, //1  function () { ; }  //2  ); X.add(); これで、くりっくしたら abc があらーとされる

life_span
質問者

お礼

たくさんご回答いただきましてありがとうございます。 じっくり内容を見させて頂きまして、後ほど最新のご回答の方にコメントさせていただきます。 取り急ぎお礼です。

すると、全ての回答が全文表示されます。
回答No.5

#4は、いみがなかった。わすれて。

すると、全ての回答が全文表示されます。
回答No.4

#3です。 まとはずれだろうか?その2 var X = { 'str': 'abc' }; X.test = (function(a) {  return function () {   alert( this.str );  }; })(  (function( that) { return function () { ; }; })(this)  );   X.test();

すると、全ての回答が全文表示されます。
回答No.3

まとはずれだろうか? //@cc_on @set @V = (@_jscript_version >= 5.5) var Xxxx = (function( handler1, handler2) {  return function ( sw ) {   if( sw ) {    document./*@if( @V ) attachEvent ('on' + @else@*/ addEventListener (/*@end@*/     'click', handler1, false);    document./*@if( @V ) attachEvent ('on' + @else@*/ addEventListener (/*@end@*/     'click', handler2, false);   } else {    document./*@if( @V ) detachEvent ('on' + @else@*/ removeEventListener (/*@end@*/     'click', handler1, false);    document./*@if( @V ) detachEvent ('on' + @else@*/ removeEventListener (/*@end@*/     'click', handler2, false);   }   return sw;  }; })(  function () { ; }, //1  function () { ; }  //2  ); 

すると、全ての回答が全文表示されます。
  • think49
  • ベストアンサー率59% (285/482)
回答No.2

> この場合はどのようにイベントを削除することが出来るのでしょうか? 匿名関数は arguments.callee によって、関数オブジェクトを拾えます。 http://un-q.net/2007/07/addeventlistener_removeeventlistener.html 以下のように、addEventListenerで定義した匿名関数内でremoveEventListenerは可能でした。 --- example 1 document.addEventListener('click', function(){ alert('test'); document.removeEventListener('click', arguments.callee, false); // remove可能 }, false); --- ただし、匿名関数の外に関数オブジェクトを持ってくることは出来ないようです。 --- example 2 var func; document.addEventListener('click', function(){ alert('test'); func = arguments.callee; }, false); document.removeEventListener('click', func, false); // remove不可 ---

life_span
質問者

補足

ご回答ありがとうございます。 実はarguments.calleeの存在は知っておったのですが、イベント付加時の無名関数内でremove出来ない例があるので、困っておった次第です。 例えば以下のようなドラッグイベントに関するものです。(以下、一部を簡略化します。) function Drag() { }; Drag.prototype = { "startDrag" : function() { //ドラッグ中のイベントthis.draggingを付加 elm.addListener("mousemove", (function(that) { return function() { that.dragging(); } } )(this), false); //「ドラッグ終了時にthis.draggingを削除する」イベントthis.endDragを付加 elm.addListener("mouseup", (function(that) { return function() { that.endDrag(); } } )(this), false); }, "dragging" : 略, "endDrag" : function() { //this.draggingを削除 elm.removeListener("mousemove", (function(that) { return function() { that.dragging(); } } )(this), false); } }; ご提示いただきましたexample2の例に該当し、無名関数で付加したイベントの削除が出来ないように思います。 このような仕様はままあると思うのですが、皆さんはOOPは使わないのでしょうか。何か私の間違いがありますでしょうか。

すると、全ての回答が全文表示されます。
回答No.1

elm.addEventListener("click", (function(that) { return function() { that.myFunc(); } } )(this), false); をやった時点でここに登録されたイベント関数は名前無しになります 素直に elm.addEventListener("click", hoge); function hoge(obj) { 色々; } elm.removeEventListener("click", hoge); ではだめですか?

life_span
質問者

お礼

ご回答ありがとうございます。もう少しOOPにこだわりたいと思います。

すると、全ての回答が全文表示されます。

関連するQ&A