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

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

ただいまの
回答率

90.50%

  • JavaScript

    16461questions

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

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

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 3
  • VIEW 383

lazex

score 411

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


 具体例

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

class X{
    setListener(){
        window.addEventListener("click", listener, false)
    }
    removeListener(){
        window.removeEventListener("click", listener, false)
    }
}

function listener(eve){
    x.prop = eve.target
}

export { X }

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

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

setListener(){
    const self = this
    this.listener = function(eve){
        listener.call(this, eve, self)
    }
    window.addEventListener("click", this.listener, false)
}
function listener(eve, x){ // 引数が追加
    x.prop = eve.target
}

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

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

setListener(){
    const self = this
    const listener_wrapper = function(eve){
        listener.call(this, eve, self)
    }
    wmap.set(this, listener_wrapper)
    window.addEventListener("click", listener_wrapper, false)
}
const wmap = new WeakMap()

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

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

 追記

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

const wmap = new WeakMap()

class X {
    setListener(){
        const self = this
        const listener_wrapper = function(eve){
            listener.call(this, eve, self)
        }
        wmap.set(this, listener_wrapper)
        window.addEventListener("click", listener_wrapper, false)
    }
    removeListener(){
        window.removeEventListener("click", wmap.get(this), false)
    }
}

function listener(eve, x){
    x.prop = eve.target
    console.log(x)
}

export { X }

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

+5

 handleEvent

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

Foo.prototype = {
    bar: function bar () {
    document.addEventListener('click'. this, false);
  },
    piyo: piyo,
    handleEvent: function handleEvent (event) {
      this.piyo();
    }
};

 Function.prototype.bind

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

 アロー関数

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

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

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

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

const Clicker = (() => {
  var pm = createPivateMap();

  return class Clicker {
    constructor (message) {
      pm(this, {message: message});
    }
    addListener () {
      pm(this).listener = (event) => console.log(event.type, pm(this).message);
      document.addEventListener('click', pm(this).listener, false);
    }
    removeListener () {
      document.removeEventListener('click', pm(this).listener, false);
    }
  };
})();

const c1 = new Clicker('Hello, World!'),
      c2 = new Clicker('Hello, JavaScript!');

c1.addListener();
c2.addListener();
document.documentElement.click();
c1.removeListener();
c2.removeListener();
document.documentElement.click();

 listenerを共通化したい

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

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

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

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

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

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

class X {
  addListener () {
    window.addEventListener("click", listener, false)
  }
  removeListener () {
    window.removeEventListener("click", listener, false)
  }
}

function listener (event) {
  console.log(event.type);
}

const x1 = new X(),
      x2 = new X();

x1.addListener();       // x1 で listener 追加
x2.addListener();       // x2 で listener 追加(上書き)
document.body.click();  // コンソールには "click" が「1回だけ」出力される
x2.removeListener();    // 上書きされた listener 一つを削除
document.body.click();  // x1, x2 共に click イベントハンドラが発動しない

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

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

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

 まとめ

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

const wmap = new WeakMap();

class X {
  addListener () {
    const listener = templateListener.bind(this);

    wmap.set(this, listener);
    window.addEventListener("click", listener, false);
  }
  removeListener(){
    window.removeEventListener("click", wmap.get(this), false);
  }
}

function templateListener (event) {
  this.prop = event.target;
  console.log(this);
}

const x1 = new X(),
      x2 = new X();

x1.addListener();
x2.addListener();
document.body.click();             // 2回発火
document.documentElement.click();  // 2回発火
x2.removeListener();
document.body.click();             // 1回発火

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

const wmap = new WeakMap()

class X {
  addListener () {
    const listener = createListener(this, templateListener);

    wmap.set(this, listener);
    window.addEventListener("click", listener, false);
  }
  removeListener(){
    window.removeEventListener("click", wmap.get(this), false);
  }
}

function createListener (thisArg, handleEvent) {
  return Object.assign(Object.create(Object.getPrototypeOf(thisArg)), thisArg, {handleEvent: handleEvent, thisArg: thisArg});
}

function templateListener (event) {
  this.thisArg.prop = event.target
  console.log(this);
}

const x1 = new X(),
      x2 = new X();

x1.addListener();
x2.addListener();
document.body.click();             // 2回発火
document.documentElement.click();  // 2回発火
x2.removeListener();
document.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/13 01:01

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

    キャンセル

  • 2018/04/13 10:40

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

    キャンセル

  • 2018/04/13 12:28

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

    キャンセル

  • 2018/04/14 09:27

    プロトタイプに置く、(質問のクラス定義なら `X` の中に置いてメソッドとする)ではインスタンスごとに関数を作るコストはないのですが、いわゆるプライベートメソッドではないため、ユーザが x のプロパティとして見ることが可能になってしまいます。
    公開しないために現時点では WeakMap に保持しているのですが、そこは変えられないですよね。

    下2つのコードでは addEventListener に登録する関数が bind/アロー関数によってその場で作られた新規のものになるので、 removeEventListener をするためにはここで作ったものの参照を保持することになり、結局インスタンスごとに保持することになると思います。

    質問が分かりづらいんだと思いますが、リスナ実体は1つ、リスナのadd/removeが可能、リスナの関数を外部から参照できない(公開するのは export している X のみ・メソッドとしてはもたせたくない)としたいです。

    キャンセル

  • 2018/04/14 10:08

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

    キャンセル

  • 2018/04/14 10:28 編集

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

    キャンセル

  • 2018/04/14 10:38

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

    キャンセル

  • 2018/04/14 11:01

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

    キャンセル

  • 2018/04/14 17:15

    (A) listenerにthis値の参照を持たせたい
    (B) listenerを非公開にしたい
    (C) listenerを共通化したい

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

    キャンセル

  • 2018/04/14 18:03

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

    キャンセル

  • 2018/04/14 18:17

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

    キャンセル

  • 2018/04/14 18:36 編集

    > 理由は公開したくないのと、リスナをインスタンスごとに持たせるのがムダに思うからです(可能なら共通のものにしたい)。
    実際、listenerを参照保存しなければ、removeEventListenerで削除対象を選べませんので、この条件は腑に落ちないものがあります。
    あるいは、「対象のノードに結びつけられた全ての listener を削除したい」という事なのでしょうか。

    > たかだかリスナにオブジェクト参照をもたせたいだけでこんなに変更するのはなにか違う気がします。
    こちらは同じ条件でより簡単なコードを求めていると思われますが、this値を変数束縛はアロー関数で回避できるのではないかと。

    キャンセル

  • 2018/04/14 18:41

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

    キャンセル

  • 2018/04/15 12:09

    最終的に全体がどうなるかがわかりづらいようだったので、追記しました。
    クロージャについては EsModules のためなくても外部ファイルには公開されません。

    > 質問者が迷っておられるのは、おそらく、(C) の部分で、これは基本的に他の条件と合致しない「矛盾する条件」と認識しており、(C) さえなければ質問者が自己解決できている可能性もあります。

    そのとおりで、 `x` の参照さえ渡せれば C も解決できるので、 A,B,C すべてを達成する方法がないのだろうかと思っての質問です。

    addEventListener の機能がリスナとは別にオプションとして値を渡せばリスナの2つ目以降の引数として渡されるsetTimeoutのような機能があればよかったのに・・・
    整理し直してみると「リスナの実体はひとつでそれに対して event 以外に任意の値を渡す機能はあるか」になりますね

    リスナの共通化不可能ということで了解しました。ありがとうございます。

    キャンセル

  • 2018/04/15 14:03

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

    listener共通化、質問文に追記されたコードを基にした改善案(まとめ)、を親記事に追記しました。

    キャンセル

  • 2018/04/15 14:13

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

    キャンセル

  • 2018/04/15 16:24

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

    キャンセル

+1

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

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

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

class X{
    constructor(){
        this.constructor.prototype.listener = listener.bind(this);
    }
    setListener(){
        window.addEventListener("click", this.listener, false);
    }
    removeListener(){
        window.removeEventListener("click", this.listener, false);
    }
}
function listener (eve){
    this.prop = eve.target;// this === x
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/04/13 15:21

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

    キャンセル

  • 2018/04/13 22:46

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

    キャンセル

  • 2018/04/14 09:34

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

    キャンセル

  • 2018/04/14 09:57

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

    キャンセル

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

  • ただいまの回答率 90.50%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • JavaScript

    16461questions

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