2007年08月20日

thisの件

window.setInterval()や、addEventListner()、attachEvent()で関数を指定する時のthisの件についてなんとなくわかっていたような気がしていましたが、わかっていなかったので調べてみました。

まず、thisはキーワードです。読み取り専用です。
thisはカレントオブジェクトを参照できます。
function hoe() {
this.name = 'hoe desu!';
this.age = '10 sai!';
}
var hoehoe = new hoe();
alert(hoehoe.name); //'hoe desu!'
alert(hoehoe.age); //'10 sai!'

としてコンストラクタ関数でオブジェクトのプロパティの初期化などに使用します。
この場合、関数hoe内のthisは新しく作成されて変数hoehoeに結びついているオブジェクトを参照しています。カレントオブジェクトはhoehoeです。
もし、newしないで、関数hoeをそのまま呼び出すと、
hoe();
alert(name); //'hoe desu!'
alert(age); //'10 sai!'

グローバル変数 name と age が作成されました。
これは this がブラウザ上でのグローバルオブジェクトである window を参照しているからです。window.name、window.ageに値を設定しているという事です。

...
function hoe2(obj) {
alert(obj.id);
}
...
<input type="button" id="button1" onclick="hoe2(this)">
...
と、フォーム要素のonclickで、thisを引数にして関数を呼ぶと、thisは呼び出しもとのフォーム要素を参照するようになります。
ここで
function hoe3() {
alert(this);
}
...
<input type="button" id="button2" onclick="hoe3()">
...
とすると、thisはwindowを参照します。
hoe3は、ただ関数を呼び出しただけです。フォーム要素もオブジェクトですが、そのオブジェクトのメソッド内で関数を呼び出しただけです。なので、thisはwindowを参照する事になります。
hoe2(this)の場合は、hoe2を呼び出した時点でフォーム要素オブジェクトを参照するthisを引数にしているという事になります。

thisの話になると、必ず出てくるsetInterval()を使ってみます。
なんとなくこんな感じというコードを書いてみます。
function hoe() {
this.count = 0;
this.countUp = function() {
this.count++;
if (this.count > 3) {
clearInterval(this.interval_id);
}
};
this.start = function() {
this.interval_id = setInterval(this.countUp, 500);
};
}
var obj_hoe = new hoe();
...
<input id="button3" type="button" onclick="obj_hoe.start()">

...

これは予想した動作を行わずに、延々と
NaN
を表示し続けます。hoeの中で定義されている関数countUp()を呼び出しているのですが、setIntervalに設定された関数countUp内で、thisがwindowになってしまっているからです。
setIntervalに設定する時にはthisはhoeを参照しています。
一定時間毎に呼び出される時countUp関数内のthisはwindowを参照しています。
countUpはhoeのメソッドではないですか!と思いますが、setIntervalにしてみれば、「なんだか知らないが関数が指定された」だけです。
オブジェクトhoeと、setIntervalに指定された関数とは関係がないと言う事です。hoeはcountUp関数をプロパティとしてはいるけれども、countUp関数はhoeに所属しているわけではないという事です。
function hoe() {...の前に
var count = 0;
とグローバルの変数countを追加すると、NaNではなく数字をカウントアップするようになります。但し、関数countUp内のthis.interval_idがwindow.intervl_idを参照しているため、数字が3より大きくなっても停止しません。

setIntervalで呼ぶ関数を、無名関数でラップ(包む)してみます。

// this.interval_id = setInterval(this.countUp, 500);
this.interval_id = setInterval(function() {this.countUp()}, 500);

(関数の中で別の関数を呼ぶので、this.countUpではなくて、this.countUp()です。function xxx() {alert}ではなくて、function xxx() {alert()}と同じです。)
これを実行すると、エラーになります。今度はhoeの中で定義されている関数countUp()を呼び出していません。setInvervalで設定している関数のthisがwindowになってしまっているからです。
setIntervalが一定時間毎に無名関数
function() {...}
を呼び出しています。その関数のbodyで使用されているthisは実行時に参照を解決するので、参照先はwindowとなるのです。
試しにvar obj_hoe = new hoe();の後に
function countUp() {alert('global no countUp desu!');
とグローバルの関数を定義すると、'global no countUp desu!'と表示し続けます。

今度はsetIntervalに関数を設定する前に、ローカル変数selfを作成し、thisを入れておきます。(selfは特別意味のあるものではなく、慣例的にselfという名前の変数を使用するという事のようです。)
this.start = function() {
// this.interval_id = setInterval(this.countUp, 500);
// this.interval_id = setInterval(function() {this.countUp()}, 500);
var self = this;
self.interval_id = setInterval(function() {self.countUp()}, 500);
};

今度は当初の目論見どおり、1から4まで数字を表示して停止しました。

これまで、なぜこうなるのかわかりませんでした。
setInterval(function() {....}, 500);

とした時点で、この無名関数はクロージャを作成しているのですね?
クロージャはその環境を保存しますから、
var local_value = xxx;
setInterval(function() {....}, 500);

とまわりにローカル変数があれば、クロージャにその値が保存され、クロージャが呼び出される度にその値が使われるという事です。
thisは変数ではなくて、キーワードだから呼び出される度に、その時の呼び出し元のオブジェクトを参照する。
selfはクロージャが作成された時点で、thisが参照しているオブジェクト、つまりhoeを保存しているので、呼び出される度にhoeを参照できるという事です。

やっとわかりました。
posted by ほえ at 15:45| Comment(1) | TrackBack(0) | JavaScript | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
追記させて頂きます。

thisを使うと現在のコンテキストオブジェクトへの参照を得ることができ、その実行コンテキストのプロパティやメソッドを this を通して (this.member のようにして) 参照できます。コンテキストオブジェクトは関数に渡される「隠しパラメータ」と みなす事が出来ます。this が渡されるのには 3 通りの方法があります。

型                     /トリガー
メソッドの呼び出しで暗黙的に        /object.method( . . . )
Function.prototype.call を経由して明示的に /function.call(object, . . . )
Function.prototype.apply を経由して明示的に /function.apply(object, [. . .])

上記のいずれの方法も取られなければ、グローバルオブジェクト(window)がコンテキストオブジェクトとして渡されます。これは、例えば関数外部のトップレベルコードで this が使われた時や、func(arg1, arg2); のようにオブジェクトに付随せずに関数が呼ばれた時などです。

引用元:http://developer.mozilla.org/ja/docs/Core_JavaScript_1.5_Reference:Operators:Special_Operators:this_Operator


setInteval関数の第一引数は、コンテキストオブジェクト、すなわち呼び出し元オブジェクトからは切り離されます。よってこの上記のサンプルコードの場合“this.countUp”はトップレベルコードとしてみなされ、thisはwindowと解釈されます。
Posted by 島袋哲也 at 2008年06月25日 18:48
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

この記事へのトラックバックURL
http://blog.seesaa.jp/tb/52075978
※言及リンクのないトラックバックは受信されません。

この記事へのトラックバック
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。