「asa1984 Advent Calendar 2024」10日目の記事です。
JavaScript の関数には
function f() {}
で宣言する通常の関数const f = () => {}
で宣言するアロー関数式
の2種類があります。これらの何が異なり、結局どちらを使えばいいのか?という話題は何度も繰り返されてきましたが、未だ明確な合意は得られていません。
本稿ではその性質の違いを論じ、「vs」と題しているからにはその結論を導き出します。
見た目が違うだけ。好きに使え(終)
......
「やあ、僕のことを忘れてないかい?」
ディ、This Man だァ〜〜〜!!!!!!
「this が肝心なのサ」
通常の関数とアロー関数式の違いを調べたことがある人なら大抵知っていると思いますが、アロー関数式は this
を持ちません。だから何だよ、という反応はごもっともで、このような明確な差異がありながらも、通常の関数で this
を使う機会はほぼゼロなので、あまり議論に上がりません。
しかし、この性質の違いが大きな影響をもたらす実用上の場面が1つあります。それは、クラスのメソッド内でクロージャーを定義する時です。
「This Man は僕一人で十分サ」
this
に対する挙動の違いが議論に上がらないのは、そもそも JS でクラスを使う機会があまり多くないからです。特に、フロントエンドのアプリケーションを書いているだけなら一度も触ったことがなくても不思議ではありません。しかし、クラスが必要になるような複雑なライブラリを作ったり、設計の手段としてクラスを使い始めたりすると高確率でこの問題に遭遇します。
次のようなクラスの定義があったとします。
class ThisMan {
name: string;
constructor() {
this.name = "This Man";
}
greet() {
console.log(`Hi! I'm ${this.name}!`);
}
}
greet()
メソッドは this
を介して name
フィールドにアクセスし、このように元気に自己紹介することができます。
const thisMan = new ThisMan();
thisMan.greet();
// Hi! I'm This Man!
ではちょっとだけコードを変更します。greet()
内で getFirstName()
というクロージャーを定義し、その中で this.name
にアクセスしてみます。
class ThisMan {
name: string;
constructor() {
this.name = "This Man";
}
greet() {
function getFirstName() {
return this.name.split(" ")[0];
}
console.log(`Hi! I'm ${getFirstName()}!`);
}
}
const thisMan = new ThisMan();
thisMan.greet();
// 8 | greet() {
// 9 | // const getFirstName = () => {
// 10 | // return this.name.split(" ")[0];
// 11 | // };
// 12 | function getFirstName() {
// 13 | return this.name.split(" ")[0];
// ^
// TypeError: undefined is not an object (evaluating 'this.name')
エラーになってしまいました。
function
はそれ自身が this
を持つので、クラスの this
を参照することができないのです。なので、メソッドの中で定義されたクロージャーからクラスの this
にアクセスするには、this
を持たないアロー関数式を利用する必要があります。
class ThisMan {
name: string;
constructor() {
this.name = "This Man";
}
greet() {
const getFirstName = () => {
return this.name.split(" ")[0];
};
console.log(`Hi! I'm ${getFirstName()}!`);
}
}
const thisMan = new ThisMan();
thisMan.greet();
// Hi! I'm This!
上手く呼び出せました。
「やあ!僕は This サ!」
「ジェネレーターとはズッ友なのサ」
ジェネレーターは関数とよく似た構文を持ちますが、ジェネレーター版アロー関数式は存在しないため、クラスの this
を直接参照できません。
class ThisMan {
name: string;
constructor() {
this.name = "This Man";
}
greet() {
function* nameChars() {
for (const char of this.name) {
yield* this.name;
}
}
for (const char of nameChars()) {
console.log(char);
}
}
}
const thisMan = new ThisMan();
thisMan.greet();
// 5 | this.name = "This Man";
// 6 | }
// 7 |
// 8 | greet() {
// 9 | function* nameChars() {
// 10 | for (const char of this.name) {
// ^
// TypeError: undefined is not an object (evaluating 'this.name')
アロー関数式がないならもうダメだ…
…ということはなく、bind()
を使ってクラスの this
を注入することでこの問題を解決できます。
class ThisMan {
name: string;
constructor() {
this.name = "This Man";
}
greet() {
function* nameChars() {
for (const char of this.name) {
yield* char;
}
}
for (const char of nameChars.bind(this)()) {
console.log(char);
}
}
}
const thisMan = new ThisMan();
thisMan.greet();
// T
// h
// i
// s
//
// M
// a
// n
通常の関数とジェネレーターは bind()
というメソッドを持っており、これを使って任意の値を this
として注入できます。
class ThisMan {
name: string;
constructor() {
this.name = "This Man";
}
greet() {
function* nameChars() {
for (const char of this.name) {
yield* char;
}
}
for (const char of nameChars.bind({ name: "That Woman" })()) {
console.log(char);
}
}
}
const thisMan = new ThisMan();
thisMan.greet();
// T
// h
// a
// t
//
// W
// o
// m
// a
// n
他の bind()
の実用例としては、Next.js の Server Actions があります。
Data Fetching: Server Actions and Mutations | Next.js
他にもホイスティングの挙動などの違いがありますが、まあこれくらいでいいでしょう(疲れた)
正直、これでもどちらかを選択する決め手にはならないのですが、それはそれとしてアロー関数式に統一すべきであると筆者は考えています。
基本的にソフトウェア開発における制約は強い方が好ましく、プログラマーによる意思決定の余地は少なければ少ないほどいいです。通常の関数は this
の挙動を考慮する必要があったり、bind()
みたいなメソッドがくっついていたりしますが、これらの特性を利用する機会はほとんどありません。ならばそういった余計なモノがついていないアロー関数式でよくないか?というのが筆者の考えです。その上で function
が使われている箇所があれば、それは通常の関数の挙動が必要な特殊な処理なのだとすぐ分かります。
「クロージャーさえ注意すればいいんだから普通に function
でよくない?」という反論もあるとは思いますが、わざわざクロージャーの時だけアロー関数式を使うみたいな切り替えをするくらいなら最初から全部アロー関数式でいいんじゃないっすかね。
「でも海外のコードは大体 function
で書いてあるよ?」とか思った奴は大和魂を見せろ。
Linter を設定しろ
通常の関数またはアロー関数式のどちらかに宣言方式を統一する Linter ルールを適用しましょう。
ちゃぶ台を返すようなことを言いますが、本質的に問題なのは通常の関数とアロー関数式の性質の違いではなく、どちらかを選択する余地があることです。コードスタイルのブレが発生するだけでなく、開発者の思考のリソースを無駄遣いします。こういった「選択の余地」は Linter を用いてきちんと撲滅しましょう。
〜あなたの夢の中〜
「ねえ、ちゃんと Linter 設定してる?本当にしてるの?ねえねえ…」