ECMAScript 2015以降のJavaScriptの`this`を理解する
この記事はJavaScriptの入門書として書いているjs-primerのthis
に関する部分をベースにしています。
またjs-primerでは書けなかった現在時点(2018年1月1日)でのブラウザの挙動についてを加えたものです。
次の場所にjs-primer版(書籍版)のthis
についての解説があります。
この記事と違って実際にコードを実行しながら読めるので、学習ソースとしては書籍版を推奨します。
また、バグ報告やPRも直接リポジトリにして問題ありません。
おかしい場所を選択した状態で右下にある”Bug Report”ボタンを押せば、簡単にtypoとかのバグを報告できます。(PRでも歓迎)
前置きはこの辺までで、ここから本編。
この記事では注釈がないコードはstrict modeとして扱います
ECMAScript 2015以降のthis
この記事ではthis
という特殊な動作をするキーワードについてを見ていきます。
this
は基本的にはメソッドの中で利用します。しかし、this
は読み取り専用のグローバル変数のようなものでどこにでも書くことができます。
また、this
の参照先(値)は条件によってさまざまです。
this
の参照先は主に次の条件によって変化します。
- 実行コンテキストにおける
this
- コンストラクタにおける
this
- 関数とメソッドにおける
this
- Arrow Functionにおける
this
もっとも複雑な条件が存在するのは「関数とメソッドにおけるthis
」です。
そのためこの記事では関数とthis
の関係を主に扱います。
(コンストラクタにおけるthis
はクラスと一緒に学んだ方がいいので省きます。)
この記事では、さまざまな条件下で変わるthis
の参照先と関数やArrow Functionとの関係を見ていきます。
目標: this
の評価結果を理解する
this
はさまざまな条件でその評価結果(参照先)は異なります。
基本的な関数やメソッドについては次のようなパターンが考えられます。
この記事では表では???
となっているthis
の評価結果を仕組みから理解できることを目標にします。
*
はどの場合でも結果に影響しないということを示すワイルドカード- 関数は
fn()
と実行した場合のthis
の評価結果、メソッドはobj.method()
と実行した場合のthis
の評価結果
例えば、実行コンテキストがScriptでstrict modeがNO(strict modeはない)ときに、コードがconst fn = function(){ return this; }
というのは、次のように評価した場合におけるthis
の評価結果(???
)を表にしたものです。
<script>
const fn = function(){ return this; }
console.log(fn()); // このときに出力結果が ??? に入る
</script>
表の結果がすべてわかっている人にはこの記事は不要だと思います。
記事の最後に答え合わせ用の表を置いています。
目標ではないこと
この記事はthis
を使うために必要な実用的な使い方(プラクティス)を学ぶことが目標の記事ではありません。
この記事ではthis
の実用外の知識(仕様やトップレベルのthis
など)についても書いています。
実用的に使うだけなら、不要な知識も含まれていることにご了承ください。
実用性だけを学ぶならもっと簡略化した内容を元にした方が混乱が減って良いと思います。
実行コンテキストとthis
JavaScriptには実行コンテキストとして”Script”と”Module”があります。
トップレベルにある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”はES 2015で導入され、名前の通りimport
やexport
などのES modulesが動作する実行コンテキストです。
実行コンテキストが”Module”である場合、トップレベルに書かれたthis
は常にundefined
となります。
ブラウザでは、script
要素のtype="module"
属性がついた場合は実行コンテキストが”Module”として実行されます。
このscript
要素の直下に書いたthis
はundefined
となります。
<script type="module">
// 実行コンテキストは"Module"
console.log(this); // => undefined
</script>
このように、コード直下のthis
は実行コンテキストによってundefined
となる場合があります。
単純にグローバルオブジェクトを参照したい場合は、this
ではなくwindow
などのグローバルオブジェクトを直接参照した方がよいです。
📝 Note
なぜModuleコンテキストではトップレベルのthis
がundefined
となるかは次の記事で解説しています。
また現時点では環境へ依存せずにグローバルオブジェクトを取得するのはややこしい方法が必要です。
しかし、現在(2018-01-01) Stage 3のProposalであるglobal
が将来的には利用できます。
関数とメソッドにおけるthis
関数を定義する方法として、function
キーワードによる関数宣言と関数式、Arrow Functionなどがあります。
this
が参照先を決めるルールはArrow 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
まずは、関数宣言や関数式の場合を見ていきます。
次の例では、関数宣言と関数式で定義した関数の中のthis
をコンソールに出力しています。
このとき、fn1
とfn2
はただの関数として呼び出されています。
つまり、ベースオブジェクトがないためthis
はundefined
となります。
"use strict";
function fn1() {
return this;
}
const fn2 = function() {
return this;
};
// 関数の中の`this`が参照する値は呼び出し方によって決まる
// `fn1`と`fn2`どちらもただの関数として呼び出している
// メソッドとして呼び出していないためベースオブジェクトはない
// ベースオブジェクトがない場合、`this`は`undefined`となる
fn1(); // => undefined
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
の場合は、this
がグローバルオブジェクトへと暗黙的に変換されてしまう問題があるからです。
strict modeかどうかによって挙動が異なるのは混乱の元であるため、関数呼び出しする関数においてはthis
を使うべきではありません。
メソッド呼び出しにおけるthis
次に、メソッドの場合を見ていきます。 メソッドの場合は、そのメソッドは何かしらのオブジェクトに所属しています。 なぜなら、JavaScriptではオブジェクトのプロパティとして指定される関数のことをメソッドと呼ぶためです。
次の例ではmethod1
とmethod2
はそれぞれメソッドとして呼び出されています。
このとき、それぞれのベースオブジェクトはobject
となり、this
はobject
となります。
"use strict";
const object = {
// 関数式をプロパティの値にしたメソッド
method1: function() {
return this;
},
// 短縮記法で定義したメソッド
method2() {
return this;
}
};
// メソッド呼び出しの場合、それぞれの`this`はベースオブジェクト(`object`)を参照する
// メソッド呼び出しの`.`の左にあるオブジェクトがベースオブジェクト
object.method1(); // => object
object.method2(); // => object
これを利用すれば、メソッドの中から同じオブジェクトに所属する別のプロパティをthis
で参照できます。
"use strict";
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`オブジェクトとなる
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
に入ります。
function say(message) {
return `${message} ${this.fullName}!`;
}
const person = {
fullName: "Brendan Eich"
};
// `this`を`person`にして`say`関数を呼びだす
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
に入ります。
function say(message) {
return `${message} ${this.fullName}!`;
}
const person = {
fullName: "Brendan Eich"
};
// `this`を`person`にして`say`関数を呼びだす
// callとは異なり引数を配列として渡す
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を渡す
add.call(null, 1, 2); // => 3
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, "こんにちは");
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, "こんにちは");
};
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";
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";
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
を渡して関数を呼びだすこともできます。
また、Arry#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
これにより、Arrow Functionにおける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;
};
fn() === this; // => true
トップレベルに書かれたthis
の値は実行コンテキストによって異なることを紹介しました。
this
の値は、実行コンテキストが”Script”ならばグローバルオブジェクトとなり、“Module”ならばundefined
となります。
次の例のように、Arrow Functionを包むように通常の関数が定義されている場合はどうでしょうか。
"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
は「自身の外側のスコープにあるもっとも近い関数のthis
の値」となります。
つまり、この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
を扱うことができます。
"use strict";
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`は変わらない
fn.call({}); // グローバルオブジェクト
最初に述べたようfunction
キーワードで定義した関数は呼び出し時に、ベースオブジェクトが暗黙的な引数のようにthis
の値として渡されます。
一方、Arrow Functionの関数は呼び出し時にthis
を受け取らずに、定義時のArrow Functionにおけるthis
の参照先が静的に決定されます。
また、this
が変わらないのはあくまでArrow Functionで定義した関数だけで、Arrow Functionのthis
が参照する「自身の外側のスコープにあるもっとも近い関数のthis
の値」はcall
メソッドで変更できます。
"use strict";
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
は状況によって異なる値を参照する性質を持ったキーワードであることについてを紹介しました。
そのthis
の評価結果をまとめると次の表のようになります。
実際にブラウザで実行した結果はWhat is this
value in JavaScript?で確認できます。
Chrome 63を使ってる人は”Module”コンテキストのトップレベルArrow Functionにおけるthis
の挙動が表と一致しないことに気づいたかもしれません。
Chrome 63では次のコードを”Module”コンテキストで実行するとthis
がundefined
ではなく、グローバルオブジェクトを参照します。
<script type="module">
// "Module" context @ Chrome 63
const fn = () => this;
console.log(fn()); // => window
</script>
これはChrome(V8)のバグです。すでに報告して最新のCanary(65相当)では修正されています。
Chrome 63とChrome 65では次のサイトの結果は異なることが分かります。
まとめ
this
はオブジェクト指向プログラミングのメソッドでの利用を目的としています。
メソッド以外においてもthis
は評価できますが、実行コンテキストやstrict modeなどによって結果が異なり混乱の元となります。
そのため、メソッド以外ではthis
を使うべきではありません(ここでは紹介してないコンストラクタは例外です)
この記事で紹介している半分以上(トップレベルにおけるthis
、関数呼び出しのthis
など)のことは知らなくても、実用的にはあまり問題はありません。
実際にはメソッドやArrow Functionにおけるthis
について理解していれば十分です。
特殊な書き方をしていると必要になる知識が半分なので、普通の書き方をして普通の使い方をしましょう。
- ‘this’ in TypeScript · Microsoft/TypeScript Wiki
- TypeScript向けですが、この記事で扱った内容が簡潔にまとまっています
- no-invalid-this - Rules - ESLint - Pluggable JavaScript linter
- 混乱の元となる
this
の書き方をチェックするESLintのルール
- 混乱の元となる
+infinity
— Allen Wirfs-Brock (@awbjs) December 6, 2017
JS `this` is for OO methods, not standalone functions. Never use `this` as a parameter to non-method functions. https://t.co/tFHkqEepcM
ECMAScript 2015の仕様策定者であるAllen Wirfs-Brock氏の意見
また、メソッドにおいてもthis
は呼び出し方によって異なる値となり、それにより発生する問題と対処法についてを紹介しました。
コールバック関数におけるthis
はArrow Functionを使うことで分かりやすく解決できます。
この背景にはArrow Functionで定義した関数はthis
を持たないという性質があります。
もっとthis
について理解してみたい人は、書籍版も実行しながら見ると良さそうです。
書籍の更新を追いたい方はリポジトリをStarやWatchしてください。
📝 Note: This-Binding Syntax proposal
通常の関数をmixin関数のように扱いやすくする::
という構文のProposalがありましたが、しばらくステータスが更新されていません。
(Proposalの元々のAuthorがTC39メンバーではなくなったのも1つの理由)
📝 this
名前解決の仕様
this
bindingの設定は、関数を呼ぶときの次の仕様で決定される。
- https://tc39.github.io/ecma262/#sec-evaluatecall
- https://tc39.github.io/ecma262/#sec-call
- https://tc39.github.io/ecma262/#sec-ecmascript-function-objects-call-thisargument-argumentslist
- https://tc39.github.io/ecma262/#sec-ordinarycallbindthis
大きく分けると、WriteとReadの2つのフェーズでthis
が決まる。
ここでWriteとReadとつけているけど、仕様にそういうフェーズがあるわけじゃなくて自分の解釈です。
Write: 関数呼び出しをする際に、その関数のFunction Environment Recordsの[[ThisValue]]
にthis
の値を入れる
[[ThisValue]]
には次のステップの結果が入る。
(ただしArrow Functionはlexicalなので[[ThisValue]]
を持たない。Arrow Functionの詳細)
12.3.4.2Runtime Semantics: EvaluateCall(func, ref, arguments, tailPosition )のステップを参照する
- プロパティならば
this
はGetThisValue()
の結果super.prop
ならsuper
となる
- それ以外なら
- 6.2.4.1GetBase ( V )の結果
- ベースオブジェクトが
this
となる
- それ以外(ただの関数呼び出し)ならWithBaseObjectの結果
- withの場合
- with bingingの値
- それ以外
- undefined
- withの場合
Read: this
という識別子から、その値が何を参照するかを決めるフェーズ
- https://tc39.github.io/ecma262/#sec-getthisenvironment
this
の解決はスコープと同じく、一個つづ順に内側から外側へ探すのはスコープチェーンと同じ- 見つかるまで再帰的に外側のEnvironment Recordsを探索する
- 見つからない場合は、“Script”や”Module”の実行コンテキストの
this
の値になる - “Module”のGetThisBinding ( )は常に
undefined
- “Script”のGetThisBinding ( )は
[[GlobalThisValue]]
- ただし、Arrow Functionは
[[ThisValue]]
を持たないので必ずスキップされる - もっと近い関数(Function Environment Records)の
[[ThisValue]]
の値がthis
の値となる
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。