JavaScriptの this が分かりにくいのは、関数を定義した場所だけでは参照先が決まらないからだ。同じ関数でも、

foo();
obj.foo();
foo.call(obj);
new Foo();

のように呼び出し方が変われば、関数内の this も変わる。さらにアロー関数は通常の関数と同じ規則を使わず、定義位置の外側にある this を引き継ぐ。

したがって、通常の関数は「どう呼ばれたか」、アロー関数は「どの this を外側から引き継いだか」を見る。この区別ができると、this のコードはかなり追いやすくなる。

この記事のサンプルコードはES2015(ES6)以降を前提としている。ES6が標準になってから長いため、普段の開発ではあまり意識する場面はないが、const、アロー関数、class、ES ModulesなどはES2015で追加された構文だ。

一方で、this 自体はES2015で追加された機能ではない。callapply はさらに古く、bind とstrict modeはES5から使える。この記事でES2015以降を前提にしているのは、this の仕組みそのものではなく、説明に使っているコード全体をそのまま動かせる範囲として考えると分かりやすい。

そもそもthisは何のためにあるのか

this は、関数の呼び出し時に「どのオブジェクトを対象に処理するのか」を暗黙に渡す仕組みだ。たとえば次の関数には、特定のオブジェクト名が書かれていない。

function showName() {
  console.log(this.name);
}

そのため、同じ関数を複数のオブジェクトから利用できる。

const user = {
  name: "Alice",
  showName,
};

const admin = {
  name: "Bob",
  showName,
};

user.showName(); // Alice
admin.showName(); // Bob

showName 自体は同じ関数で、違うのは user.showName()admin.showName() という呼び出し方だけだ。通常の関数では、この呼び出し方が this の値を決める。

thisを判断するときは2段階で考える

this を読むときは、最初に次の2点を分ける。

  1. その this が通常の関数に属しているのか、アロー関数から外側の this を参照しているのか
  2. 通常の関数なら、どのような形で呼び出されているのか

通常の関数は自身の this を持つ。一方、アロー関数にはそれがなく、this を使うと外側のスコープにある値を参照する。つまり両者は、同じ this という記法を使っていても判定基準が異なる。

グローバルスコープのthis

ブラウザで通常の <script> としてJavaScriptを実行すると、トップレベルの thiswindow を指す。

console.log(this === window); // true

ただし、<script type="module"> やJavaScriptモジュールでは前提が変わり、ES Modulesのトップレベル thisundefined になる。

<script type="module">
  console.log(this); // undefined
</script>

「グローバルスコープの this は常に window」と覚えると、モジュール化されたコードを読むときに誤る。ブラウザの通常スクリプトとES Modulesでは、この時点ですでに前提が違う。

通常の関数は呼び出し方でthisが決まる

通常の関数では、関数を書いた場所だけを見ても this は決まらない。次の show は定義自体は1つしかない。

function show() {
  console.log(this);
}

この show を単独で呼ぶか、オブジェクト経由で呼ぶか、call を使うかによって参照先が変わる。

show();
obj.show();
show.call(obj);

この3つでは this の扱いがそれぞれ異なる。代表的な呼び出し方まで含めると、次のようになる。

呼び出し方this
foo()strict modeでは undefined、非strictではグローバルオブジェクト
obj.foo()obj
outer.inner.foo()inner
foo.call(obj)obj
foo.apply(obj)obj
foo.bind(obj)()obj
new Foo()新しく作られたインスタンス

1. 単独で関数を呼び出した場合

単独呼び出しとは、オブジェクトや call などを介さず、次のように関数だけを実行する形を指す。

foo();

この場合はstrict modeかどうかで this の結果が変わる。

非strictの通常スクリプト

ブラウザの非strictな通常スクリプトでは、単独呼び出しされた関数の this はグローバルオブジェクト、つまり通常は window になる。

function foo() {
  console.log(this === window);
}

foo(); // true

この前提では、次のコードも 1 を出力する。

var a = 1;

function bar() {
  var a = 2;

  function foo() {
    console.log(this.a);
  }

  foo();
}

bar(); // 1

foo() は単独呼び出しなので、非strictな通常スクリプトでは thiswindow になる。さらに、トップレベルで var a = 1 と宣言すると window.a として参照できるため、this.a の結果は 1 だ。

一方、bar 内の var a = 2 はローカル変数にすぎない。関数が入れ子になっていることと this の参照先は別の話であり、foothisbar のローカルスコープへ結び付くことはない。

strict modeではundefinedになる

同じ単独呼び出しでも、strict modeでは thisundefined になる。

"use strict";

function foo() {
  console.log(this);
}

foo(); // undefined

ES Modulesも自動的にstrict modeとして実行される。現代のフロントエンドではモジュールが前提になることも多く、「foo() なら window」という覚え方は実務ではむしろ事故の元になる。

2. オブジェクトのメソッドとして呼び出した場合

関数をオブジェクト経由で呼び出すと、this はその呼び出し元のオブジェクトを指す。

function foo() {
  console.log(this.a);
}

const obj = {
  a: 1,
  foo,
};

obj.foo(); // 1

ここで効いているのは、関数がどこで定義されたかではなく obj.foo() という呼び出し方だ。同じ foo を別のオブジェクトへ持たせれば、this もそのオブジェクトへ変わる。

const another = {
  a: 2,
  foo,
};

another.foo(); // 2

foo の中身は一切変わっていない。それでも obj.foo() では objanother.foo() では anotherthis になる。

3. オブジェクトが多段になっている場合

オブジェクトが何段も続く場合でも、見る場所は最後の呼び出し部分だ。

function foo() {
  console.log(this.a);
}

const obj = {
  a: 1,
  foo,
};

const outer = {
  a: 2,
  inner: obj,
};

outer.inner.foo(); // 1

outer.inner の値は obj であり、実際の呼び出しは次の形になる。

outer.inner.foo();

ここでは foo の直前にある outer.innerthis になる。結果として this.aobj.a を読み、出力は 1。最も外側に outer が見えていても、それだけで thisouter になるわけではない。

4. メソッドを変数へ取り出すとthisが変わる

this で特に混乱しやすいのが、オブジェクトのメソッドを一度変数へ取り出した場合だ。

const user = {
  name: "Alice",
  showName() {
    console.log(this.name);
  },
};

user.showName(); // Alice

ここでは user.showName() なので thisuser。ところが関数だけを取り出すと、同じ関数でも呼び出し方が変わる。

const showName = user.showName;

showName();

この時点では user.showName() ではなく、単独の showName() になっている。strict modeでは thisundefined になるため、そのまま this.name を読むとエラーだ。

"use strict";

const user = {
  name: "Alice",
  showName() {
    console.log(this.name);
  },
};

const showName = user.showName;

showName();
// TypeError

イベントハンドラやコールバックへメソッドをそのまま渡したときに this が失われる問題も、本質的には「メソッド呼び出しではなくなった」と考えれば理解しやすい。

5. callとapplyでthisを明示的に指定する

callapply を使うと、関数を呼び出す時点で this を明示できる。

function foo(x, y) {
  console.log(this.a, x + y);
}

const first = { a: 1 };
const second = { a: 2 };

foo.call(first, 1, 2); // 1 3
foo.apply(second, [2, 3]); // 2 5

どちらも最初の引数で this を指定し、違うのはそれ以降の引数の渡し方だ。

  • call は第2引数以降へ通常の引数として渡す
  • apply は引数を配列または配列風オブジェクトとして渡す

現在はスプレッド構文があるため、単に配列を展開して関数を呼ぶ目的だけなら apply を使わないコードも多い。それでも、既存コードを読んだり this の明示的な束縛を理解したりするうえで、callapply の違いは押さえておきたい。

6. bindはthisを固定した新しい関数を作る

bindthis を指定できるが、callapply と違って、その場では関数を実行しない。

function foo(x, y) {
  console.log(this.a, x + y);
}

const obj = { a: 3 };

const boundFoo = foo.bind(obj, 3, 4);

boundFoo(); // 3 7

foo.bind(obj, 3, 4) が返すのは、thisobj に結び付けた新しい関数だ。そのため、先ほどのようにメソッドを変数へ取り出したい場合でも、bind を使えば参照先を固定したまま呼び出せる。

const user = {
  name: "Alice",
  showName() {
    console.log(this.name);
  },
};

const showName = user.showName.bind(user);

showName(); // Alice

7. newで呼び出すと新しいインスタンスがthisになる

通常の関数を new で呼び出すと、関数内の this は新しく作られるインスタンスを指す。

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

const person = new Person("Alice");

console.log(person.name); // Alice

new Person("Alice") の実行中、Person 内の this は新しいインスタンスになる。

this.name = name;

この代入は、その新しいインスタンスへ name プロパティを追加している。class構文でも考え方は同じで、インスタンスメソッド内の this は基本的にそのインスタンスを指す。

class Person {
  constructor(name) {
    this.name = name;
  }

  showName() {
    console.log(this.name);
  }
}

const person = new Person("Alice");

person.showName(); // Alice

ただし、classのコードは常にstrict modeだ。メソッドだけを取り出して単独呼び出しすれば thisundefined になるため、「classのメソッドなら常にインスタンスを指す」と考えるのも正しくない。

アロー関数には自身のthisがない

アロー関数は通常の関数と扱いが異なり、自身の this を持たない。そこで参照される this は、アロー関数を定義した場所の外側から引き継がれる。

function foo() {
  const fn = () => {
    this.a = 2;
  };

  fn();
}

const obj = {
  a: 1,
  bar: foo,
};

obj.bar();

console.log(obj.a); // 2

このコードでは、this の参照先が次の順でつながる。

  1. obj.bar() で通常の関数 foo が呼ばれる
  2. foothisobj になる
  3. fn はアロー関数なので自身の this を作らない
  4. fn 内の this は外側にある foothis を使う
  5. 結果として this.a = 2obj.a = 2 になる

この性質は、コールバックの中でも外側の this を維持したい場面で使われる。

アロー関数のthisはcallやbindでも変えられない

アロー関数は自身の this を持たないため、callapplybind を使って別の値へ差し替えることもできない。

const obj = {
  name: "Alice",
};

const arrow = () => {
  console.log(this);
};

arrow.call(obj);

call(obj) と書いても、アロー関数の thisobj になるわけではない。アロー関数では呼び出し方を追うのではなく、「どこで定義されたか」を外側へたどる必要がある。通常の関数とは、this を判定する基準がほぼ逆だ。

アロー関数はコンストラクタにはできない

アロー関数はコンストラクタとして使えず、new で呼び出すとエラーになる。

const Person = (name) => {
  this.name = name;
};

const person = new Person("Alice");
// TypeError: Person is not a constructor

コンストラクタとしてインスタンスを生成したい場合は、通常の関数かclassを使う必要がある。

thisを読むときの判定順序

実際のコードでは、次の順で追うと this の参照先を見失いにくい。

1. アロー関数の中か

アロー関数なら、その関数自身の呼び出し方は this を決めない。外側のスコープへたどり、どの this を引き継いでいるかを見る。

2. newで呼ばれているか

new Foo();

なら、新しく作られるインスタンスが this だ。

3. call、apply、bindで指定されているか

foo.call(obj);
foo.apply(obj);
foo.bind(obj)();

の形なら、明示された objthis になる。

4. オブジェクト経由で呼ばれているか

obj.foo();

なら objthis になる。多段の場合も、関数呼び出しの直前にあるオブジェクトを見る。

outer.inner.foo();

この場合は outer.inner だ。

5. 単独呼び出しか

foo();

なら、strict modeでは undefined、非strictの通常スクリプトではグローバルオブジェクトになる。ES Modulesは自動的にstrict modeなので、現在のJavaScriptで「単独呼び出し = window」と覚えるのは避けたい。

まとめ

通常の関数の this は、基本的に「その関数をどう呼び出したか」で決まる。

foo();
obj.foo();
foo.call(obj);
new Foo();

同じ関数でも呼び出し方が変われば this は変わる。一方、アロー関数は自身の this を持たず、定義された場所の外側にある値を引き継ぐ。

だから、this を見つけたときに関数の定義場所だけを眺めても十分ではない。通常の関数なら呼び出し式を見る。アロー関数なら外側の this をたどる。この2つを分けて考えるのが、最も単純で再現性のある読み方だ。

参考