関数とthis
この章ではthisという特殊な動作をするキーワードについて見ていきます。
基本的にはメソッドの中で利用しますが、thisは読み取り専用のグローバル変数のようなものでどこにでも書けます。
加えて、thisの参照先(評価結果)は条件によって異なります。
thisの参照先は主に次の条件によって変化します。
- 実行コンテキストにおける
this - コンストラクタにおける
this - 関数とメソッドにおける
this - Arrow Functionにおける
this
コンストラクタにおけるthisは、次の章である「クラス」で扱います。
この章ではさまざまな条件でのthisについて扱いますが、thisが実際に使われるのはメソッドにおいてです。
そのため、あらゆる条件下でのthisの動きを覚える必要はありません。
この章では、さまざまな条件下で変わるthisの参照先と関数やArrow Functionとの関係を見ていきます。
また、実際にどのような状況で問題が発生するかを知り、thisの動きを予測可能にするにはどのようにするかを見ていきます。
実行コンテキストとthis
最初に「JavaScriptとは」の章において、JavaScriptには実行コンテキストとして"Script"と"Module"があるという話をしました。
どの実行コンテキストでJavaScriptのコードを評価するかは、実行環境によってやり方が異なります。
この章では、ブラウザのscript要素とtype属性を使い、それぞれの実行コンテキストを明示しながらthisの動きを見ていきます。
トップレベル(もっとも外側のスコープ)にあるthisは、実行コンテキストによって値が異なります。
実行コンテキストの違いは意識しにくい部分であり、トップレベルでthisを使うと混乱を生むことになります。
そのため、コードのトップレベルにおいてはthisを使うべきではありませんが、それぞれの実行コンテキストにおける動作を紹介します。
スクリプトにおけるthis
実行コンテキストが"Script"である場合、トップレベルのスコープに書かれたthisはグローバルオブジェクトを参照します。
グローバルオブジェクトは、実行環境ごとに異なるものが定義されています。
ブラウザのグローバルオブジェクトはwindowオブジェクト、Node.jsのグローバルオブジェクトはglobalオブジェクトとなります。
ブラウザでは、script要素のtype属性を指定していない場合は、実行コンテキストが"Script"として実行されます。
このscript要素の直下に書いたthisはグローバルオブジェクトであるwindowオブジェクトとなります。
<script>
// 実行コンテキストは"Script"
console.log(this); // => window
</script>
モジュールにおけるthis
実行コンテキストが"Module"である場合、そのトップレベルのスコープに書かれたthisは常にundefinedとなります。
ブラウザで、script要素にtype="module"属性がついた場合は、実行コンテキストが"Module"として実行されます。
このscript要素の直下に書いたthisはundefinedとなります。
<script type="module">
// 実行コンテキストは"Module"
console.log(this); // => undefined
</script>
このように、トップレベルのスコープのthisは実行コンテキストによってundefinedとなる場合があります。
単純にグローバルオブジェクトを参照したい場合は、thisではなくglobalThisを使います。
globalThisは実行環境のグローバルオブジェクトを参照するためにES2020で導入されました。
実行環境のグローバルオブジェクトは、ブラウザではwindow、Node.jsではglobalのように名前が異なります。
そのため同じコードで、異なるグローバルオブジェクトを参照するには、コード上で分岐する必要がありました。
ES2020ではこの問題を解決するために、実行環境のグローバルオブジェクトを参照するglobalThisが導入されました。
// ブラウザでは`window`オブジェクト、Node.jsでは`global`オブジェクトを参照する
console.log(globalThis);
関数とメソッドにおけるthis
関数を定義する方法として、functionキーワードによる関数宣言と関数式、Arrow Functionなどがあります。
thisが参照先を決めるルールは、Arrow Functionとそれ以外の関数定義の方法で異なります。
そのため、まずは関数定義の種類について振り返ってから、それぞれのthisについて見ていきます。
関数の種類
「関数と宣言」の章で詳しく紹介していますが、関数の定義方法と呼び出し方について改めて振り返ってみましょう。 関数を定義する場合には、次の3つの方法を利用します。
// `function`キーワードからはじめる関数宣言
function fn1() {}
// `function`を式として扱う関数式
const fn2 = function() {};
// Arrow Functionを使った関数式
const fn3 = () => {};
それぞれ定義した関数は関数名()と書くことで呼び出せます。
// 関数宣言
function fn() {}
// 関数呼び出し
fn();
メソッドの種類
JavaScriptではオブジェクトのプロパティが関数である場合にそれをメソッドと呼びます。 一般的にはメソッドも含めたものを関数と言い、関数宣言などとプロパティである関数を区別する場合にメソッドと呼びます。
メソッドを定義する場合には、オブジェクトのプロパティに関数式を定義するだけです。
const obj = {
// `function`キーワードを使ったメソッド
method1: function() {
},
// Arrow Functionを使ったメソッド
method2: () => {
}
};
これに加えてメソッドには短縮記法があります。
オブジェクトリテラルの中で メソッド名(){ /*メソッドの処理*/ }と書くことで、メソッドを定義できます。
const obj = {
// メソッドの短縮記法で定義したメソッド
method() {
}
};
これらのメソッドは、オブジェクト名.メソッド名()と書くことで呼び出せます。
const obj = {
// メソッドの定義
method() {
}
};
// メソッド呼び出し
obj.method();
関数定義とメソッドの定義についてまとめると、次のようになります。
| 名前 | 関数 | メソッド |
|---|---|---|
関数宣言(function fn(){}) |
✔ | x |
関数式(const fn = function(){}) |
✔ | ✔ |
Arrow Function(const fn = () => {}) |
✔ | ✔ |
メソッドの短縮記法(const obj = { method(){} }) |
x | ✔ |
最初に書いたようにthisの挙動は、Arrow Functionの関数定義とそれ以外(functionキーワードやメソッドの短縮記法)の関数定義で異なります。
そのため、まずはArrow Function以外の関数やメソッドにおけるthisを見ていきます。
Arrow Function以外の関数におけるthis
Arrow Function以外の関数(メソッドも含む)におけるthisは、実行時に決まる値となります。
言い方を変えるとthisは関数に渡される暗黙的な引数のようなもので、その渡される値は関数を実行するときに決まります。
次のコードは疑似的なものです。
関数の中に書かれたthisは、関数の呼び出し元から暗黙的に渡される値を参照することになります。
このルールはArrow Function以外の関数やメソッドで共通した仕組みとなります。Arrow Functionで定義した関数やメソッドはこのルールとは別の仕組みとなります。
// 疑似的な`this`の値の仕組み
// 関数は引数として暗黙的に`this`の値を受け取るイメージ
function fn(暗黙的に渡されるthisの値, 仮引数) {
console.log(this); // => 暗黙的に渡されるthisの値
}
// 暗黙的に`this`の値を引数として渡しているイメージ
fn(暗黙的に渡すthisの値, 引数);
関数におけるthisの基本的な参照先(暗黙的に関数に渡すthisの値)はベースオブジェクトとなります。
ベースオブジェクトとは「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことを言います。
ベースオブジェクトがない場合のthisはundefinedとなります。
たとえば、fn()のように関数を呼び出したとき、このfn関数呼び出しのベースオブジェクトはないため、thisはundefinedとなります。
一方、obj.method()のようにメソッドを呼び出したとき、このobj.methodメソッド呼び出しのベースオブジェクトはobjオブジェクトとなり、thisはobjとなります。
// `fn`関数はメソッドではないのでベースオブジェクトはない
fn();
// `obj.method`メソッドのベースオブジェクトは`obj`
obj.method();
// `obj1.obj2.method`メソッドのベースオブジェクトは`obj2`
// ドット演算子、ブラケット演算子どちらも結果は同じ
obj1.obj2.method();
obj1["obj2"]["method"]();
thisは関数の定義ではなく呼び出し方で参照する値が異なります。これは、後述する「thisが問題となるパターン」で詳しく紹介します。
Arrow Function以外の関数では、関数の定義だけを見てthisの値が何かということは決定できない点に注意が必要です。
関数宣言や関数式におけるthis
まずは、関数宣言や関数式の場合を見ていきます。
次の例では、関数宣言で関数fn1と関数式で関数fn2を定義し、それぞれの関数内でthisを返します。
定義したそれぞれの関数をfn1()とfn2()のようにただの関数として呼び出しています。
このとき、ベースオブジェクトはないため、thisはundefinedとなります。
"use strict";
function fn1() {
return this;
}
const fn2 = function() {
return this;
};
// 関数の中の`this`が参照する値は呼び出し方によって決まる
// `fn1`と`fn2`どちらもただの関数として呼び出している
// メソッドとして呼び出していないためベースオブジェクトはない
// ベースオブジェクトがない場合、`this`は`undefined`となる
console.log(fn1()); // => undefined
console.log(fn2()); // => undefined
これは、関数の中に関数を定義して呼び出す場合も同じです。
"use strict";
function outer() {
console.log(this); // => undefined
function inner() {
console.log(this); // => undefined
}
// `inner`関数呼び出しのベースオブジェクトはない
inner();
}
// `outer`関数呼び出しのベースオブジェクトはない
outer();
この書籍では注釈がないコードはstrict modeとして扱いますが、コード例に"use strict";と改めてstrict modeを明示しています(詳細は「JavaScriptとは」のstrict modeを参照)。
なぜなら、strict modeではない状況でthisがundefinedの場合は、thisがグローバルオブジェクトを参照するように変換される問題があるためです。
strict modeは、このような意図しにくい動作を防止するために導入されています。
しかしながら、strict modeのメソッド以外の関数におけるthisはundefinedとなるため使い道がありません。
そのため、メソッド以外でthisを使う必要はありません。
メソッド呼び出しにおけるthis
次に、メソッドの場合を見ていきます。 メソッドの場合は、そのメソッドが何かしらのオブジェクトに所属しています。 なぜなら、JavaScriptではオブジェクトのプロパティとして指定される関数のことをメソッドと呼ぶためです。
次の例ではmethod1とmethod2はそれぞれメソッドとして呼び出されています。
このとき、それぞれのベースオブジェクトはobjとなり、thisはobjとなります。
const obj = {
// 関数式をプロパティの値にしたメソッド
method1: function() {
return this;
},
// 短縮記法で定義したメソッド
method2() {
return this;
}
};
// メソッド呼び出しの場合、それぞれの`this`はベースオブジェクト(`obj`)を参照する
// メソッド呼び出しの`.`の左にあるオブジェクトがベースオブジェクト
console.log(obj.method1()); // => obj
console.log(obj.method2()); // => obj
これを利用すれば、メソッドの中から同じオブジェクトに所属する別のプロパティをthisで参照できます。
const person = {
fullName: "Brendan Eich",
sayName: function() {
// `person.fullName`と書いているのと同じ
return this.fullName;
}
};
// `person.fullName`を出力する
console.log(person.sayName()); // => "Brendan Eich"
このようにメソッドが所属するオブジェクトのプロパティを、オブジェクト名.プロパティ名の代わりにthis.プロパティ名で参照できます。
オブジェクトは何重にもネストできますが、thisはベースオブジェクトを参照するというルールは同じです。
次のコードを見てみると、ネストしたオブジェクトにおいてメソッド内のthisがベースオブジェクトであるobj3を参照していることがわかります。
このときのベースオブジェクトはドットでつないだ一番左のobj1ではなく、メソッドから見てひとつ左のobj3となります。
const obj1 = {
obj2: {
obj3: {
method() {
return this;
}
}
}
};
// `obj1.obj2.obj3.method`メソッドの`this`は`obj3`を参照
console.log(obj1.obj2.obj3.method() === obj1.obj2.obj3); // => true
thisが問題となるパターン
thisはその関数(メソッドも含む)呼び出しのベースオブジェクトを参照することがわかりました。
thisは所属するオブジェクトを直接書く代わりとして利用できますが、一方thisにはいろいろな問題があります。
この問題の原因はthisがどの値を参照するかは関数の呼び出し時に決まるという性質に由来します。
このthisの性質が問題となるパターンの代表的な2つの例とそれぞれの対策について見ていきます。
問題: thisを含むメソッドを変数に代入した場合
JavaScriptではメソッドとして定義したものが、後からただの関数として呼び出されることがあります。 なぜなら、メソッドは関数を値に持つプロパティのことで、プロパティは変数に代入し直すことができるためです。
そのため、メソッドとして定義した関数も、別の変数に代入してただの関数として呼び出されることがあります。
この場合には、メソッドとして定義した関数であっても、実行時にはただの関数であるためベースオブジェクトが変わっています。
これはthisが定義した時点ではなく実行したときに決まるという性質そのものです。
具体的に、thisが実行時に変わる例を見ていきます。
次の例では、person.sayNameメソッドを変数sayに代入してから実行しています。
このときのsay関数(sayNameメソッドを参照)のベースオブジェクトはありません。
そのため、thisはundefinedとなり、undefined.fullNameは参照できずに例外を投げます。
"use strict";
const person = {
fullName: "Brendan Eich",
sayName: function() {
// `this`は呼び出し元によって異なる
return this.fullName;
}
};
// `sayName`メソッドは`person`オブジェクトに所属する
// `this`は`person`オブジェクトとなる
console.log(person.sayName()); // => "Brendan Eich"
// `person.sayName`を`say`変数に代入する
const say = person.sayName;
// 代入したメソッドを関数として呼ぶ
// この`say`関数はどのオブジェクトにも所属していない
// `this`はundefinedとなるため例外を投げる
say(); // => TypeError: Cannot read property 'fullName' of undefined
結果的には、次のようなコードが実行されているのと同じです。
次のコードでは、undefined.fullNameを参照しようとして例外が発生しています。
"use strict";
// const say = person.sayName; は次のようなイメージ
const say = function() {
return this.fullName;
};
// `this`は`undefined`となるため例外を投げる
say(); // => TypeError: Cannot read property 'fullName' of undefined
このように、Arrow Function以外の関数において、thisは定義したときではなく実行したときに決定されます。
そのため、関数にthisを含んでいる場合、その関数は意図した呼ばれ方がされないと間違った結果が発生するという問題があります。
この問題の対処法としては大きく分けて2つあります。
1つはメソッドとして定義されている関数はメソッドとして呼ぶということです。 メソッドをわざわざただの関数として呼ばなければそもそもこの問題は発生しません。
もう1つは、thisの値を指定して関数を呼べるメソッドで関数を実行する方法です。
対処法: call、apply、bindメソッド
関数やメソッドのthisを明示的に指定して関数を実行する方法もあります。
Function(関数オブジェクト)にはcall、apply、bindといった明示的にthisを指定して関数を実行するメソッドが用意されています。
callメソッドは第一引数にthisとしたい値を指定し、残りの引数には呼び出す関数の引数を指定します。
暗黙的に渡されるthisの値を明示的に渡せるメソッドと言えます。
関数.call(thisの値, ...関数の引数);
次の例ではthisにpersonオブジェクトを指定した状態でsay関数を呼び出しています。
callメソッドの第二引数で指定した値が、say関数の仮引数messageに入ります。
"use strict";
function say(message) {
return `${message} ${this.fullName}!`;
}
const person = {
fullName: "Brendan Eich"
};
// `this`を`person`にして`say`関数を呼びだす
console.log(say.call(person, "こんにちは")); // => "こんにちは Brendan Eich!"
// `say`関数をそのまま呼び出すと`this`は`undefined`となるため例外が発生
say("こんにちは"); // => TypeError: Cannot read property 'fullName' of undefined
applyメソッドは第一引数にthisとする値を指定し、第二引数に関数の引数を配列として渡します。
関数.apply(thisの値, [関数の引数1, 関数の引数2]);
次の例ではthisにpersonオブジェクトを指定した状態でsay関数を呼び出しています。
applyメソッドの第二引数で指定した配列は、自動的に展開されてsay関数の仮引数messageに入ります。
"use strict";
function say(message) {
return `${message} ${this.fullName}!`;
}
const person = {
fullName: "Brendan Eich"
};
// `this`を`person`にして`say`関数を呼びだす
// callとは異なり引数を配列として渡す
console.log(say.apply(person, ["こんにちは"])); // => "こんにちは Brendan Eich!"
// `say`関数をそのまま呼び出すと`this`は`undefined`となるため例外が発生
say("こんにちは"); // => TypeError: Cannot read property 'fullName' of undefined
callメソッドとapplyメソッドの違いは、関数の引数への値の渡し方が異なるだけです。
また、どちらのメソッドもthisの値が不要な場合はnullを渡すのが一般的です。
function add(x, y) {
return x + y;
}
// `this`が不要な場合は、nullを渡す
console.log(add.call(null, 1, 2)); // => 3
console.log(add.apply(null, [1, 2])); // => 3
最後にbindメソッドについてです。
名前のとおりthisの値を束縛(bind)した新しい関数を作成します。
関数.bind(thisの値, ...関数の引数); // => thisや引数がbindされた関数
次の例ではthisをpersonオブジェクトに束縛したsay関数をラップした関数を作っています。
bindメソッドの第二引数以降に値を渡すことで、束縛した関数の引数も束縛できます。
function say(message) {
return `${message} ${this.fullName}!`;
}
const person = {
fullName: "Brendan Eich"
};
// `this`を`person`に束縛した`say`関数をラップした関数を作る
const sayPerson = say.bind(person, "こんにちは");
console.log(sayPerson()); // => "こんにちは Brendan Eich!"
このbindメソッドをただの関数で表現すると次のように書けます。
bindはthisや引数を束縛した関数を作るメソッドだということがわかります。
function say(message) {
return `${message} ${this.fullName}!`;
}
const person = {
fullName: "Brendan Eich"
};
// `this`を`person`に束縛した`say`関数をラップした関数を作る
// say.bind(person, "こんにちは"); は次のようなラップ関数を作る
const sayPerson = () => {
return say.call(person, "こんにちは");
};
console.log(sayPerson()); // => "こんにちは Brendan Eich!"
このようにcall、apply、bindメソッドを使うことでthisを明示的に指定した状態で関数を呼び出せます。
しかし、毎回関数を呼び出すたびにこれらのメソッドを使うのは、関数を呼び出すための関数が必要になってしまい手間がかかります。
そのため、基本的には「メソッドとして定義されている関数はメソッドとして呼ぶこと」でこの問題を回避するほうがよいでしょう。
その中で、どうしてもthisを固定したい場合にはcall、apply、bindメソッドを利用します。
問題: コールバック関数とthis
コールバック関数の中でthisを参照すると問題となる場合があります。
この問題は、メソッドの中でArrayのmapメソッドなどのコールバック関数を扱う場合に発生しやすいです。
具体的に、コールバック関数におけるthisが問題となっている例を見てみましょう。
次のコードではprefixArrayメソッドの中でmapメソッドを使っています。
このとき、mapメソッドのコールバック関数の中で、Prefixerオブジェクトを参照するつもりでthisを参照しています。
しかし、このコールバック関数におけるthisはundefinedとなり、undefined.prefixは参照できないためTypeErrorの例外が発生します。
"use strict";
// strict modeを明示しているのは、thisがグローバルオブジェクトに暗黙的に変換されるのを防止するため
const Prefixer = {
prefix: "pre",
/**
* `strings`配列の各要素にprefixをつける
*/
prefixArray(strings) {
return strings.map(function(str) {
// コールバック関数における`this`は`undefined`となる(strict mode)
// そのため`this.prefix`は`undefined.prefix`となり例外が発生する
return this.prefix + "-" + str;
});
}
};
// `prefixArray`メソッドにおける`this`は`Prefixer`
Prefixer.prefixArray(["a", "b", "c"]); // => TypeError: Cannot read property 'prefix' of undefined
なぜコールバック関数の中のthisがundefinedとなるのかを見ていきます。
Arrayのmapメソッドにはコールバック関数として、その場で定義した匿名関数を渡していることに注目してください。
// ...
prefixArray(strings) {
// 匿名関数をコールバック関数として渡している
return strings.map(function(str) {
return this.prefix + "-" + str;
});
}
// ...
このとき、Arrayのmapメソッドに渡しているコールバック関数はcallback()のようにただの関数として呼び出されます。
つまり、コールバック関数として呼び出すとき、この関数にはベースオブジェクトはありません。
そのためcallback関数のthisはundefinedとなります。
先ほどの例では匿名関数をコールバック関数として直接メソッドに渡していますが、一度callback変数に入れてから渡しても結果は同じです。
"use strict";
// strict modeを明示しているのは、thisがグローバルオブジェクトに暗黙的に変換されるのを防止するため
const Prefixer = {
prefix: "pre",
prefixArray(strings) {
// コールバック関数は`callback()`のように呼び出される
// そのためコールバック関数における`this`は`undefined`となる(strict mode)
const callback = function(str) {
return this.prefix + "-" + str;
};
return strings.map(callback);
}
};
// `prefixArray`メソッドにおける`this`は`Prefixer`
Prefixer.prefixArray(["a", "b", "c"]); // => TypeError: Cannot read property 'prefix' of undefined
対処法: thisを一時変数へ代入する
コールバック関数内でのthisの参照先が変わる問題への対処法として、thisを別の変数に代入し、そのthisの参照先を保持するという方法があります。
thisは関数の呼び出し元で変化し、その参照先は呼び出し元におけるベースオブジェクトです。
prefixArrayメソッドの呼び出しにおいては、thisはPrefixerオブジェクトです。
しかし、コールバック関数は改めて関数として呼び出されるためthisがundefinedとなってしまうのが問題でした。
そのため、最初のprefixArrayメソッド呼び出しにおけるthisの参照先を一時変数として保存することでこの問題を回避できます。
次のコードでは、prefixArrayメソッドのthisをthat変数に保持しています。
コールバック関数からはthisの代わりにthat変数を参照することで、コールバック関数からもprefixArrayメソッド呼び出しと同じthisを参照できます。
"use strict";
const Prefixer = {
prefix: "pre",
prefixArray(strings) {
// `that`は`prefixArray`メソッド呼び出しにおける`this`となる
// つまり`that`は`Prefixer`オブジェクトを参照する
const that = this;
return strings.map(function(str) {
// `this`ではなく`that`を参照する
return that.prefix + "-" + str;
});
}
};
// `prefixArray`メソッドにおける`this`は`Prefixer`
const prefixedStrings = Prefixer.prefixArray(["a", "b", "c"]);
console.log(prefixedStrings); // => ["pre-a", "pre-b", "pre-c"]
もちろんFunctionのcallメソッドなどで明示的にthisを渡して関数を呼び出すこともできます。
また、Arrayのmapメソッドなどはthisとなる値を引数として渡せる仕組みを持っています。
そのため、次のように第二引数にthisとなる値を渡すことでも解決できます。
"use strict";
const Prefixer = {
prefix: "pre",
prefixArray(strings) {
// Arrayの`map`メソッドは第二引数に`this`となる値を渡せる
return strings.map(function(str) {
// `this`が第二引数の値と同じになる
// つまり`prefixArray`メソッドと同じ`this`となる
return this.prefix + "-" + str;
}, this);
}
};
// `prefixArray`メソッドにおける`this`は`Prefixer`
const prefixedStrings = Prefixer.prefixArray(["a", "b", "c"]);
console.log(prefixedStrings); // => ["pre-a", "pre-b", "pre-c"]
しかし、これらの解決方法はコールバック関数においてthisが変わることを意識して書く必要があります。
そもそもメソッド呼び出しとその中でのコールバック関数におけるthisが変わってしまうのが問題でした。
ES2015ではthisを変えずにコールバック関数を定義する方法として、Arrow Functionが導入されました。
対処法: Arrow Functionでコールバック関数を扱う
通常の関数やメソッドは呼び出し時に暗黙的にthisの値を受け取り、関数内のthisはその値を参照します。
一方、Arrow Functionはこの暗黙的なthisの値を受け取りません。
そのためArrow Function内のthisは、スコープチェーンの仕組みと同様に外側の関数(この場合はprefixArrayメソッド)を探索します。
これにより、Arrow Functionで定義したコールバック関数は呼び出し方には関係なく、常に外側の関数のthisをそのまま利用します。
Arrow Functionを使うことで、先ほどのコードは次のように書けます。
"use strict";
const Prefixer = {
prefix: "pre",
prefixArray(strings) {
return strings.map((str) => {
// Arrow Function自体は`this`を持たない
// `this`は外側の`prefixArray`関数が持つ`this`を参照する
// そのため`this.prefix`は"pre"となる
return this.prefix + "-" + str;
});
}
};
// このとき、`prefixArray`のベースオブジェクトは`Prefixer`となる
// つまり、`prefixArray`メソッド内の`this`は`Prefixer`を参照する
const prefixedStrings = Prefixer.prefixArray(["a", "b", "c"]);
console.log(prefixedStrings); // => ["pre-a", "pre-b", "pre-c"]
このように、Arrow Functionでのコールバック関数におけるthisは簡潔です。
コールバック関数内でのthisの対処法としてthisを代入する方法を紹介しましたが、
ES2015からはArrow Functionを使うのがもっとも簡潔です。
このArrow Functionとthisの関係についてより詳しく見ていきます。
Arrow Functionとthis
Arrow Functionで定義された関数やメソッドにおけるthisがどの値を参照するかは関数の定義時(静的)に決まります。
一方、Arrow Functionではない関数においては、thisは呼び出し元に依存するため関数の実行時(動的)に決まります。
Arrow Functionとそれ以外の関数で大きく違うことは、Arrow Functionはthisを暗黙的な引数として受けつけないということです。
そのため、Arrow Function内にはthisが定義されていません。このときのthisは外側のスコープ(関数)のthisを参照します。
これは、変数におけるスコープチェーンの仕組みと同様で、そのスコープにthisが定義されていない場合には外側のスコープを探索します。
そのため、Arrow Function内のthisの参照で、常に外側のスコープ(関数)へとthisの定義を探索しに行きます(詳細はスコープチェーンを参照)。
また、thisはECMAScriptのキーワードであるため、ユーザーはthisという変数を定義できません。
// thisはキーワードであるため、ユーザーは`this`という名前の変数を定義できない
const this = "thisは読み取り専用"; // => SyntaxError: Unexpected token this
これにより、通常の変数のようにthisがどの値を参照するかは静的(定義時)に決定されます(詳細は静的スコープを参照)。
つまり、Arrow Functionにおけるthisは「Arrow Function自身の外側のスコープに定義されたもっとも近い関数のthisの値」となります。
具体的なArrow Functionにおけるthisの動きを見ていきましょう。
まずは、関数式のArrow Functionを見ていきます。
次の例では、関数式で定義したArrow Functionの中のthisをコンソールに出力しています。
このとき、fnの外側には関数がないため、「自身より外側のスコープに定義されたもっとも近い関数」の条件にあてはまるものはありません。
このときのthisはトップレベルに書かれたthisと同じ値になります。
// Arrow Functionで定義した関数
const fn = () => {
// この関数の外側には関数は存在しない
// トップレベルの`this`と同じ値
return this;
};
console.log(fn() === this); // => true
トップレベルに書かれたthisの値は実行コンテキストによって異なることを紹介しました。
thisの値は、実行コンテキストが"Script"ならばグローバルオブジェクトとなり、"Module"ならばundefinedとなります。
次の例のように、Arrow Functionを包むように通常の関数が定義されている場合はどうでしょうか。
Arrow Functionにおけるthisは「自身の外側のスコープにあるもっとも近い関数のthisの値」となるのは同じです。
"use strict";
function outer() {
// Arrow Functionで定義した関数を返す
return () => {
// この関数の外側には`outer`関数が存在する
// `outer`関数に`this`を書いた場合と同じ
return this;
};
}
// `outer`関数の返り値はArrow Functionにて定義された関数
const innerArrowFunction = outer();
console.log(innerArrowFunction()); // => undefined
つまり、このArrow Functionにおけるthisはouter関数でthisを参照した場合と同じ値になります。
"use strict";
function outer() {
// `outer`関数直下の`this`
const that = this;
// Arrow Functionで定義した関数を返す
return () => {
// Arrow Function自身は`this`を持たない
// `outer`関数に`this`を書いた場合と同じ
return that;
};
}
// `outer()`と呼び出したときの`this`は`undefined`(strict mode)
const innerArrowFunction = outer();
console.log(innerArrowFunction()); // => undefined
メソッドとコールバック関数とArrow Function
メソッド内におけるコールバック関数はArrow Functionをより活用できるパターンです。
functionキーワードでコールバック関数を定義すると、thisの値はコールバック関数の呼ばれ方を意識する必要があります。
なぜなら、functionキーワードで定義した関数におけるthisは呼び出し方によって変わるためです。
コールバック関数側から見ると、どのように呼ばれるかによって変わってしまうthisを使うことはできません。
そのため、コールバック関数の外側のスコープでthisを一時変数に代入し、それを使うという回避方法を取っていました。
// `callback`関数を受け取り呼び出す関数
const callCallback = (callback) => {
// `callback`を呼び出す実装
};
const obj = {
method() {
callCallback(function() {
// ここでの `this` は`callCallback`の実装に依存する
// `callback()`のように単純に呼び出されるなら`this`は`undefined`になる
// Functionの`call`メソッドなどを使って特定のオブジェクトを指定するかもしれない
// この問題を回避するために`const that = this`のような一時変数を使う
});
}
};
一方、Arrow Functionでコールバック関数を定義した場合は、1つ外側の関数のthisを参照します。
このときのArrow Functionで定義したコールバック関数におけるthisは呼び出し方によって変化しません。
そのため、thisを一時変数に代入するなどの回避方法は必要ありません。
// `callback`関数を受け取り呼び出す関数
const callCallback = (callback) => {
// `callback`を呼び出す実装
};
const obj = {
method() {
callCallback(() => {
// ここでの`this`は1つ外側の関数における`this`と同じ
});
}
};
このArrow Functionにおけるthisは呼び出し方の影響を受けません。
つまり、コールバック関数がどのように呼ばれるかという実装についてを考えることなくthisを扱えます。
const Prefixer = {
prefix: "pre",
prefixArray(strings) {
return strings.map((str) => {
// `Prefixer.prefixArray()` と呼び出されたとき
// `this`は常に`Prefixer`を参照する
return this.prefix + "-" + str;
});
}
};
const prefixedStrings = Prefixer.prefixArray(["a", "b", "c"]);
console.log(prefixedStrings); // => ["pre-a", "pre-b", "pre-c"]
Arrow Functionはthisをbindできない
Arrow Functionで定義した関数ではcall、apply、bindを使ったthisの指定は単に無視されます。
これは、Arrow Functionはthisを持てないためです。
次のようにArrow Functionで定義した関数に対してcallでthisを指定しても、thisの参照先が代わっていないことがわかります。
同様にapplyやbindメソッドを使った場合もthisの参照先は変わりません。
const fn = () => {
return this;
};
// Scriptコンテキストの場合、スクリプト直下のArrow Functionの`this`はグローバルオブジェクト
console.log(fn()); // グローバルオブジェクト
// callで`this`を`{}`にしようとしても、`this`は変わらない
console.log(fn.call({})); // グローバルオブジェクト
最初に述べたようにfunctionキーワードで定義した関数では呼び出し時に、ベースオブジェクトがthisの値として暗黙的な引数のように渡されます。
一方、Arrow Functionの関数は呼び出し時にthisを受け取らず、thisの参照先は定義時に静的に決定されます。
また、thisが変わらないのはあくまでArrow Functionで定義した関数だけで、Arrow Functionのthisが参照する「自身の外側のスコープにあるもっとも近い関数のthisの値」はcallメソッドで変更できます。
const obj = {
method() {
const arrowFunction = () => {
return this;
};
return arrowFunction();
}
};
// 通常の`this`は`obj.method`の`this`と同じ
console.log(obj.method()); // => obj
// `obj.method`の`this`を変更すれば、Arrow Functionの`this`も変更される
console.log(obj.method.call("THAT")); // => "THAT"
まとめ
thisは状況によって異なる値を参照する性質を持ったキーワードであることを紹介しました。
そのthisの評価結果をまとめると次の表のようになります。
| 実行コンテキスト | strict mode | コード | thisの評価結果 |
|---|---|---|---|
| Script | * | this |
globalThis |
| Script | * | const fn = () => this |
globalThis |
| Script | NO | const fn = function(){ return this; } |
globalThis |
| Script | YES | const fn = function(){ return this; } |
undefined |
| Script | * | const obj = { method: () => { return this; } } |
globalThis |
| Module | YES | this |
undefined |
| Module | YES | const fn = () => this |
undefined |
| Module | YES | const fn = function(){ return this; } |
undefined |
| Module | YES | const obj = { method: () => { return this; } } |
undefined |
| * | * | const obj = { method(){ return this; } } |
obj |
| * | * | const obj = { method: function(){ return this; } } |
obj |
*はどの場合でも
thisの評価結果に影響しないということを示しています。
実際にブラウザで実行した結果はWhat is this value in JavaScriptというサイトで確認できます。
thisはオブジェクト指向プログラミングの文脈でJavaScriptに導入されました。
メソッド以外においてもthisは評価できますが、実行コンテキストやstrict modeなどによって結果が異なり、混乱の元となります。
そのため、メソッドではない通常の関数においてはthisを使うべきではありません。1
また、メソッドにおいてもthisは呼び出し方によって異なる値となり、それにより発生する問題と対処法について紹介しました。
コールバック関数におけるthisはArrow Functionを使うことでわかりやすく解決できます。
この背景にはArrow Functionで定義した関数はthisを持たないという性質があります。
1. ES2015の仕様編集者であるAllen Wirfs-Brock氏もただの関数においてはthisを使うべきではないと述べている。https://twitter.com/awbjs/status/938272440085446657; ↩