👌

【JS】thisの挙動を理解する

2023/06/11に公開

このページでは JavaScript における"this"について解説します

問題

早速ですが、問題です。

下記コードでは、thisが正しく読み込めずthis.datasetがundifinedとなっており、
undefinedのプロパティ(今回は'buttonNumber')を読み取ろうとしてTypeErrorとなっています。
thisがbuttonオブジェクトを指し示すように修正できればエラーが解消するのですが、
どのように修正すればよいでしょうか。

※こちらは私がこの記事を書くきっかけになったコードになります。

const button = document.querySelector('.problem-button')
button.addEventListener('click', () => {
    // ボタンのdata-button-number属性を取得
    const buttonNumber = this.dataset.buttonNumber;
    console.log(buttonNumber)
});

解答

解答は、addEventListnerの第二引数に指定したアロー関数を変更する
となります。解答例では、アロー関数表記から無名関数表記に変更させています。
このようにすることで、thisの参照先がbuttonとなります。

const button = document.querySelector('.problem-button')
button.addEventListener('click', function () {
    // ボタンのdata-button-number属性を取得
    const buttonNumber = this.dataset.buttonNumber;
    console.log(buttonNumber)
});

こちらの問題の解答が分かった方は、以降の記事を読んでいただいても得られるものはないと
思われますので、読み飛ばしていただければと思います...

そうでない方に向けて、thisの基本から今回の問題の解説までを説明したいと思います。

"this"とは何か?

"this"は日本語で"これ"という意味ですが、JavaScriptにおける"this"は"このオブジェクト"
と読み替えられます。

すなわち、thisはオブジェクトを指し示すものとなります。
基本的にはこの理解で問題ないのですが、"この"が指しているものが様々な場面で異なります...
(この点が、thisがややこしくなっている点だと思います...)

基本

通常の場合、thisは呼び出し元のオブジェクトを指し示します。

const car = {
    color: 'black',
    checkColor: function() {
        console.log('この車の色は' + this.color + 'です')
    }
}

car.checkColor(); // 出力:この車の色はblackです

上記の例では、thisが含まれる関数(checkColor)はcarオブジェクトから呼び出されているので、
thisはcarオブジェクトを指します。
carのcolorプロパティは'black'であるため、出力は'black'となっています。

例外

呼び出し元のオブジェクトがない場合

呼び出し元のオブジェクトがない状態で呼び出された場合、thisはグローバルオブジェクトを指し示します。

window.color = 'white';

function sample() {
    console.log('この車の色は' + this.color + 'です')
}

sample() // 出力:この車の色はwhiteです

上記のように、呼び出し元のオブジェクトがなくthisが含まれる関数がそのまま呼び出されている場合、thisはスコープチェーンを辿って参照すべきオブジェクトを探しに行きます

結果、グローバルオブジェクト(windowオブジェクト)をthisが参照することになり、
グローバルオブジェクトのプロパティに設定されている'white'を出力します。

アロー関数内でthisを用いた場合

アロー関数内でthisを用いた場合、呼び出し元のオブジェクト有無に関わらず
thisはグローバルオブジェクト
を指し示します。

window.color = 'white';

const car2 = {
    color: 'black',
    checkColor: () => {
        console.log('この車の色は' + this.color + 'です')
    }
};

car2.checkColor() // 出力:この車の色はwhiteです

一見すると上記の場合、checkColor()はcar2オブジェクトから呼び出されているため、
this.color = car2.color = 'black'となり出力が'black’となりそうです。

しかし、結果はグローバルオブジェクトのプロパティである'white'が出力されています。
こちらの理由ですが、アロー関数内ではthisが使えないためとなります。

呼び出し元のオブジェクトを指し示すというthisをアロー関数では使うことが出来ないため、
thisは呼び出し元オブジェクトがない場合と同じ挙動で参照すべきオブジェクトを探しに行き
結果としてグローバルオブジェクトを参照することになります。

解答の解説

前段が長くなってしまいましたが、これまでの説明を踏まえて、
冒頭でご紹介した問題の解説をします。

const button = document.querySelector('.problem-button')
button.addEventListener('click', () => {
    // ボタンのdata-button-number属性を取得
    const buttonNumber = this.dataset.buttonNumber;
    console.log(buttonNumber)
});

上記は問題のコードとなります。
ボタンをクリックしたら、addEventListnerのトリガーが発火し、
第二引数の関数が呼び出されます。

その第二引数の関数の中で、thisが用いられています。
addEventListnerが呼び出し元でそのオブジェクトはbuttonオブジェクトであるため
thisが指し示すオブジェクトは通常であればbuttonオブジェクトとなります。

しかし、addEventListnerの第二引数に記載されている関数をよく見ると、アロー関数が用いられています。
アロー関数の場合、呼び出し元オブジェクトを参照するthisを用いる事ができないため、
thisはグローバルオブジェクトを指し示す
ことになります。

問題のコードではグローバルオブジェクトのプロパティとしてdatasetが設定されていないため、
this.datasetは'undefined'となります。
よって、undefinedからプロパティ(buttonNumber)を読み取ろうとしてエラーとなっているという状況でした。

そのため、対処法としてはthisが使えるようにアロー関数表記を書き換える事が必要になります。
解答例では、無名関数に書き換えることによってthisが正しく使えるように修正しています。

const button = document.querySelector('.problem-button')
button.addEventListener('click', function () {
    // ボタンのdata-button-number属性を取得
    const buttonNumber = this.dataset.buttonNumber;
    console.log(buttonNumber)
});

まとめ

今回は、私が実際に直面した一つのコードを通じてthisの挙動についてご紹介しました。
基本的には、thisは呼び出し元のオブジェクトを指し示すということだけを覚えておいて、
エラーに直面した段階で「そういえば例外があったな...」と思い返すというような理解で
問題がないような気がします。

それにしてもどうしてアロー関数でthisが使えないのでしょうか...
個人的にその理由や背景が気になります。

参考文献

【JS】ガチで学びたい人のためのJavaScriptメカニズム

Discussion