[JavaScript General] JavaScript における this と Arrow Function

this が何を指すか問題の整理。

定義

this キーワードはコンテキストオブジェクト (カレントオブジェクト) を参照します。一般的に、メソッド内では this は呼び出し元オブジェクト (calling object)を参照します。

JavaScript逆引きレシピ jQuery対応」P.73 より引用、追記。

JavaScript逆引きレシピ jQuery対応
山田 祥寛
翔泳社
売り上げランキング: 210,969
文脈this の指すもの
Functionグローバルオブジェクト。Strict mode では undefined
call / apply引数で指定したオブジェクト。
EventListenerイベントの発生元。
Constructor生成するインスタンス。
Method呼び出し元のオブジェクト(= Receiver)。
Arrow functionアロー関数を定義したコンテキストでの this を捕捉。

実はJavaScriptにおいてthisは非常に簡単なルールで決定されます。そのルールとは,「呼び出した関数の手前(ドットの前)のオブジェクトがthisになる。ただし,手前にドットがない場合はグローバルオブジェクトがthisになる」というものです。

Function

グローバルオブジェクト。
Strict mode では undefined

javascript
'use strict';
function f() {
  console.log(this);
  return this;
}
f(); // undefined

関数呼び出し。

call / apply

引数で指定したオブジェクト。

javascript
'use strict';
function f() {
  console.log(this);
  return this;
}
f(); // undefined
f.call({name: 'Paul'}); // Object {name: "Paul"}

callとapplyの違いは引数を個別に指定するか,配列でまとめて指定できるかの違いです。

EventListener

イベントの発生元。

javascript
'use strict';
class MyEventLister {
  constructor(name) {
    this._name = name;
  }
  onMouseClick() {
    console.log(this._name);
    console.log(this);
  }
}
const paul = new MyEventLister('Paul');
paul.onMouseClick(); // Paul, MyEventLister {_name: "Paul"}
console.log('----');
const el = document.getElementById('button');
el.addEventListener('click', paul.onMouseClick, false); // undefined, <div id="button"></div>

See the Pen JavaScript The value of this within the handler by DriftwoodJP (@DriftwoodJP) on CodePen.

Constructor

生成するインスタンス。

javascript
class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    console.log(this); // Member {firstName: "小穂", lastName: "田畑"}
  }
  getName() {
    return this.lastName + ' ' + this.firstName + '<br />';
  }
}
var mem = new Member('小穂', '田畑');
document.writeln(mem.getName());

new で生成したインスタンス自身 mem になる。

Method

呼び出し元のオブジェクト(= Receiver)。

javascript
class Member {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  getName() {
    console.log(this); // Member {firstName: "小穂", lastName: "田畑"}
    return this.lastName + ' ' + this.firstName + '<br />';
  }
}
var mem = new Member('小穂', '田畑');
document.writeln(mem.getName());

mem.getName() の呼び出し元のオブジェクト mem になる。

メソッド内での関数呼び出し

メソッド内で関数呼び出しを行うと、前述の通り thisundefined となる。

このため、クロージャを使って外側で this_this, self, that といった変数に保存したり、bind メソッドを利用していた。

詳しくは、後述の「Arrow Function」を参照のこと。

Arrow Function

アロー関数を定義したコンテキストでの this を捕捉する機能がある。

JavaScriptではイベント駆動の処理をよく書きます。
例えばDOMがクリックされたら何か処理する、XHRのリクエストが完了したら何か処理をする場合などです。
このような処理をJavaScriptで実装するには、コールバック関数やイベントリスナと呼ばれるものを対象のオブジェクト(DOMやXHR)に設定します。
このコールバック関数を登録する時点でのthisにコールバック関数内からアクセスしたくなる場面がよくありますが、これまではクロージャを使ってthisを保存しておいたり、Function.prototype.bindを使ってthisを束縛したりしていました。
ES6ではArrow Functionと呼ばれる新たな関数定義|式が導入され、このthisに対する煩わしさを解消しています。

ES5 まで。

  • 外側で this を self などの変数に保存しておく。
  • bind メソッドによって this を束縛しておく。
javascript
// ES5: self, _this
var jhon = {
  name: 'jhon',
  helloLater: function() {
    var _this = this;
    // console.log(this); // Object {name: "jhon"}
    setTimeout(function() {
      // console.log(this); // window
      console.log("Hello, I'm " + _this.name);
    }, 1000);
  }
}
jhon.helloLater();
// ES5: bind()
var ringo = {
  name: 'ringo',
  helloLater: function() {
    // console.log(this); // Object {name: "ringo"}
    setTimeout(function() {
      // console.log(this); // Object {name: "ringo"}
      console.log("Hello, I'm " + this.name);
    }.bind(this), 1000);
  }
}
ringo.helloLater();

ES2015から。

javascript
// ES2015: Arrow function
var george = {
  name: 'george',
  helloLater() {
    setTimeout(() => {
      // console.log(this); // object
      console.log("Hello, I'm " + this.name);
    }, 1000);
  }
}
george.helloLater();

See the Pen Javascript ES2015 Arrow function vs. _this vs. bind() by DriftwoodJP (@DriftwoodJP) on CodePen.

補遺

こちらで紹介されていた参考リンクを読む。

JavaScript の this を理解する – tacamy.blog

以下、リンクされていた参考サイトより引用。

 このように、JavaScriptにおけるメソッドとは、特定のオブジェクトと密に結びついているものではありません。そのため、プログラムの文脈によっては、thisが指すオブジェクトも全く異なってくることを覚えておいてください。(略)

オブジェクト指向的なJavaScriptプログラミングを行う際には、今回お見せしたように、thisが指す対象を間違ってしまうことが多いので注意してください。

ここで気づくのは、(関数である)オブジェクト f は、他のオブジェクト a の内部変数をいじる!ってことです。オブジェクト指向はカプセル化だよね〜とか思っている私のように古いタイプ(?)の人間は、this.x と書かれたらまさか他のオブジェクトの内部変数をいじってるとは思わないので、これを理解するというか納得するのに時間がかかりました。(いや、分かってしまえば、「関数がメソッドになる」「関数は独立したオブジェクトだ」とはそういう意味なんだけれども…)

というわけで私と同類な人向けに:JavaScript の this はカプセル化じゃありませんよ!

提示していただいた例と併せ、サイ本の「変数のスコープの検討」という節を読んで、ようやくthisキーワードがスコープチェーンをたどって変数名を解決するというメカニズムが理解できました。

さらに追記。