JavaScriptのprototypeで、バインドした関数を利用するには?
解決済
回答 3
投稿
- 評価
- クリップ 0
- VIEW 2,900
概要
DIV要素をドラッグできるようにしたいと思い、コードを書いています。
そのコードをJavaScriptのprototypeを使っているのですが、うまく関数を実行できません。
その理由と解決策が知りたいです。
質問本文
DIV要素をドラッグできるようにしたいと思い、コードを書いています。
以下のコードをJavaScriptのprototypeを使っているのですが、どうも、孤立したメソッドのように扱われているようです。
prototypeの中でメソッドをバインドして利用することはできないのでしょうか?
一応、 Dragpos.prototype.test
のような関数を定義し、this.test()
のように呼び出すことは可能でした。
そのときにthis.panel
をconsole.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ページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
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()
という形であれば、レシーバーa
がthis
として設定されますが、x = a.f
などと関数だけ取り出してからx()
としても、this
がa
にはなりません。つまり、コールバック関数のように、関数として渡してしまうと、メソッド呼び出しのようにレシーバーが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()
を使うと、コールバックとして呼び出される際に、どのような呼び出し方であったとしても、その関数内でのthis
がbind()
で指定したオブジェクトに束縛されます。これで現在起こっている問題は解決すると思います。
なお、コールバック関数を受け取る関数の中には、何をthis
としてそのコールバック関数を呼び出すかを指定できる物があります(ArrayのforEach()
等)。addEventListener()
では使用できませんが、そのよう関数ではbind()
の代わりにそのような方法を用いることもできます。さらに、ES2015以降はアロー関数が使えますので、手段がいくつか増えます。
とにかく、コールバック関数についてはthis
について常に意識してください。ただのメソッド呼び出しの形の()無しであるa.f
のように指定した場合、this
が想定外のオブジェクトになって、うまくいくことは少ないです。他にも関数式を直接書いた場合も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
を書き換える設計にしたほうがいいと思う。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
+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 さん
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.33%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
2017/09/06 08:56
おっしゃるとおり、「 this.mouseDown.bind(this)」とするとうまく動きました。
これがJSのthis問題……始めて出会いました。
奥が深いですね……
もっと勉強させていただきます、ありがとうございます!!