関数と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要素の直下に書いたthisundefinedとなります。

<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 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の値)はベースオブジェクトとなります。 ベースオブジェクトとは「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことを言います。 ベースオブジェクトがない場合のthisundefinedとなります。

たとえば、fn()のように関数を呼び出したとき、このfn関数呼び出しのベースオブジェクトはないため、thisundefinedとなります。 一方、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

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

次の例では、関数宣言で関数fn1と関数式で関数fn2を定義し、それぞれの関数内でthisを返します。 定義したそれぞれの関数をfn1()fn2()のようにただの関数として呼び出しています。 このとき、ベースオブジェクトはないため、thisundefinedとなります。

"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ではない場合thisundefinedではなく、グローバルオブジェクトとなってしまう問題があるためです。 これは、strict modeではない通常の関数呼び出しのみの問題であり、メソッドではこの暗黙的な型変換は行われません。

strict modeは、このような意図しにくい動作を防止するために導入されています。 しかしながら、strict modeのメソッド以外の関数におけるthisundefinedとなるため使い道がありません。 そのため、メソッド以外でthisを使う必要はありません。

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

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

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

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

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

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

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

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

次の例ではthispersonオブジェクトに束縛した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メソッドをただの関数で表現すると次のように書けます。 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, "こんにちは");
};
console.log(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";
// 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

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

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

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

先ほどの匿名関数をコールバック関数として直接メソッドに渡していますが、一度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メソッドの呼び出しにおいては、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(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は読み取り専用のキーワードであるため、ユーザーが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における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 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で定義した関数には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`は変わらない
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 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;