読者です 読者をやめる 読者になる 読者になる

かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JS 呼び出し元の this を担保するメモ。

例 オブジェクト内の関数でsetTimeoutを使ってその中で、オブジェクト自身をthisとして使いたいような場合

var obj = {
  name: 'obj',
  func: function() {
    console.log(this); // => Object
    setTimeout(function() {
      console.log(this, this.name); // => Window, "result"
    }, 100);
  }
};
obj.func();

そのまま書くと、setTimeoutで呼び出された関数のthisWindowになる
Functionが参照型だから、thisがwindowになってる? undifinedではなくwindowになる理由の理解がまだ不明瞭。

伝統的な方法

var obj = {
  name: 'obj',
  func: function() {
    var self = this;
    setTimeout(function() {
      console.log(self, self.name); // => Object, "obj"
    }, 100);
  }
};
obj.func();

thisをスコープ内にself_thisといった別の変数に割り当てて関数内で変数が定義されていなければ上のスコープから変数を探すjavascriptの仕様を利用する方法。
(self = thisobjへの参照を変数に格納してるって理解で合ってる?)

jQuery $.proxy() を使う方法

var obj = {
  name: 'obj',
  func: function() {
    setTimeout($.proxy(function() {
      console.log(self, self.name); // => Object, "obj"
    }, this), 100);
  }
};
obj.func();

jQuery.proxy( function, context )
context
Type: PlainObject
The object to which the context (this) of the function should be set.
jQuery.proxy() | jQuery API Documentation

$.proxy()は第二引数に第一引数のFunction内で this として扱うものを指定できる。

ES5 Function.prototype.bind を使う方法

'use strict';
var obj = {
  name: 'obj',
  func: function() {
    setTimeout(function() {
      console.log(this, this.name); // => Object, "obj"
    }.bind(this), 100);
  }
};
obj.func();

Function.prototype.bind()
bind() メソッドは、呼び出された時に新しい関数を生成します。最初の引数 thisArg は新しい関数の this キーワードにセットされます。2 個目以降の引数は、新しい関数より前に、ターゲット関数の引数として与えられます。

fun.bind(thisArg[, arg1[, arg2[, …]]])
thisArg
束縛された関数が呼ばれる時、this 値としてターゲット関数に渡される値を指定します。束縛された関数が new 演算子によって構築された場合、この引数は無視されます。
Function.prototype.bind() - JavaScript | MDN

function(){}.bind(<関数内でthisとして使いたいもの>) の様な感じでFunctionのthisを拘束する。
下記のような使い方もできる。

'use strict';
var obj = {
  name: 'obj',
  say: function() { console.log( this, this.name ); }
};

setTimeout( obj.say.bind(obj), 100 ); // => Object, `obj`

[参考] es5のサポート状況

ES6 アロー関数() => を使う

まだIE11とかまだサポートされていないブラウザもありますが、ES6のアロー関数を使えば関数内のthisは呼出し元のthisが保証されるという事のようなので、次のように書く事ができます。

'use strict';
var obj = {
  name: 'obj',
  func: function() {
    setTimeout( () => {
      console.log(this, this,name); // => Object, "obj"
    }, 100);
  }
};
obj.func();

[参考] es6のサポート状況

アロー関数 - JavaScript | MDN
this を束縛しない
//(中略)
アロー関数がスコープ内の this の値を捕捉するため、
//(略)

これを読む限りアロー関数関数が定義されているスコープを関数内のthisとして使用する という事、つまり「thisが何を指すかは関数定義時に決まる」のではないかと思います。(間違ってたら指摘ください
 

ES5 から ES6 へ書き換える際の アロー関数 の落とし穴

アロー関数は関数を定義した際にそのスコープをthisとして束縛するようなので、いままで function() で書いていたものをそのまま () => に書き換えると思わぬ落とし穴にハマってしまうことがあります。

例えば下記のようなコード

// ES5
'use strict';
var obj = {
  name: 'obj',
  callback: function() {
    console.log(this, this.name);
  },
  func: function() {
    setTimeout( this.callback.bind(this), 100 );
  },
};
obj.func(); // => Object "obj"

これを単純にアロー関数に書き換えると… 👇

// ES6
'use strict';
var obj = {
  name: 'obj',
  callback: () => {
    console.log(this, this.name);
  },
  func: () => {
    setTimeout( this.callback, 100 );
  },
};
obj.func(); // => consoleには何も表示されない

これは下記と書き方と同等かと思います。({}はスコープを作成しない…はず)

var obj = {}; // new Object(); でも同じ?
obj.name = 'obj';
obj.callback = () => console.log(this, this.name);
obj.func = () => setTimeout( this.callback, 100 );

なので、アロー関数で定義されたobj.callbackobj.func 内の thisobjではなく、Window(グローバルオブジェクト)に拘束されてしまっているので上手く動作しなかったのだと考えられます。

この場合は、
アロー関数を使わずに、ES5のFunction.prototype.bind()を使って obj.callback 関数を呼び出す

'use strict';
var obj = {
  name: 'obj',
  callback: function() {
    console.log(this, this.name);
  },
  func: function() {
    setTimeout( this.callback.bind(this), 100 );
  },
};
obj.func(); // => Object "obj"

又は
thisobjに拘束してあるアロー関数内から obj.callback 関数を呼び出す

var obj = {
  name: 'obj',
  callback: function() {
    console.log(this, this.name);
  },
  func: function() {
    setTimeout( () => this.callback(), 100 );
  }
};
obj.func(); // => Object "obj"

の様な書き方にすれば上手く動作します。

クラスのようなprototypeにメソッドを付けている様な場合も同様のようです。

だめな例
'use strict';
var MyObject = function() {
  this.name = 'obj';
  return this;
};
MyObject.prototype.callback = () => {
  console.log(this, this.name); // this が Windowになってしまう
}
MyObject.prototype.func = () => {
  setTimeout( this.callback, 100); // this が Windowになってしまう
};
var obj = new MyObject();
obj.func(); // =>

callbackfunc 内の this が Window(グローバルオブジェクト)になるのでうまく動作しない。

動作する例

'use strict';
var MyObject = function() {
  this.name = 'obj';
  return this;
};
MyObject.prototype.callback = () {
  console.log(this, this.name);
}
MyObject.prototype.func = function() {
  setTimeout( this.callback.bind(this), 100);
  // 又は
  // setTimeout( ()=>this.callback(), 100);
};
var obj = new MyObject();
obj.func(); // => MyObject, "obj"

関数の定義をコンストラクタ内などに記述すればアロー関数で定義することもできました。

'use strict';
var MyObject = function() {
  this.name = 'obj';
  this.callback = () => {
    console.log(this, this.name);
  };
  this.func = () => {
    setTimeout(this.callback, 100);
  }
  return this;
};
var obj = new MyObject();
obj.func(); // => MyObject, "obj"

※ 但し継承させたオブジェクトなどで試していないので、この書き方はもしかすると継承させたオブジェクトからだとアロー関数内のthisが継承させたオブジェクトにならないなど不具合が出る可能性がありそうな気がしています。
 

まとめ

ES5もES6もちゃんと学習をしていなくて、ES6にしたら関数はすべてアロー関数() =>にするのかな?のような曖昧な理解だったので、ES6はfunction()()=>を挙動の違いを理解して適材適所に共存させて使うんだ!と今回調べてみて少し理解が深まったように思います。

溜まっていたを技術書少しづづ読んでいます。
今読んでいる「オブジェクト指向JavaScriptの原則」という本は薄めの本ですが、あやふやなまま使っていたECMAScript5で導入されたことをベースに書かれているようなのでES6の前にしっかり読んで理解しておきたいと思ってます。


[参考]

オブジェクト指向JavaScriptの原則

オブジェクト指向JavaScriptの原則