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

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

ただいまの
回答率

90.52%

  • JavaScript

    16398questions

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

JavaScriptのprototypeで、バインドした関数を利用するには?

解決済

回答 3

投稿

  • 評価
  • クリップ 0
  • VIEW 794

nnahito

score 1706

 概要

DIV要素をドラッグできるようにしたいと思い、コードを書いています。
そのコードをJavaScriptのprototypeを使っているのですが、うまく関数を実行できません。
その理由と解決策が知りたいです。

 質問本文

DIV要素をドラッグできるようにしたいと思い、コードを書いています。
以下のコードをJavaScriptのprototypeを使っているのですが、どうも、孤立したメソッドのように扱われているようです。

prototypeの中でメソッドをバインドして利用することはできないのでしょうか?

一応、 Dragpos.prototype.testのような関数を定義し、this.test()のように呼び出すことは可能でした。
そのときにthis.panelconsole.logで表示させてもうまく表示ができます。

しかし、Dragpos.prototype.moveは、「TypeError: this.move is not a function」と出ます。
この理由がまだ勉強中なのでわかりません。
ご存知の方がいらっしゃいましたら、ご教授ください。
よろしくお願いいたします。

var Dragpos = (function() {
    // プロパティ定義エリア
    var panel = ''; // ドラッグさせたいオブジェクトを保存するもの
    var drag = false; // trueのとき、オブジェクトが移動するというフラグ
    var mousemove = {};

    /**
     * コンストラクタ
     * @param       {Object}    element     ドラッグさせたいオブジェクト
     * @constructor
     */
    function Dragpos(element) {
        // 初期値で与えられた要素をプロパティとして持たせる
        this.panel = document.getElementById(element);

        // フラグを(一応)初期化しておく
        this.drag = false;

        // アクションをすべてバインド
        document.addEventListener('mousedown', this.mouseDown, false);
        document.addEventListener('mouseup', this.mouseUp, false);
        document.addEventListener('mousemove', this.mouseMove, false);

    }


    // クリックされたとき
    Dragpos.prototype.mouseDown = function(e){
        drag = true;
    }



    // クリックが離されたとき
    Dragpos.prototype.mouseUp = function(e){
        drag = false;
    }



    // マウスが移動したとき
    Dragpos.prototype.mouseMove = function(e){
        if (drag == true){
            //this.move(e);
            console.log(this.panel);
        }
    }



    // 要素の座標を移動させる処理
    Dragpos.prototype.move = function (e) {
        var offsetX; // スクロール位置(横)
        var offsetY; // スクロール位置(縦)
        var x; // x座標
        var y; // y座標
        var rect = {}; // 四角形の(x, y, w, h)が入る

        // 四角形の(x, y, w, h) = (X座標, Y座標, 幅, 高さ)を取得
        rect.x = this.panel.offsetLeft;
        rect.y = this.panel.offsetTop;
        rect.w = this.panel.clientWidth;
        rect.h = this.panel.clientHeight;

        offsetX = rect.w / 2;
        offsetY = rect.h / 2;

        if (e.pageX > rect.x && e.pageX < rect.x + rect.w) {
            if (e.pageY > rect.y && e.pageY < rect.y + rect.h) {
                x = e.pageX - offsetX;
                y = e.pageY - offsetY;
                this.panel.style.position = 'absolute';
                this.panel.style.top = y + 'px';
                this.panel.style.left = x + 'px';
            }
        }

    }

    return Dragpos;
})();
<!DOCTYPE html>
<html lang="ja">

<head>
    <!-- ページタイトル -->
    <title>ドラッグテスト</title>

    <!-- メタタグエリア -->
    <meta charset="utf-8">

    <!-- JS読み込み -->
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script src="dragpos.js"></script>

    <!-- ページ内CSS -->
    <style>
        #foo {
            width: 150px;
            height: 150px;
            margin: 20px;
            color: #CCC;
            background: #EFEFEF;
            border: 3px dotted #DDD;
            font-size: 2em;
            font-weight: 900;
            text-align: center;
            line-height: 150px;
        }
    </style>

</head>

<body>

    <div id="foo" draggable="true">Move</div>


    <script>
    $(document).ready(function(){
        // 要素を動かせるようにする
        var dp = new Dragpos('foo');
    });
    </script>

</body>

</html>

 参考

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 3

checkベストアンサー

+3

問題は下記の部分です。

        // アクションをすべてバインド
        document.addEventListener('mousedown', this.mouseDown, false);
        document.addEventListener('mouseup', this.mouseUp, false);
        document.addEventListener('mousemove', this.mouseMove, false);

JavaScriptでコールバック関数を渡す場合、いわゆるthis問題がおきます。(アロー関数ではない)関数宣言や関数式で作られた関数自体にthisが何であるかという情報は含まれていません。そのため、呼び出す時の状況に応じてthisが変わります。よくあるメソッド呼び出しa.f()という形であれば、レシーバーathisとして設定されますが、x = a.fなどと関数だけ取り出してからx()としても、thisaにはなりません。つまり、コールバック関数のように、関数として渡してしまうと、メソッド呼び出しのようにレシーバーがthisになるとは限らなくなります。

方法はいくつかありますが、ES5でも使用できる方法としてbind()thisが何であるかを束縛することです。

        // アクションをすべてバインド
        document.addEventListener('mousedown', this.mouseDown.bind(this), false);
        document.addEventListener('mouseup', this.mouseUp.bind(this), false);
        document.addEventListener('mousemove', this.mouseMove.bind(this), false);

※ 将来的なバグを防ぐために、全てに.bind(this)を付けることを推奨します。

上記のようにbind()を使うと、コールバックとして呼び出される際に、どのような呼び出し方であったとしても、その関数内でのthisbind()で指定したオブジェクトに束縛されます。これで現在起こっている問題は解決すると思います。

なお、コールバック関数を受け取る関数の中には、何をthisとしてそのコールバック関数を呼び出すかを指定できる物があります(ArrayのforEach()等)。addEventListener()では使用できませんが、そのよう関数ではbind()の代わりにそのような方法を用いることもできます。さらに、ES2015以降はアロー関数が使えますので、手段がいくつか増えます。

とにかく、コールバック関数についてはthisについて常に意識してください。ただのメソッド呼び出しの形の()無しであるa.fのように指定した場合、thisが想定外のオブジェクトになって、うまくいくことは少ないです。他にも関数式を直接書いた場合もthis問題が起きますので、同じく注意が必要です。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2017/09/06 08:56

    ご回答ありがとうございます。
    おっしゃるとおり、「 this.mouseDown.bind(this)」とするとうまく動きました。

    これがJSのthis問題……始めて出会いました。
    奥が深いですね……
    もっと勉強させていただきます、ありがとうございます!!

    キャンセル

+2

raccyさんの回答で正常に動作しそうなので、
私は他の軽微な不具合になりそうな箇所を補修します。

var Person = (function() {
  var name;

  var Person = function(_name, _age){
    name = _name;
    this.age = _age;
  };

  Person.prototype.hello = function(){
    return "Hello, my name is " + name + ". age is " + this.age;
  };

  return Person;
})();

var miyabi = new Person("miyabi", 17);

// ===== ここから検証開始 =====

console.log(miyabi);
// Person {age: 17} <- nameが表示されない

console.log(miyabi.hello());
// "Hello, my name is miyabi. age is 17"

miyabi.name = "nnahito";
miyabi.age = 18;

console.log(miyabi);
// Person {age: 18, name: "nnahito"}

console.log(miyabi.hello());
// "Hello, my name is miyabi. age is 18" <- nameが変わってない

これはよくあるクロージャーを利用したクラスの定義方法です。
この例の時、nameとageはそれぞれ以下のような扱いになります。

name: プライベート変数
即時実行関数の中で隠蔽され、インスタンスからはアクセスできない。

age: パブリック変数
外部から容易に書き換え可能なプロパティ

※このプライベート変数、パブリック変数というのはPHPの世界の呼び方であり、その認識で名付けた造語です。


なんでそんなことを説明するん?という話ですが…
このプライベート変数とパブリック変数は相互不干渉なので片方を書き換えても、もう片方の同名の変数は値が切り替わらない。

混同してるみたいだからちゃんと意識して使い分けてね!

var Dragpos = (function() {
    // これらはプライベート変数だね
    var panel = ''; 
    var drag = false;
    var mousemove = {};

    function Dragpos(element) {
        // これらはthis.xxxだからパブリック変数を書き換えてるね
        this.panel = document.getElementById(element);
        this.drag = false;
    }
}

蛇足のおまけ

この方法のプライベート変数は片手落ちなので、
正しく理解して梱包出来なきゃ使わない方がいいっす。

var Person = (function() {
  var name;
  var age;

  var Person = function(_name, _age){
    name = _name;
    age = _age;
  };

  Person.prototype.hello = function(){
    return "Hello, my name is " + name + ". age is " + age;
  };

  return Person;
})();

var miyabi = new Person("miyabi", 17);
var nnahito = new Person("nnahito", 18);

console.log(miyabi.hello());
// "Hello, my name is nnahito. age is 18" <- miyabiどこいった

console.log(nnahito.hello());
// "Hello, my name is nnahito. age is 18"

当然同じnameとage変数参照するんだからこうなるよね。
管理出来ないならおとなしくthis.xxxを書き換える設計にしたほうがいいと思う。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2017/09/06 10:34

    補足ありがとうございます。

    先程、
    var a = new Dragpos('hoge');
    var b = new Dragpos('hoga');
    とやると、「蛇足のおまけ」と同じ現象が生じました。。。

    > 当然同じnameとage変数参照するんだから
    new でインスタンスが区切れていると思っていても、実際はそうでないんですね…なんでやねん。
    JSは謎が多いです……

    キャンセル

+1

下記スレッドを参考にしてみて下さい。

(2017/09/06 11:05追記)
サンプルコードを追記しました。

 addEventListener + handleEvent

addEventListener は第二引数に handleEvent プロパティを持つオブジェクトを指定した場合、this 値を指定したオブジェクトに束縛することが出来ます。

function Dragpos (element) {
  // 中略

  document.addEventListener('mousedown', this, false);
  document.addEventListener('mouseup', this, false);
  document.addEventListener('mousemove', this, false);
}

Dragpos.prototype.handleEvent = function handleEvent (event) {
  switch (event.type) {
    case 'mousedown':
      return this.mouseDown.call(this, event);
    case 'mouseup':
      return this.mouseUp.call(this, event);
    case 'mousemove':
      return this.mouseMove.call(this, event);
  }

  throw new Error('unknown event type');
};

 Function.prototype.bind

event.currentTarget に束縛された this 値を Function.prototype.bind で new Dragpos に束縛し直します。

document.addEventListener('mousedown', this.mouseDown.bind(this), false);
document.addEventListener('mouseup', this.mouseUp.bind(this), false);
document.addEventListener('mousemove', this.mouseMove.bind(this), false);

 jQuery.proxy

Function .prototype.bind と同様。

document.addEventListener('mousedown', jQuery.proxy(this.mouseDown, this), false);
document.addEventListener('mouseup', jQuery.proxy(this.mouseUp, this), false);
document.addEventListener('mousemove', jQuery.proxy(this.mouseMove, this), false);

 jQuery.prototype.on + event.data

this から取り出すのを諦めて、jQuery.prototype.on の event.data 経由で this 値を参照します。

function Dragpos (element) {
  // 中略

  jQuery(document).on('mousemove', {thisArg: this}, this.mouseMove);
}

Dragpos.prototype.mouseMove = function mouseMove (event) {
  var thisArg = event.data.thisArg;

  if (thisArg.drag) {
    console.log(thisArg.panel);
  }
};

Re: nnahito さん

投稿

編集

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2017/09/06 01:49

    ご回答ありがとうございます。

    申し訳ないのですが、私の知識レベルが不足しており、どこを参考にすればよいかわかりませんでした…
    おそらく「(方法2) Function.prototype.bind を使う」かなと思い、
    コンストラクタ部分に「this.move.bind(this);」を追記してみましたがだめでした……

    キャンセル

  • 2017/09/06 11:14

    親記事にサンプルコードを追記しました。
    どの方法を選択するかは好みもありますが、個人的には handleEvent or bind の二択ですね。

    本題ではないですが、jQuery#ready と addEventListener を併用しているのがやや気になりました。
    ここまで書いたのであれば、jQuery は外してもいいのではと思います。

    キャンセル

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

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

関連した質問

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

  • JavaScript

    16398questions

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