KDE.BLOG

web制作で学んだことを記していきます

【JavaScript基礎】thisとは何か・シーン別参照先のまとめ

<目次>

thisとは何か

JavaScriptでのthisは、関数内で使用されるキーワードで、関数が実行される際に値が設定されます。
このときに設定される値は関数を呼び出すオブジェクトへのリンクです。 別の言い方をすると、実行中の関数をプロパティ、もしくはメソッドとして保持するオブジェクトへの参照と言えますが、言葉だけでは説明しにくいのでコードを見てみます。

const taro = {
    age: 30,
    gender: '男性',
    getGender: function() {
        return this.gender;
    }
};

console.log(taro.getGender()); // '男性'

上記コード内のthisは実行中の関数つまりgetGenderメソッドを保持するtaroオブジェクトを参照しています。

thisの値の決められ方

thisの値は、関数が実行時に呼び出される際のコンテクスト(状況)によって変わります。 下記コードで確認してみます。

const foo = 'foo';

const myObj = {
    foo: 'object foo',
}

function myFunc() {
    console.log(this.foo);
}

// myObjのbarメソッドにmyFunc関数を定義
myObj.myFunc = myFunc;

myFunc(); // 'foo'
myObj.myFunc(); // 'object foo'

myFunc()とmyObj.myFunc()は同じ関数を呼び出していますが、その関数内のthisはmyFunc()が呼び出されたコンテクストによって異なっています。
myFunc()をグローバルスコープで実行するか、myObjオブジェクトのメソッドとして実行するかによって呼び出しているthisが違うことが確認できます。

※また、最初に言っておくとstrictモードと非strictモードでもthisの値に違いがあります。
これについては違いが生じるときに説明します。

グローバルコンテクスト内でのthis

グローバルコンテクスト内(すべての関数の外側)では、webブラウザでの実行の場合、thisはwindowを参照します。
strictモード、非strictモード問わずです。

console.log(this === window); // true

this.a = 1;
console.log(window.a); // 1

関数内でのthis

関数宣言、関数式でのthisはwindowを参照します。

const myFunc1 = function() {
    return this;
};
console.log(myFunc1()); // window

function myFunc2() {
    return this;
}
console.log(myFunc2()); // window

しかしstrictモード、ES2015では異なります
下記のようにundefinedになります。

'use strict'

const myFunc1 = function() {
    return this;
};
console.log(myFunc1()); // undefined

function myFunc2() {
    return this;
}
console.log(myFunc2()); // undefined

これはどういうことでしょうか。

strictモードについて調べてみると下記のような記述があります。

strict モードでは、this として関数に渡された値をオブジェクトへボクシングしません。非ストリクトモードでの関数にとってthis は常にオブジェクトになります。thisの値は、実行時にthisオブジェクト値として提供されたオブジェクトであったり、真偽値・文字列・数値などのプリミティブな値がthis として呼び出した時はオブジェクトへボクシングした値、undefined または null の this で呼び出された時はグローバルオブジェクトとなります。 (特定の this を指定するために call、apply、bind を使用してください)。
(中略)
strict モードの関数では、指定された this を変更せずに使用します (Strict モード - JavaScript | MDNより)

オブジェクトへボクシング(Boxing)とは、値の型をオブジェクト型に変換することをいいます。
つまり非strictモードでは、本来undefinedやnullである値が、ボクシングされてグローバルオブジェクトのwindowになっているということだと思われます。

'use strict';

function myFunc() {
    return this;
}

// すべてtrue
console.log(
    myFunc() === undefined,
    myFunc.call(2) === 2,
    myFunc.call(null) === null,
    myFunc.call(undefined) === undefined,
    myFunc.bind(true)() === true
);

メソッド内でのthis

メソッド内でのthisはそのメソッドを格納しているオブジェクトを参照します。
本記事の冒頭に挙げたコードもメソッドとして呼び出しています。

下記でもう少し詳しく見てみます。

'use strict';

// (1)
const myObj = {
    foo: 'foo',
    checkProp: function() {
        return this.foo;
    }
}
console.log(myObj.checkProp()); // foo

// (2)
function myFunc() {
    return this;
}
console.log(window.myFunc() === window); // true

(1)に関して、myObjオブジェクトのcheckPropメソッドにthisを用いています。
このthisはmyObj自身を指すため、this.fooはmyObj.fooになります。直感的でわかりやすいかと思います。

(2)に関して、これはひとつ前の「関数でのthis」の通りにいくとthisはstrictモードではundefinedとなりますが、最後の実行文で呼び出し方をwindowオブジェクトのmyFunc()メソッドとしているので、thisはwindowとなります。

入れ子の関数でのthis

入れ子での関数はどうなるのか、実際に試してみると下記のような結果になります。

const myFunc1 = function() {
    console.log(this); // (1) window
     
    const myFunc2 = function() {
        console.log(this); // (2) window

        const myObj1 = {
            prop1: function() {
                console.log(this); // (3) Object {prop1: function}

                const myFunc3 = function() {
                    console.log(this); // (4) window
                    console.log(func()); // (5) window

                    function func() {
                        return this;
                    }
                }();
            }
        }
        myObj1.prop1();
    }();
}();

一瞬混乱しそうですが、それぞれの関数は関数宣言・関数式か、メソッドかを判断すれば簡単です。 (1)、(2)、(4)は関数式(即時実行しています)なのでグローバルオブジェクトを参照、
(3)はmyObj1のprop1()メソッドで、myObj1を参照、
(5)は関数宣言を実行しているのでグローバルオブジェクトを参照しています。

ただ、これもやはりstrictモードではグローバルオブジェクトの参照はせずにundefinedとなります。

コンストラクタ内でのthis

new演算子を使って呼び出されたコンストラクタ関数内でのthisは、そのコンストラクタによって生成されたインスタンス自身を参照します。 この挙動はstrictモードでも変わりません。

const Person = function(name) {
    this.name = name;
}
Person.prototype.checkName = function() {
    console.log(this.name);
}

const taro = new Person('taro');
const hanako = new Person('hanako');

taro.checkName();   // taro
hanako.checkName(); // hanako

ちなみにnew演算子を伴わない場合、コンストラクタ関数は普通の関数になります。

const Person = function(name) {
  this.name = name;
}
Person.prototype.checkName = function() {
  console.log(this.name);
}

const taro = Person('taro');

console.log(taro) // undefined (1)
taro.checkName(); // エラー  (2)

変数taroにはインスタンスではなくPerson関数の実行結果(戻り値)が入っています。
Person関数には戻り値が明示されていないのでデフォルトのundefinedが返るため(1)ではそのような結果となっています。
(2)では当然checkNameプロパティは存在しない(というよりundefinedにプロパティはセットできない)ためエラーとなります。

call()メソッド、apply()メソッドでのthis

ここまで見てきたように、thisの参照先は呼び出され方によって変化します。
しかしcall()メソッド、apply()メソッドを使うとthisを束縛して関数を呼び出すことが可能です。

下記ではオブジェクトと関数を生成しています。
MyFunctionを通常通り呼び出すとグローバルで実行されるのでwindowオブジェクトにfoo、barプロパティが追加されるのですが、call()もしくはapply()メソッドを使うことでmyObjectを呼び出し元にすることができます。
call()、apply()メソッドはFunction()オブジェクトのメソッドのため関数オブジェクトすべてが持っているメソッドです。

構文は下記になります。

関数名.call(thisとするオブジェクト, 関数に渡す引数1, 引数2...);

関数名.apply(thisとするオブジェクト, [関数に渡す引数1, 引数2...]);

thisとするオブジェクト、つまり第一引数は必須で、第二引数以降の関数に渡す引数は任意です。
call()とapply()の違いは、その第二引数以降の値の渡し方です。
call()ではカンマ区切りに対して、apply()は配列の形で渡します。実行結果はどちらも同じです。

const myObj = {};

const myFunc = function(param1, param2) {
    this.foo = param1;
    this.bar = param2;
    console.log(this);
};

// 通常通り実行するとwindowに追加される
myFunc('foo', 'bar'); // window {foo: "foo", bar: "bar", ...略}

// myObjをthisとしてMyFuncを呼ぶ
myFunc.call(myObj, 'foo', 'bar'); // Object {foo: "foo", bar: "bar"}
// 下も同じ実行結果
myFunc.apply(myObj, ['foo', 'bar']); // Object {foo: "foo", bar: "bar"}

このように関数内でthisが参照する値をオーバーライド(上書き)、束縛できることが分かるかと思います。
他にもcall()、apply()メソッドのメリットはあるのですが、また後日まとめたいと思います。

アロー関数内でのthis

ES2015で追加されたアロー関数では、thisを束縛せず、関数定義時のコンテクストのthisを参照します。
アロー関数が定義された場所によって、関数内のthisの値が固定されます。

const myObj = {
    foo: 'foo',
    method: function(){
        console.log(this);   // (1) Object {foo: "foo", method: function}
        
        // 従来の関数
        const myFunc = function(){
            console.log(this);   // (2) window
        }();
        
        // アロー関数
        const arrowFunc = (() => {
            console.log(this);   // (3) Object {foo: "foo", method: function}
        })();
    }
};

myObj.method();

(1)はメソッド、(2)は関数式なのでこのような結果になります。 (3)はアロー関数で、定義された場所はmethod()メソッド内なので、thisはmyObj()オブジェクトを参照しています。

アロー関数を使うと、入れ子での関数で、thisを退避させる必要がなくなります。

thisの退避

thisの退避とは、コンテクストによって変わるthisの参照先を変数に入れておき、一定の値を参照しつづけるようにするテクニックのことをいいます。

const myObj = {
    foo: 'foo',
    checkThis: function() {
        const self = this; // thisを退避させておく
        console.log(self.foo);    // (1) foo
        
        const myFunc1 = function() {
            console.log(self.foo);    // (2) foo 
        }();
    }
};
myObj.checkThis();

(1)と(2)ともにthisの代わりにselfを指定しています。
(2)ではthisを与えるとグローバルオブジェクトの参照となりエラーとなりますが、selfを使うことでmyObjオブジェクトを参照できます。

ちなみに、変数名ではself以外に_this、thatなどが慣習として使用されるようです。

まとめ

以上長くなりましたので、コンパクトにまとめてみます。

thisの使用シーン 参照先
関数宣言、関数式 グローバルオブジェクト(strictモード、ES2015ではundefined)
メソッド そのメソッドを格納しているオブジェクト自身
コンストラクタ 生成されたインスタンス自身
call()、apply()メソッド 引数に渡されたオブジェクト
アロー関数 関数定義時のコンテクスト(スコープ)のthis

参考