この記事はJavaScriptの入門書として書いているjs-primerthisに関する部分をベースにしています。 また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の評価結果を仕組みから理解できることを目標にします。

`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で導入され、名前の通りimportexportなどのES modulesが動作する実行コンテキストです。 実行コンテキストが"Module"である場合、トップレベルに書かれたthisは常にundefinedとなります。

ブラウザでは、script要素のtype="module"属性がついた場合は実行コンテキストが"Module"として実行されます。 このscript要素の直下に書いたthisundefinedとなります。

<script type="module">
// 実行コンテキストは"Module"
console.log(this); // => undefined
</script>

このように、コード直下のthisは実行コンテキストによってundefinedとなる場合があります。 単純にグローバルオブジェクトを参照したい場合は、thisではなくwindowなどのグローバルオブジェクトを直接参照した方がよいです。

📝 Note

なぜModuleコンテキストではトップレベルのthisundefinedとなるかは次の記事で解説しています。

また現時点では環境へ依存せずにグローバルオブジェクトを取得するのはややこしい方法が必要です。 しかし、現在(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の値)はベースオブジェクトとなります。 ベースオブジェクトとは「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことを言います。 ベースオブジェクトがない場合のthisundefinedとなります。

たとえば、fn()のように関数を呼び出したとき、このfn関数呼び出しのベースオブジェクトはないため、thisundefiendとなります。 一方、obj.method()のようにメソッドを呼び出したとき、このobj.methodメソッド呼び出しのベースオブジェクトはobjオブジェクトとなり、thisobjとなります。

// `fn`関数はメソッドではないのでベースオブジェクトはない
fn();
// `obj.method`メソッドのベースオブジェクトは`obj`
obj.method();
// `obj1.obj2.method`メソッドのベースオブジェクトは`obj2`
// ドット演算子、ブラケット演算子どちらも結果は同じ
obj1.obj2.method();
obj1["obj2"]["method"]();

thisは関数の定義ではなく呼び出し方で参照する値が異なります。これは、後述する「thisが問題となるパターン」で詳しく紹介します。 Arrow Function以外の関数では、関数の定義だけを見てthisの値が何かということは決定できない点には注意が必要です。

関数宣言や関数式におけるthis

まずは、関数宣言や関数式の場合を見ていきます。

次の例では、関数宣言と関数式で定義した関数の中のthisをコンソールに出力しています。 このとき、fn1fn2はただの関数として呼び出されています。 つまり、ベースオブジェクトがないためthisundefinedとなります。

"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ではない場合にthisundefinedの場合は、thisがグローバルオブジェクトへと暗黙的に変換されてしまう問題があるからです。 strict modeかどうかによって挙動が異なるのは混乱の元であるため、関数呼び出しする関数においてはthisを使うべきではありません。

メソッド呼び出しにおけるthis

次に、メソッドの場合を見ていきます。 メソッドの場合は、そのメソッドは何かしらのオブジェクトに所属しています。 なぜなら、JavaScriptではオブジェクトのプロパティとして指定される関数のことをメソッドと呼ぶためです。

次の例ではmethod1method2はそれぞれメソッドとして呼び出されています。 このとき、それぞれのベースオブジェクトはobjectとなり、thisobjectとなります。

"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メソッドを参照)のベースオブジェクトはありません。 そのため、thisundefinedとなり、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(関数オブジェクト)にはcallapplybindといった明示的にthisを指定して関数を実行するメソッドが用意されています。

callメソッドは第一引数にthisとしたい値を指定し、残りの引数は呼びだす関数の引数となります。 暗黙的に渡されるthisの値を明示的に渡せるメソッドといえます。

関数.call(thisの値, ...関数の引数);

次の例ではthispersonオブジェクトを指定した状態で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]);

次の例ではthispersonオブジェクトを指定した状態で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された関数

次の例ではthispersonオブジェクトに束縛した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メソッドをただの関数で表現すると次のように書けます。 bindthisや引数を束縛した関数を作るメソッドということがわかります。

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!"

このようにcallapplybindメソッドを使うことでthisを明示的に指定した状態で関数を呼び出せます。 しかし、毎回関数を呼びだすたびにこれらのメソッドを使うのは、関数を呼びだすための関数が必要になってしまい手間がかかります。 そのため、基本的には「メソッドとして定義されている関数はメソッドとして呼ぶこと」でこの問題を回避するほうがよいでしょう。 その中で、どうしてもthisを固定したい場合にはcallapplybindメソッドを利用します。

問題: コールバック関数とthis

コールバック関数の中でthisを参照すると問題となる場合があります。 この問題は、メソッドの中でArray#mapメソッドなどコールバック関数を扱う場合に発生しやすいです。

具体的に、コールバック関数におけるthisが問題となっている例を見てみましょう。 次のコードではprefixArrayメソッドの中でArray#mapメソッドを使っています。 このとき、Array#mapメソッドのコールバック関数の中で、Prefixerオブジェクトを参照するつもりでthisを参照しています。

しかし、このコールバック関数におけるthisundefinedとなり、this.prefixundefined.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

なぜコールバック関数の中でのthisundefinedとなるのかを見ていきます。 Array#mapメソッドにはコールバック関数として、その場で定義した匿名関数を渡していることに注目してください。

// ...
    prefixArray(strings) {
        // 匿名関数をコールバック関数として渡している
        return strings.map(function(string) {
            return this.prefix + "-" + string;
        });
    }
// ...

このとき、Array#mapメソッドに渡しているコールバック関数はcallback()のようにただの関数として呼び出されます。 つまり、コールバック関数として呼びだすとき、この関数にはベースオブジェクトはありません。 そのためcallback関数のthisundefinedとなります。

先ほどの匿名関数をコールバック関数として渡しているのは、一度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メソッドの呼び出しにおいては、thisPrefixerオブジェクトです。 しかし、コールバック関数はあらためて関数として呼び出されるためthisundefinedとなってしまうのが問題でした。

そのため、最初のprefixArrayメソッド呼び出しにおけるthisの参照先を一時変数として保存することでこの問題を回避できます。 つぎのように、prefixArrayメソッドのthisthat変数に保持しています。 コールバック関数からは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におけるthisouter関数で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で定義した関数にはcallapplybindを使ったthisの指定は単に無視されます。 これは、Arrow Functionはthisをもつことができないためです。

次のようにArrow Functionで定義した関数に対してcallthisをしても、thisの参照先が代わっていないことが分かります。 同様にapplybindメソッドを使った場合も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の評価結果をまとめると次の表のようになります。

`this`の評価結果のまとめ

実際にブラウザで実行した結果はWhat is this value in JavaScript?で確認できます。

Chrome 63を使ってる人は"Module"コンテキストのトップレベルArrow Functionにおけるthisの挙動が表と一致しないことに気づいたかもしれません。 Chrome 63では次のコードを"Module"コンテキストで実行するとthisundefinedではなく、グローバルオブジェクトを参照します。

<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について理解していれば十分です。 特殊な書き方をしていると必要になる知識が半分なので、普通の書き方をして普通の使い方をしましょう。

ECMAScript 2015の仕様策定者であるAllen Wirfs-Brock‏氏の意見

また、メソッドにおいてもthisは呼び出し方によって異なる値となり、それにより発生する問題と対処法についてを紹介しました。 コールバック関数におけるthisはArrow Functionを使うことで分かりやすく解決できます。 この背景にはArrow Functionで定義した関数はthisを持たないという性質があります。

もっとthisについて理解してみたい人は、書籍版も実行しながら見ると良さそうです。

書籍の更新を追いたい方はリポジトリをStarやWatchしてください。

asciidwango/js-primer: JavaScriptの入門書

Star Watch

📝 Note: This-Binding Syntax proposal

通常の関数をmixin関数のように扱いやすくする::という構文のProposalがありましたが、しばらくステータスが更新されていません。 (Proposalの元々のAuthorがTC39メンバーではなくなったのも1つの理由)

📝 this名前解決の仕様

this bindingの設定は、関数を呼ぶときの次の仕様で決定される。

大きく分けると、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 )のステップを参照する

  • プロパティならば
    • thisGetThisValue()の結果
      • super.propなら
        • superとなる
      • それ以外なら
  • それ以外(ただの関数呼び出し)ならWithBaseObjectの結果
    • withの場合
      • with bingingの値
    • それ以外
      • undefined

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の値となる