Skip to content

Instantly share code, notes, and snippets.

@asa1984
Last active December 12, 2024 10:07
Show Gist options
  • Save asa1984/35bfb00dec2e77bf5f94b0b6592e564b to your computer and use it in GitHub Desktop.
Save asa1984/35bfb00dec2e77bf5f94b0b6592e564b to your computer and use it in GitHub Desktop.
通常の関数 vs アロー関数式 / asa1984 Advent Calendar 2024 10日目

通常の関数 vs アロー関数式

「asa1984 Advent Calendar 2024」10日目の記事です。

JavaScript の関数には

  • function f() {} で宣言する通常の関数
  • const f = () => {} で宣言するアロー関数式

の2種類があります。これらの何が異なり、結局どちらを使えばいいのか?という話題は何度も繰り返されてきましたが、未だ明確な合意は得られていません。

本稿ではその性質の違いを論じ、「vs」と題しているからにはその結論を導き出します。

結論

見た目が違うだけ。好きに使え(終)





......





This Man

「やあ、僕のことを忘れてないかい?」

ディ、This Man だァ〜〜〜!!!!!!

"this"

This Man

「this が肝心なのサ」


通常の関数とアロー関数式の違いを調べたことがある人なら大抵知っていると思いますが、アロー関数式は this を持ちません。だから何だよ、という反応はごもっともで、このような明確な差異がありながらも、通常の関数で this を使う機会はほぼゼロなので、あまり議論に上がりません。

しかし、この性質の違いが大きな影響をもたらす実用上の場面が1つあります。それは、クラスのメソッド内でクロージャーを定義する時です。

class と this

This Man

「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 Man

「やあ!僕は This サ!」

ジェネレーター と this

This Man

「ジェネレーターとはズッ友なのサ」


ジェネレーターは関数とよく似た構文を持ちますが、ジェネレーター版アロー関数式は存在しないため、クラスの 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 を用いてきちんと撲滅しましょう。


〜あなたの夢の中〜

This Man

「ねえ、ちゃんと Linter 設定してる?本当にしてるの?ねえねえ…」

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment