関数とthis
この章では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
ではなくwindow
などのグローバルオブジェクトを直接参照した方がよいです。
関数とメソッドにおける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 object = {
// `function`キーワードを使ったメソッド
method1: function() {
},
// Arrow Functionを使ったメソッド
method2: () => {
}
};
これに加えてメソッドには短縮記法があります。
オブジェクトリテラルの中で メソッド名(){ /*メソッドの処理*/ }
と書くことで、メソッドを定義できます。
const object = {
// メソッドの短縮記法で定義したメソッド
method() {
}
};
これらのメソッドは、オブジェクト名.メソッド名()
と書くことで呼び出すことができます。
const object = {
// メソッドの定義
method() {
}
};
// メソッド呼び出し
object.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
はundefiend
となります。
一方、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を明示しています。
なぜなら、strict modeではない場合this
はundefined
ではなく、グローバルオブジェクトとなってしまう問題があるためです。
これは、strict modeではない通常の関数呼び出しのみの問題であり、メソッドではこの暗黙的な型変換は行われません。
strict modeは、このような意図しにくい動作を防止するために導入されています。
しかしながら、strict modeのメソッド以外の関数におけるthis
はundefined
となるため使い道がありません。
そのため、メソッド以外でthis
を使う必要はありません。
メソッド呼び出しにおけるthis
次に、メソッドの場合を見ていきます。 メソッドの場合は、そのメソッドは何かしらのオブジェクトに所属しています。 なぜなら、JavaScriptではオブジェクトのプロパティとして指定される関数のことをメソッドと呼ぶためです。
次の例ではmethod1
とmethod2
はそれぞれメソッドとして呼び出されています。
このとき、それぞれのベースオブジェクトはobject
となり、this
はobject
となります。
const object = {
// 関数式をプロパティの値にしたメソッド
method1: function() {
return this;
},
// 短縮記法で定義したメソッド
method2() {
return this;
}
};
// メソッド呼び出しの場合、それぞれの`this`はベースオブジェクト(`object`)を参照する
// メソッド呼び出しの`.`の左にあるオブジェクトがベースオブジェクト
console.log(object.method1()); // => object
console.log(object.method2()); // => object
これを利用すれば、メソッドの中から同じオブジェクトに所属する別のプロパティを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 sayName = person.sayName; は次のようなイメージ
const say = function() {
return this.fullName;
};
// `this`は`undefined`となるため例外をなげる
say(); // => TypeError: Cannot read property 'fullName' of undefined
このように、Arrow Function以外の関数において、this
は定義した時ではなく実行した時に決定されます。
そのため、関数にthis
を含んでいる場合、その関数は意図した呼ばれ方がされないと間違った結果が発生するという問題があります。
この問題の対処法としては大きく分けて2つあります。
ひとつはメソッドとして定義されている関数はメソッドとして呼ぶということです。 メソッドをわざわざただの関数として呼ばなければそもそもこの問題は発生しません。
もうひとつは、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
メソッドの中でArray#map
メソッドを使っています。
このとき、Array#map
メソッドのコールバック関数の中で、Prefixer
オブジェクトを参照するつもりでthis
を参照しています。
しかし、このコールバック関数におけるthis
はundefined
となり、this.prefix
はundefined.prefix
であるためTypeErrorとなります。
"use strict";
// strict modeを明示しているのは、thisがグローバルオブジェクトに暗黙的に変換されるのを防止するため
const Prefixer = {
prefix: "pre",
/**
* `strings`配列の各要素にprefixをつける
*/
prefixArray(strings) {
return strings.map(function(string) {
// コールバック関数における`this`は`undefined`となる(strict mode)
// そのため`this.prefix`は`undefined.prefix`となり例外が発生する
return this.prefix + "-" + string;
});
}
};
// `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(string) {
return this.prefix + "-" + string;
});
}
// ...
このとき、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(string) {
return this.prefix + "-" + string;
};
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(string) {
// `this`ではなく`that`を参照する
return that.prefix + "-" + string;
});
}
};
// `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(string) {
// `this`が第二引数の値と同じになる
// つまり`prefixArray`メソッドと同じ`this`となる
return this.prefix + "-" + string;
}, 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((string) => {
// Arrow Function自体は`this`を持たない
// `this`は外側の`prefixArray`関数がもつ`this`を参照する
// そのため`this.prefix`は"pre"となる
return this.prefix + "-" + string;
});
}
};
// この時、`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
は読み取り専用のキーワードであるため、ユーザーが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 object = {
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 object = {
method() {
callCallback(() => {
// ここでの`this`は1つ外側の関数における`this`と同じ
});
}
};
このArrow Functionにおけるthis
は呼び出し方の影響を受けません。
つまり、コールバック関数がどのように呼ばれるかという実装についてを考えることなくthis
を扱うことができます。
const Prefixer = {
prefix: "pre",
prefixArray(strings) {
return strings.map((string) => {
// `Prefixer.prefixArray()` と呼び出されたとき
// `this`は常に`Prefixer`を参照する
return this.prefix + "-" + string;
});
}
};
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 object = {
method() {
const arrowFunction = () => {
return this;
};
return arrowFunction();
}
};
// 通常の`this`は`object.method`の`this`と同じ
console.log(object.method()); // => object
// `object.method`の`this`を変更すれば、Arrow Functionの`this`も変更される
console.log(object.method.call("THAT")); // => "THAT"
まとめ
this
は状況によって異なる値を参照する性質を持ったキーワードであることを紹介しました。
そのthis
の評価結果をまとめると次の表のようになります。
実行コンテキスト | strict mode | コード | this の評価結果 |
---|---|---|---|
Script | NO | this |
global |
Script | NO | const fn = () => this |
global |
Script | NO | const fn = function(){ return this; } |
global |
Script | YES | this |
global |
Script | YES | const fn = () => this |
global |
Script | YES | const fn = function(){ return this; } |
undefined |
Module | YES | this |
undefined |
Module | YES | const fn = () => this |
undefined |
Module | YES | const fn = function(){ 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に導入されました。1
メソッド以外においてもthis
は評価できますが、実行コンテキストやstrict modeなどによって結果が異なり混乱の元となります。
そのため、メソッドではない通常の関数においてはthis
を使うべきではありません。
また、メソッドにおいてもthis
は呼び出し方によって異なる値となり、それにより発生する問題と対処法についてを紹介しました。
コールバック関数におけるthis
はArrow Functionを使うことで分かりやすく解決できます。
この背景にはArrow Functionで定義した関数はthis
を持たないという性質があります。
1. ES 2015の仕様編集者であるAllen Wirfs-Brock氏もただの関数においてはthis
を使うべきではないと述べている。https://twitter.com/awbjs/status/938272440085446657; ↩