JavaScriptの this が分かりにくいのは、関数を定義した場所だけでは参照先が決まらないからだ。同じ関数でも、
foo();
obj.foo();
foo.call(obj);
new Foo();
のように呼び出し方が変われば、関数内の this も変わる。さらにアロー関数は通常の関数と同じ規則を使わず、定義位置の外側にある this を引き継ぐ。
したがって、通常の関数は「どう呼ばれたか」、アロー関数は「どの this を外側から引き継いだか」を見る。この区別ができると、this のコードはかなり追いやすくなる。
この記事のサンプルコードはES2015(ES6)以降を前提としている。ES6が標準になってから長いため、普段の開発ではあまり意識する場面はないが、const、アロー関数、class、ES ModulesなどはES2015で追加された構文だ。
一方で、this 自体はES2015で追加された機能ではない。call や apply はさらに古く、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点を分ける。
- その
thisが通常の関数に属しているのか、アロー関数から外側のthisを参照しているのか - 通常の関数なら、どのような形で呼び出されているのか
通常の関数は自身の this を持つ。一方、アロー関数にはそれがなく、this を使うと外側のスコープにある値を参照する。つまり両者は、同じ this という記法を使っていても判定基準が異なる。
グローバルスコープのthis
ブラウザで通常の <script> としてJavaScriptを実行すると、トップレベルの this は window を指す。
console.log(this === window); // true
ただし、<script type="module"> やJavaScriptモジュールでは前提が変わり、ES Modulesのトップレベル this は undefined になる。
<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な通常スクリプトでは this が window になる。さらに、トップレベルで var a = 1 と宣言すると window.a として参照できるため、this.a の結果は 1 だ。
一方、bar 内の var a = 2 はローカル変数にすぎない。関数が入れ子になっていることと this の参照先は別の話であり、foo の this が bar のローカルスコープへ結び付くことはない。
strict modeではundefinedになる
同じ単独呼び出しでも、strict modeでは this が undefined になる。
"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() では obj、another.foo() では another が this になる。
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.inner が this になる。結果として this.a は obj.a を読み、出力は 1。最も外側に outer が見えていても、それだけで this が outer になるわけではない。
4. メソッドを変数へ取り出すとthisが変わる
this で特に混乱しやすいのが、オブジェクトのメソッドを一度変数へ取り出した場合だ。
const user = {
name: "Alice",
showName() {
console.log(this.name);
},
};
user.showName(); // Alice
ここでは user.showName() なので this は user。ところが関数だけを取り出すと、同じ関数でも呼び出し方が変わる。
const showName = user.showName;
showName();
この時点では user.showName() ではなく、単独の showName() になっている。strict modeでは this が undefined になるため、そのまま this.name を読むとエラーだ。
"use strict";
const user = {
name: "Alice",
showName() {
console.log(this.name);
},
};
const showName = user.showName;
showName();
// TypeError
イベントハンドラやコールバックへメソッドをそのまま渡したときに this が失われる問題も、本質的には「メソッド呼び出しではなくなった」と考えれば理解しやすい。
5. callとapplyでthisを明示的に指定する
call と apply を使うと、関数を呼び出す時点で 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 の明示的な束縛を理解したりするうえで、call と apply の違いは押さえておきたい。
6. bindはthisを固定した新しい関数を作る
bind も this を指定できるが、call や apply と違って、その場では関数を実行しない。
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) が返すのは、this を obj に結び付けた新しい関数だ。そのため、先ほどのようにメソッドを変数へ取り出したい場合でも、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だ。メソッドだけを取り出して単独呼び出しすれば this は undefined になるため、「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 の参照先が次の順でつながる。
obj.bar()で通常の関数fooが呼ばれるfooのthisはobjになるfnはアロー関数なので自身のthisを作らないfn内のthisは外側にあるfooのthisを使う- 結果として
this.a = 2はobj.a = 2になる
この性質は、コールバックの中でも外側の this を維持したい場面で使われる。
アロー関数のthisはcallやbindでも変えられない
アロー関数は自身の this を持たないため、call、apply、bind を使って別の値へ差し替えることもできない。
const obj = {
name: "Alice",
};
const arrow = () => {
console.log(this);
};
arrow.call(obj);
call(obj) と書いても、アロー関数の this が obj になるわけではない。アロー関数では呼び出し方を追うのではなく、「どこで定義されたか」を外側へたどる必要がある。通常の関数とは、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)();
の形なら、明示された obj が this になる。
4. オブジェクト経由で呼ばれているか
obj.foo();
なら obj が this になる。多段の場合も、関数呼び出しの直前にあるオブジェクトを見る。
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つを分けて考えるのが、最も単純で再現性のある読み方だ。
参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode