「JavaScript の関数内での this」の続きです。先の記事ではクラス内に定義した関数の話を書きましたが、この記事ではオブジェクト内に定義した関数について書きます。
クラス内でもオブジェクト内でも同じかと思っていたのですが、実はアロー関数の場合は this に設定されるものが違ってくるということを知ったので、調べたことをまとめて備忘録として書いておくことにした次第です。
下の画像のように Person オブジェクトをグローバルコンテキストで定義した場合、Visual Studio 2022 のインテリセンスで f2 に設定したアロー関数内の this を見ると this: typeof globalThis となっています。
MDN のドキュメント「globalThis」によると "globalThis はグローバルプロパティで、グローバルオブジェクトと同等であるグローバルな this が格納されています" とのことで、ブラウザでは window になるそうです。実際 Person.f2() を実行するとコンソールには window と出力されました。
MDN のドキュメント「this」に以下の説明があり、上の画像の (1), (2) はその通りの結果になっています。
(1) 「オブジェクトのメソッドとして」のセクション: 関数がオブジェクトのメソッドとして呼び出されるとき、その this にはメソッドが呼び出されたオブジェクトが設定されます。
(2) 「アロー関数」のセクション: アロー関数では、this はそれを囲む構文上のコンテキストの this の値が設定されます。グローバルコードでは、グローバルオブジェクトが設定されます。
アロー関数で this を使うことのメリットについては、MDN のドキュメント「アロー関数式」の「例」のセクションに以下の説明があり、setTimeout を使ったコード例が載っています。
"おそらくアロー関数を使う最大の利点は、 DOM レベルのメソッド(setTimeout() や EventTarget.prototype.addEventListener())で、通常は何らかのクロージャ、call()、apply()、bind() を使用して、関数が適切なスコープで実行されることを確認する必要があることです。"
addEventListener() を使った場合はどういう利点があるか、自分が考えた話なのでイマイチかもしれませんが、以下に例を書きます。
<body>
<button type="button" id="button1">button</button>
</body>
<script>
document.getElementById("button1")
.addEventListener("click", listener);
function listener() {
const Person1 = {
name: "Person1",
f1: function () { console.log(this); },
f2: () => { console.log(this); }
}
Person1.f1(); // Object
Person1.f2(); // <button type="button" id="button1">button</button>
}
</script>
関数がイベントハンドラとして使用された場合 this はリスナーがアタッチされている DOM に設定されることを期待するはずです。上のコード例ではアロー関数の this は期待通りとなりますが、function() { ... } 内の this は Person1 オブジェクトが設定されます。
もう一つ、Promise オブジェクトのメソッド then() の中に設定され、非同期で実行されるコールバックの場合もアロー関数を使う利点があると思います。その例を以下に書きます。
const WeatherData = {
result: "",
setData: function (data) { this.result = data; },
fetchData: function() {
fetch('jsonSample.json')
.then(function (response) {
return response.json();
})
//.then(function (data) {
// this.setData(data[0].name); // this は window
//});
.then(data => {
this.setData(data[0].name); // this は WeatherData
console.log("fetchData:", this);
});
}
}
WeatherData.fetchData();
上のコードで、コメントアウトした方の this には window が設定されるので this.setData(data[0].name); で "Uncaught (in promise) TypeError: this.setData is not a function" というエラーになります。
一方、その下のアロー関数を使った場合、this には WeatherData オブジェクトが設定され、期待通り this.setData(data[0].name); で WeatherData オブジェクトの result プロパティが書き換えられます。
ちなみにですが、クラスの場合はアロー関数内の this は下の画像のようになり、クラス本体の this すなわちクラスのインスタンスが設定されるようです。
これに関しては、MDN のドキュメント「アロー関数式」の「メソッドとしては使用不可」のセクションに以下の説明があります。
"クラスの本体は this コンテキストを持っているので、クラスフィールドのようなアロー関数はクラスの this コンテキストを閉じ、アロー関数の本体の中の this はインスタンス(または静的フィールドの場合はクラス自体)を正しく参照します。しかし、これは関数自身のバインディングではなく、クロージャであるため、 this の値が実行コンテキストによって変わることはありません。"
以下に検証に使ったコードを載せておきます。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
</head>
<body>
<button type="button" id="button1">button</button>
</body>
<script>
// グローバルコンテキストでオブジェクトを定義
const Person = {
name: "Person",
f1: function () { console.log(this); },
f2: () => { console.log(this); }
}
Person.f1(); // Object -------- (1)
Person.f2(); // Window -------- (2)
// addEventListener で設定するリスナでアロー関数を使う
document.getElementById("button1")
.addEventListener("click", listener);
function listener() {
const Person1 = {
name: "Person1",
f1: function () { console.log(this); },
f2: () => { console.log(this); }
}
Person1.f1(); // Object
Person1.f2(); // <button type="button" id="button1">button</button>
}
// 以下は蛇足でクロージャーとクラスの例
// クロージャー
const Person2 = function () {
this.Name = "クロージャー";
this.f1 = function () { console.log(this); }
this.f2 = () => { console.log(this); }
}
let p2 = new Person2();
p2.f1(); // Person2
p2.f2(); // Person2
// クラス
class Example {
Name = "クラス";
f1 = function () { console.log(this); }
f2 = () => { console.log(this); }
}
let e = new Example();
e.f1(); // Example -------- (3)
e.f2(); // Example -------- (4)
// then() の中に設定するコールバックでアロー関数を使う
const WeatherData = {
result: "",
setData: function (data) { this.result = data; },
fetchData: function() {
fetch('jsonSample.json')
.then(function (response) {
return response.json();
})
//.then(function (data) {
// this.setData(data[0].name); // this は Window
//});
.then(data => {
this.setData(data[0].name); // this は WeatherData
console.log("fetchData:", this);
});
},
fetchData2: async function () {
const response = await fetch('jsonSample.json');
const data = await response.json();
this.setData(data[0].name);
console.log("fetchData2:", this);
}
}
WeatherData.fetchData();
WeatherData.fetchData2();
</script>
</html>
上のコードを実行したコンソール出力は以下の通りです。最後の 2 つは button をクリックして listener() を起動して出力させたものです。