きんめも

語彙力がヤバイ

コールバック関数でうまくthisが使えない問題

Javascriptで内部変数を保持できるClass的なものを実装しようとすると、クロージャやプロトタイプが使える。
クロージャの場合、内部で定義される関数(メソッド)がインスタンス生成される度に定義されてしまうのでパフォーマンスが良くない。
なので今回はプロトタイプを使って実装していた。そこで少しつまったこと。

function Person(name){
  this.name = name;
}

Person.prototype.sayName = function(){
  console.log("My name is ",this.name);// -> 'My name is  Taro'
}

Person.prototype.delaySayName = function(){
  window.setTimeout(function(){//コールバックとしての無名関数
    console.log("My name is ",this.name);// -> 'undefined'
  }, 1000);
}

var taro = new Person('Taro');
taro.sayName();     
taro.delaySayName();

このdelaySayNameのコールバック関数のthisはwindowを指している。
これは、関数内のthisが指すのはその関数を呼び出したオブジェクトであることに起因している。
イメージとしてはこんな感じ。

//ブラウザ上のjavascriptはグローバルオブジェクト(Window)上で動いている

var obj = {name:'test'};
var sayThis = function() { 
	console.log(this);
};
obj.sayThis = sayThis;//objオブジェクトにsayThis関数を追加

obj.sayThis(); //objから呼ばれたsayThis関数のthisはobj
sayThis(); //グローバルオブジェクト上のsayThis関数なのでthisはWindow

つまり、問題のコードは

Person.prototype.delaySayName = function(){
  // ここのthisはPersonオブジェクト
  window.setTimeout(function(){
    console.log("My name is ",this.name);
    //この無名関数はwindowのコールバック関数と呼ばれている
    //よってここのthisはwindowとなる(もちろんwindow.nameは存在しない)
  }, 1000);
}

ではどうすればいいかというと、以下の感じ。

function Person(name){
  this.name = name;
}

Person.prototype.sayName = function(){
  console.log("My name is ",this.name);
}

Person.prototype.delaySayName = function(){
  // ここのthisはPersonオブジェクト
  window.setTimeout(function(name){
    console.log("My name is ",name);
  }, 1000, this.name); //第三引数でthis.nameを”値”として与える
}

var taro = new Person('Taro');

taro.sayName();     // -> 'My name is  Taro'
taro.delaySayName();// -> 'My name is  Taro'
taro.name = 'Jiro';

ただしこの場合、this.nameは値として渡るので、途中でnameがJiroになっても出力結果は変更されない。
参照(ポインタ)として渡したい場合、this.nameの参照をコールバック関数に渡す必要がある。
javascriptで参照渡しをしたい時、渡すものがオブジェクトだと勝手に参照が渡される(怖い)。

function Person(name){
  this.name = name;
}

Person.prototype.sayName = function(){
  console.log("My name is ",this.name);
}

Person.prototype.delaySayName = function(){
  // ここのthisはPersonオブジェクト
  var self = this;//thisへの参照をselfに渡す
  window.setTimeout(function(name){
    console.log("My name is ",self.name);
  }, 1000); //self(Personオブジェクト)はオブジェクトなので参照が渡る
}

var taro = new Person('Taro');

taro.sayName();     // -> 'My name is  Taro'
taro.delaySayName();// -> 'My name is  Jiro'
taro.name = 'Jiro';

答えをしればなるほどなぁって感じだった。
thisの指す先は開眼Javascriptの知識が役に立ったような気がしましたまる

参考:
setInterval() → this でひっかかった - Web系がおもしろい。
O'Reilly Japan - 開眼! JavaScript