質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

解決済

3回答

3902閲覧

共通リスナに参照をもたせる良い方法

lazex

総合スコア604

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

2グッド

3クリップ

投稿2018/04/12 15:23

編集2018/04/15 03:08

タイトルだけだと何がいいたいかよくわからないですが、
addEventListener でリスナを作るときに removeEventListener で解除するためにどこかに参照を保持する必要があります。
リスナ自体は同じ処理なのでひとつですが内部で別のインスタンスを参照させたいです。
しかし、リスナは event しか引数をとりません。
自分で 「event を受け取って、 event と インスタンスを渡してリスナを実行する」 関数を作っても結局それを addEventListener に登録する以上その参照を保持する必要があります。
その参照をオブジェクトのプロパティには可能な限りしたくないです。
理由は公開したくないのと、リスナをインスタンスごとに持たせるのがムダに思うからです(可能なら共通のものにしたい)。
公開しないというものは共通の WeakMap を 1 つ用意してインスタンスに対する任意のデータをもたせるようにして、そこに実際に登録したリスナを入れることにしました。
ただ、これはこれで手間な方法に思います。
もっと簡単な方法がありそうに思うのですが、思いつきません。
何か良いアイデアがありましたら、教えてください。


具体例

コードがないと分かりづらいと思うので一例を載せます。

js

1class X{ 2 setListener(){ 3 window.addEventListener("click", listener, false) 4 } 5 removeListener(){ 6 window.removeEventListener("click", listener, false) 7 } 8} 9 10function listener(eve){ 11 x.prop = eve.target 12} 13 14export { X }

X のインスタンスそれぞれがリスナを追加します。
このままだと、 listener 内部で x が見つからないとエラーがでます。
x は各 X のインスタンスを指します。

x で参照できるように x を受け取る作りにします。

js

1setListener(){ 2 const self = this 3 this.listener = function(eve){ 4 listener.call(this, eve, self) 5 } 6 window.addEventListener("click", this.listener, false) 7}

js

1function listener(eve, x){ // 引数が追加 2 x.prop = eve.target 3}

ここで this.listener の関数がそれぞれのインスタンスに対して作らないといけないのと、それを X のインスタンスに保持しないといけないのが嫌な部分です。

X のインスタンスに持たさないためにこうしました。

js

1setListener(){ 2 const self = this 3 const listener_wrapper = function(eve){ 4 listener.call(this, eve, self) 5 } 6 wmap.set(this, listener_wrapper) 7 window.addEventListener("click", listener_wrapper, false) 8}

js

1const wmap = new WeakMap()

たかだかリスナにオブジェクト参照をもたせたいだけでこんなに変更するのはなにか違う気がします。

書いてて無理そうな気もしてきたのですが、なにか方法がありそうにも思います。

追記

最終的にどうなっている状態かがわかりづらいため全体のコードを追記します。
また、上記コードで listener が重複していたので修正しました。

js

1const wmap = new WeakMap() 2 3class X { 4 setListener(){ 5 const self = this 6 const listener_wrapper = function(eve){ 7 listener.call(this, eve, self) 8 } 9 wmap.set(this, listener_wrapper) 10 window.addEventListener("click", listener_wrapper, false) 11 } 12 removeListener(){ 13 window.removeEventListener("click", wmap.get(this), false) 14 } 15} 16 17function listener(eve, x){ 18 x.prop = eve.target 19 console.log(x) 20} 21 22export { X }

補足すると EsModules のモジュールのため、 wmap はこのファイル外には公開されていません。

umyu, HayatoKamono👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

handleEvent

handleEvent を持つオブジェクトを指定してみては、どうでしょう。

Living Standard](https://dom.spec.whatwg.org/#interface-eventtarget)

JavaScript

1Foo.prototype = { 2 bar: function bar () { 3 document.addEventListener('click'. this, false); 4 }, 5 piyo: piyo, 6 handleEvent: function handleEvent (event) { 7 this.piyo(); 8 } 9};

Function.prototype.bind

JavaScript

1Foo.prototype.bar = function bar () { 2 document.addEventListener('click'. function handleClick (event) { this.method(); }.bind(this), false); 3};

アロー関数

JavaScript

1Foo.prototype.bar = function bar () { 2 document.addEventListener('click', (event) = > this.method(), false); 3};

プライベートプロパティを定義する

ECMAScript® 2017 (ES8) にはプライベートプロパティを作る方法が用意されていない為、WeakMap を利用します。

下記コードは、上記リンク先の createPivateMap() を利用したコードです。

JavaScript

1const Clicker = (() => { 2 var pm = createPivateMap(); 3 4 return class Clicker { 5 constructor (message) { 6 pm(this, {message: message}); 7 } 8 addListener () { 9 pm(this).listener = (event) => console.log(event.type, pm(this).message); 10 document.addEventListener('click', pm(this).listener, false); 11 } 12 removeListener () { 13 document.removeEventListener('click', pm(this).listener, false); 14 } 15 }; 16})(); 17 18const c1 = new Clicker('Hello, World!'), 19 c2 = new Clicker('Hello, JavaScript!'); 20 21c1.addListener(); 22c2.addListener(); 23document.documentElement.click(); 24c1.removeListener(); 25c2.removeListener(); 26document.documentElement.click();

listenerを共通化したい

理由は公開したくないのと、リスナをインスタンスごとに持たせるのがムダに思うからです(可能なら共通のものにしたい)。

無駄ではないと思います。
listenerを共通参照にするというのは、[JavaScript] プライベートプロパティを作る方法.mdのStatic版のコードにするという事です。
インスタンス毎に独立した処理に出来ず、removeEventListener は全てのインスタンスに影響してしまいます。
イベント周りの都合を踏まえると、listenerは各々、別の参照であるべきと考えます。

(2018/04/15 13:55追記)

最終的にどうなっている状態かがわかりづらいため全体のコードを追記します。

関数 listener の生成コストを無くしたい意図で書いたコードとお見受けしました。
前述のコードでは、コードの平易さ重視で関数をネストしましたが、私も自分でコードを書くときには関数を外出しするので気持ちは分かります。
(おそらく、「listener共通化」の本来の目的も生成コストにあるのではないでしょうか)

さて、実際に listener を共通化してみると問題点がはっきりします。

JavaScript

1class X { 2 addListener () { 3 window.addEventListener("click", listener, false) 4 } 5 removeListener () { 6 window.removeEventListener("click", listener, false) 7 } 8} 9 10function listener (event) { 11 console.log(event.type); 12} 13 14const x1 = new X(), 15 x2 = new X(); 16 17x1.addListener(); // x1 で listener 追加 18x2.addListener(); // x2 で listener 追加(上書き) 19document.body.click(); // コンソールには "click" が「1回だけ」出力される 20x2.removeListener(); // 上書きされた listener 一つを削除 21document.body.click(); // x1, x2 共に click イベントハンドラが発動しない

内部的には以下のコードが実行されています。

JavaScript

1window.addEventListener("click", listener, false); // x1 で listener 追加 2window.addEventListener("click", listener, false); // x2 で listener 追加 (listener が x1 と同一なので、上書き処理) 3document.body.click(); // listener は一つしか追加されなかったので、コンソールには "click" が「1回だけ」出力される 4window.removeEventListener("click", listener, false); // x2 で listener 削除 (listener は1つしか登録されてないので全削除) 5document.body.click(); // 何も起きない

addEventListener から見れば、同じ listener を与えれば同じイベントハンドラと認識します。
従って、listener を共通化出来ないのです。

まとめ

最終的に質問文に追記されたコードを元に今までのロジックを詰め込むと、下記コードになります。

JavaScript

1const wmap = new WeakMap(); 2 3class X { 4 addListener () { 5 const listener = templateListener.bind(this); 6 7 wmap.set(this, listener); 8 window.addEventListener("click", listener, false); 9 } 10 removeListener(){ 11 window.removeEventListener("click", wmap.get(this), false); 12 } 13} 14 15function templateListener (event) { 16 this.prop = event.target; 17 console.log(this); 18} 19 20const x1 = new X(), 21 x2 = new X(); 22 23x1.addListener(); 24x2.addListener(); 25document.body.click(); // 2回発火 26document.documentElement.click(); // 2回発火 27x2.removeListener(); 28document.body.click(); // 1回発火

listener に任意の引数を指定したい場合は Function.prototype.bind の第二引数以降を利用するか、handleEvent プロパティ付のオブジェクトを渡して、プロパティ経由で取得します。

JavaScript

1const wmap = new WeakMap() 2 3class X { 4 addListener () { 5 const listener = createListener(this, templateListener); 6 7 wmap.set(this, listener); 8 window.addEventListener("click", listener, false); 9 } 10 removeListener(){ 11 window.removeEventListener("click", wmap.get(this), false); 12 } 13} 14 15function createListener (thisArg, handleEvent) { 16 return Object.assign(Object.create(Object.getPrototypeOf(thisArg)), thisArg, {handleEvent: handleEvent, thisArg: thisArg}); 17} 18 19function templateListener (event) { 20 this.thisArg.prop = event.target 21 console.log(this); 22} 23 24const x1 = new X(), 25 x2 = new X(); 26 27x1.addListener(); 28x2.addListener(); 29document.body.click(); // 2回発火 30document.documentElement.click(); // 2回発火 31x2.removeListener(); 32document.body.click(); // 1回発火

渡している this 値は new X とほぼ同一ですが、this.prop = event.target のようにインスタンスにプロパティ拡張する場合には参照値が異なるので、this.thisArg 経由で渡してやります。
プロパティ拡張を listener 内でやらなければ、this.adddListenerでメソッドは呼べますし、メソッド経由でプロパティ拡張をすれば、thisArg プロパティは不要ですが、設計思想の違いが現われるところですね。

それから、このコードは簡易版なので、enumerable 等の descriptor まではコピーしません。 Object.getOwnPropertyDescriptor を併用する事で、本来の this 値と同じ挙動となります。

Re: lazex さん

投稿2018/04/12 15:29

編集2018/04/15 04:57
think49

総合スコア18156

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

lazex

2018/04/12 16:01

いいアイデアだと思ったのですが、 removeEventListener するときにも同じオブジェクトが要求されるようです。 `{x: this, handleEvent: listener}` を第二引数に渡して登録すると、 listener ではなくこのオブジェクトを removeEventListener に渡さないと解除されませんでした。 ラッパー関数ではなくて単純なオブジェクトな分コストは小さくなりそうですが、このオブジェクトの参照を `x` 外に保存するために WeakMap を使うのは同じになりそうです
Lhankor_Mhy

2018/04/13 01:40

横からすみません。 `window.addEventListener("click", this, false)`とすればいいのでは?
think49

2018/04/13 03:28

Lhankor_Mhy さん、フォローありがとうございます。 Lhankor_Mhy さんが仰る通り、ハンドラが単一なら、handleEventを定義したインスタンスをそのまま渡せばよいと思います。 ハンドラが複数あるなら、Function#bindもしくはアロー関数を利用する必要があります。
lazex

2018/04/14 00:27

プロトタイプに置く、(質問のクラス定義なら `X` の中に置いてメソッドとする)ではインスタンスごとに関数を作るコストはないのですが、いわゆるプライベートメソッドではないため、ユーザが x のプロパティとして見ることが可能になってしまいます。 公開しないために現時点では WeakMap に保持しているのですが、そこは変えられないですよね。 下2つのコードでは addEventListener に登録する関数が bind/アロー関数によってその場で作られた新規のものになるので、 removeEventListener をするためにはここで作ったものの参照を保持することになり、結局インスタンスごとに保持することになると思います。 質問が分かりづらいんだと思いますが、リスナ実体は1つ、リスナのadd/removeが可能、リスナの関数を外部から参照できない(公開するのは export している X のみ・メソッドとしてはもたせたくない)としたいです。
Lhankor_Mhy

2018/04/14 01:08

listenerをプライベートメンバにする、という条件がつくと、さすがにお手上げな気がしますね。
Lhankor_Mhy

2018/04/14 01:29 編集

ご質問をよく読んでみましたが、変数 wmap が クラス X と同じスコープにありますから、WeakMap を使っても隠蔽し切れていないのではないでしょうか。 wmap.get(x) でリスナが取得可能なような? x.listener でリスナが取得可能であることと、本質的な違いはありませんよね?
think49

2018/04/14 01:38

private methodについて。一応、解決策はありますが、少し時間をください。
Lhankor_Mhy

2018/04/14 02:01

ああ、違う。exportすればいいだけですね。 大変失礼しました。
think49

2018/04/14 08:15

(A) listenerにthis値の参照を持たせたい (B) listenerを非公開にしたい (C) listenerを共通化したい (A), (B) を実装する方法を親記事に追記しました。 (C) はイベント周りで不都合が発生するので、親記事に理由を書きました。 ( (C) を実装しようとしたが為に、Lhankor_Mhyさんが罠にはまった、ともいえます)
Lhankor_Mhy

2018/04/14 09:03

これは、lazex さんの WeakMap 案と概ね同様のように見えました。lazex さんはクロージャを作ることを明示してはいないですが、隠蔽する目的のコードなら、きっとそうするおつもりだと思います。
think49

2018/04/14 09:17

WeakMapを使用する発想は同じで、privateなプロパティを作るにはこれしか方法がない認識です。 質問者が迷っておられるのは、おそらく、(C) の部分で、これは基本的に他の条件と合致しない「矛盾する条件」と認識しており、(C) さえなければ質問者が自己解決できている可能性もあります。 でなければ、私が質問文を誤解している事になりますが、私の認識は (A), (B), (C) なので、相違点があれば指摘頂きたく思います。
think49

2018/04/14 09:39 編集

> 理由は公開したくないのと、リスナをインスタンスごとに持たせるのがムダに思うからです(可能なら共通のものにしたい)。 実際、listenerを参照保存しなければ、removeEventListenerで削除対象を選べませんので、この条件は腑に落ちないものがあります。 あるいは、「対象のノードに結びつけられた全ての listener を削除したい」という事なのでしょうか。 > たかだかリスナにオブジェクト参照をもたせたいだけでこんなに変更するのはなにか違う気がします。 こちらは同じ条件でより簡単なコードを求めていると思われますが、this値を変数束縛はアロー関数で回避できるのではないかと。
think49

2018/04/14 09:41

> const wmap = new WeakMap() こちらはコードが示されていない為、私が書いたコードとどちらが「簡単」なのか、を判断できません。 より明確に「簡単なコード」を求める場合は、動くコードを示して頂きたく思います。 https://ja.stackoverflow.com/help/mcve
lazex

2018/04/15 03:09

最終的に全体がどうなるかがわかりづらいようだったので、追記しました。 クロージャについては EsModules のためなくても外部ファイルには公開されません。 > 質問者が迷っておられるのは、おそらく、(C) の部分で、これは基本的に他の条件と合致しない「矛盾する条件」と認識しており、(C) さえなければ質問者が自己解決できている可能性もあります。 そのとおりで、 `x` の参照さえ渡せれば C も解決できるので、 A,B,C すべてを達成する方法がないのだろうかと思っての質問です。 addEventListener の機能がリスナとは別にオプションとして値を渡せばリスナの2つ目以降の引数として渡されるsetTimeoutのような機能があればよかったのに・・・ 整理し直してみると「リスナの実体はひとつでそれに対して event 以外に任意の値を渡す機能はあるか」になりますね リスナの共通化不可能ということで了解しました。ありがとうございます。
think49

2018/04/15 05:03

To: lazex さん > addEventListener の機能がリスナとは別にオプションとして値を渡せばリスナの2つ目以降の引数として渡されるsetTimeoutのような機能があればよかったのに・・・ 引数束縛は Function.prototype.bind によって実装可能です。 (ECMAScript に足りないのは、this 値束縛せずに、引数束縛する機能だと思っています) コードが複雑化する要因は private method がない事にあると思われますが、変数束縛とFunction#callでthis値を渡している事も原因の一つだと思います。 listener共通化、質問文に追記されたコードを基にした改善案(まとめ)、を親記事に追記しました。
think49

2018/04/15 05:13

全体として、複数の問題が併発している状態で、全てをまとめて解決しようとしている為に混乱が生まれている気がします。 問題点を一つずつリストアップして、一つずつ解決していくようにすると、整理しやすいと思います。
lazex

2018/04/15 07:24

まとめありがとうございます。 仮に同一リスナでCもできたとしても、実体が同じだと別インスタンスからリスナセットしたときに上書きになって削除時に問題がでますね ここは気づけていませんでした。 そう考えれば納得いきます。 ありがとうございました。
guest

0

デザインパターンのObserverパターンが適用できると思います。

投稿2018/04/12 15:44

euledge

総合スコア2404

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

回答に誤りがありましたので参考にしないでください。

think49さんのご回答がすっきりしてていいと思いますが、一応別解を。

js

1class X{ 2 constructor(){ 3 this.constructor.prototype.listener = listener.bind(this); 4 } 5 setListener(){ 6 window.addEventListener("click", this.listener, false); 7 } 8 removeListener(){ 9 window.removeEventListener("click", this.listener, false); 10 } 11} 12function listener (eve){ 13 this.prop = eve.target;// this === x 14}

投稿2018/04/13 03:52

編集2018/04/14 00:58
Lhankor_Mhy

総合スコア35869

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Lhankor_Mhy

2018/04/13 06:21

Object.getPrototypeOf(this).listener で普通に動作するみたい。 インスタンス生成前だから動作しないだろう、と思い込んでた。
think49

2018/04/13 13:46

constructor は書き換え可能ですので、Object.getPrototypeOf の方が安全ですね。 ところで、prototypeを上書きすると複数のインスタンスを生成した場合に最新のインスタンスにlistenerが上書きされ、最後のインスタンス以外はthis値が狂ってしまいます。 書き換えるなら、this.listener であるべきではないかと。
lazex

2018/04/14 00:34

bind は内部状態を変えるものではなく、this/引数を bind した新たな関数を作るので think49 さんの書いているように this.listener を書き換える必要があって、質問文で書いた setListener の変更をコンストラクタでしているだけで「xのプロパティとして公開される」のは一緒になってしまいます
Lhankor_Mhy

2018/04/14 00:57

あー、おっしゃるとおりでした。 this を束縛しないとインスタンスを示せず、かといって束縛するならインスタンスに持たせるしかないのか…… とはいえ、this.listener を bind すると今度は別関数になって removeEventListener でイベントを削除できないということですか。 なかなか難問ですね。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問