JavaScriptにおける関数宣言とアロー関数の使い分け
JavaScriptで関数を定義する際、functionキーワードによる関数宣言とconstを使ったアロー関数の2つの方法があります。本記事では、それぞれの特徴と使い分けについて解説します。
基本的な書き方の比較
function宣言
export function getPrismaClient(): PrismaClient {
if (!prisma) {
prisma = new PrismaClient();
}
return prisma;
}
アロー関数
export const getPrismaClient = (): PrismaClient => {
if (!prisma) {
prisma = new PrismaClient();
}
return prisma;
};
主な違いの比較表
| 特徴 | function宣言 | アロー関数 (const) |
|---|---|---|
| ホイスティング | あり(宣言前でも呼び出し可能) | なし(宣言後のみ呼び出し可能) |
| thisの扱い | 呼び出し元に依存(実行時に決定) | 自身のthisを持たず、外側のスコープのthisを参照 |
| constructorとして使用 | 可能 | 不可能 |
| 型定義の書き方 | シンプル | やや冗長 |
| 可読性 | 高い(従来の関数らしい) | モダンでコンパクト |
| argumentsオブジェクト | 使用可能 | 使用不可(rest parametersを使う) |
ホイスティングの違い
// function宣言:ファイルのどこからでも呼び出せる
console.log(add(2, 3)); // ✅ 5
export function add(a: number, b: number): number {
return a + b;
}
// アロー関数:宣言前には呼び出せない
console.log(multiply(2, 3)); // ❌ ReferenceError
export const multiply = (a: number, b: number): number => {
return a * b;
};
thisの挙動の違い
基本的な動作例
class Counter {
count = 0;
// function宣言:thisは呼び出し元に依存
incrementFunction() {
this.count++;
}
// アロー関数:自身のthisを持たず、外側のスコープ(Counterクラス)のthisを参照
incrementArrow = () => {
this.count++;
};
}
// ✅ Promiseベースのdelay関数を定義
const delay = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
(async () => {
const counter = new Counter();
// ---- 直接呼び出し ----
counter.incrementFunction();
console.log("1:Counter count:", counter.count); // 1
counter.incrementArrow();
console.log("2:Counter count:", counter.count); // 2
// ---- コールバックとして渡す ----
// function版: thisがundefinedになる
await delay(100);
setTimeout(counter.incrementFunction, 0);
await delay(50); // 少し待ってログ出力
console.log("3:Counter count:", counter.count); // 2
// arrow版: 外側のスコープ(Counter)のthisを参照するため正常に動作
await delay(100);
setTimeout(counter.incrementArrow, 0);
await delay(50);
console.log("4:Counter count:", counter.count); // 3
})();
function宣言だとコールバックでthisが失われてしまう。
この理由を理解するには、「関数参照」と「メソッド呼び出し」の違いを知る必要があります。
補足:関数とメソッドの違い、そして()の利用
関数 vs メソッド
JavaScriptでは、同じ関数でも呼び出し方によって挙動が変わります。
function greet() {
console.log(`Hi, I'm ${this?.name}`);
}
const user = {
name: "Alice",
greet // プロパティとして登録
};
greet(); // thisはundefined(関数として呼び出し)
user.greet(); // thisはuser(メソッドとして呼び出し)
| 用語 | 定義 | thisの扱い | 呼び出し方 |
|---|---|---|---|
| 関数 | オブジェクトに属さない独立した処理 | 呼び出し方によって決まる(多くの場合undefined) |
fn() |
| メソッド | オブジェクトのプロパティとして登録された関数 | 呼び出し元のオブジェクトがthisになる |
obj.fn() |
()は「コンテキストの結合」
関数呼び出しの()は、単に「関数を実行する」だけでなく、**「実行コンテキスト(this)を結合して実行する」**という役割を持ちます。
const user = {
name: "Bob",
greet() {
console.log(this.name);
}
};
// メソッド呼び出し:thisが結合される
user.greet(); // "Bob" ← user.greet() の()が「thisをuserに結合」
// 関数参照を取り出す:thisの情報が失われる
const fn = user.greet;
fn(); // undefined ← fn()の()は「thisを結合できない」
コールバック関数でthisが失われる理由
setTimeout(counter.incrementFunction, 100);
このコードで何が起きているか、ステップごとに見てみましょう:
-
counter.incrementFunctionを評価 → 関数オブジェクトだけを取得(counterとの結びつきは失われる) - その関数を
setTimeoutに渡す - 100ms後、
setTimeout内部でcallback()として呼び出される - この時点で呼び出し元オブジェクトがないため、
thisはundefined
// setTimeoutの内部イメージ
function setTimeout(callback, delay) {
// 指定時間後に...
callback(); // ← 呼び出し元(レシーバ)がない!
}
解決方法
1. アロー関数でラップする
setTimeout(() => counter.incrementFunction(), 100);
// ラップ内で counter.incrementFunction() と呼ぶため、
// ()がthisをcounterに結合してくれる
2. bindで明示的に束縛する
setTimeout(counter.incrementFunction.bind(counter), 100);
// bindで「この関数のthisは常にcounter」と固定
3. 最初からアロー関数で定義する
class Counter {
count = 0;
incrementArrow = () => {
this.count++;
};
}
// アロー関数は自身のthisを持たないため、
// thisを参照すると外側のスコープ(この例ではCounterクラス)のthisを参照します
再度振り返り:なぜfunction宣言だけthisが失われるのか
// どちらも「関数参照」を渡している
setTimeout(counter.incrementFunction, 0); // function版
setTimeout(counter.incrementArrow, 0); // arrow版
どちらもcounter.incrementFunctionやcounter.incrementArrowという関数オブジェクトへの参照をsetTimeoutに渡しています。この時点でメソッドとしての呼び出しコンテキスト(counter)は失われます。
では、なぜ挙動が違うのか?違いは関数の定義時点にあります。
1. function宣言:thisは「実行時」に決まる
incrementFunction: function() {
this.count++; // このthisは「実行時」に決定される
}
2. アロー関数:「外側のスコープのthis」を参照する
incrementArrow: () => {
this.count++; // アロー関数は自身のthisを持たず、外側のスコープ(Counter)のthisを参照
}
コードで確認
class Counter {
count = 0;
incrementFunction = function() {
console.log("function内のthis:", this);
this.count++;
};
incrementArrow = () => {
console.log("arrow内のthis:", this);
this.count++;
};
}
const counter = new Counter();
// 直接呼び出し(メソッドとして)
counter.incrementFunction(); // this = counter ✓
counter.incrementArrow(); // this = counter ✓
// 関数参照として取り出す
const funcRef = counter.incrementFunction;
const arrowRef = counter.incrementArrow;
// 関数として呼び出し
funcRef(); // this = undefined ✗
arrowRef(); // this = counter ✓(外側のスコープのthisを参照)
まとめ:なぜfunction宣言だけthisが失われるか
-
両方とも関数参照になる -
setTimeoutに渡す時点で、どちらも「関数オブジェクトの参照」として渡される -
function宣言 - thisは実行時に決まるため、
callback()として呼ばれた時に呼び出し元がなくundefinedになる -
アロー関数 - 自身のthisを持たず、定義された場所の外側のスコープの
thisを参照するため、関数参照として渡されても正しく動作する
つまり、関数参照として渡されるのは同じですが、thisの扱い方が異なるため、結果が変わるのです。
使い分けのガイドライン
function宣言を使うべきケース
- トップレベルのユーティリティ関数
// ✅ 推奨
export function formatDate(date: Date): string {
return date.toISOString();
}
export async function fetchUserData(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
- 名前付きエクスポート関数
// ✅ シンプルで読みやすい
export function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
- ホイスティングを活用したい場合
// ✅ 宣言前でも呼び出せる
const result = processData(data);
function processData(data: string): string {
return data.trim().toLowerCase();
}
アロー関数を使うべきケース
- コールバック関数
// ✅ 推奨
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
- 外側のスコープのthisを「参照」したい場合
class DataProcessor {
private prefix = 'data_';
// ✅ アロー関数は自身のthisを持たず、外側のスコープ(DataProcessor)のthisを参照する
formatItems = (items: string[]) => {
return items.map(item => `${this.prefix}${item}`);
};
// イベントハンドラなどで有用
handleClick = () => {
console.log(this.prefix); // 常にインスタンスのthisを参照
};
}
- 高階関数を返す場合
// ✅ クロージャーを活用
const createMultiplier = (factor: number) => {
return (value: number) => value * factor;
};
const double = createMultiplier(2);
const triple = createMultiplier(3);
- 関数を変数として扱う場合
// ✅ 条件によって関数を切り替える
const logger = process.env.NODE_ENV === 'development'
? (msg: string) => console.log(`[DEV] ${msg}`)
: (msg: string) => console.log(msg);
実践例:Prismaクライアントのシングルトン
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
// ✅ function宣言が適している
// - トップレベルのユーティリティ関数
// - thisを使わない
// - シンプルで読みやすい
export function getPrismaClient(): PrismaClient {
if (!prisma) {
prisma = new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
}
return prisma;
}
export async function disconnectPrisma(): Promise<void> {
if (prisma) {
await prisma.$disconnect();
}
}
この例では、以下の理由からfunction宣言が適しています:
- ユーティリティ関数として利用される
-
thisを使用しない - 型定義がシンプルで読みやすい
- モジュールレベルの関数として自然
実用的な使い分けパターン
// ✅ トップレベル関数:function宣言
export function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// ✅ 配列操作:アロー関数
const users = await prisma.user.findMany();
const emails = users.map(user => user.email);
const activeUsers = users.filter(user => user.isActive);
// ✅ クラスメソッド(thisを使う):アロー関数
class UserService {
private apiUrl = 'https://api.example.com';
fetchUser = async (id: string) => {
const response = await fetch(`${this.apiUrl}/users/${id}`);
return response.json();
};
}
// ✅ 非同期ユーティリティ:function宣言
export async function fetchWithRetry(
url: string,
maxRetries = 3
): Promise<Response> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === maxRetries - 1) throw error;
}
}
throw new Error('Max retries reached');
}
まとめ
| 用途 | 推奨スタイル | 理由 |
|---|---|---|
| トップレベル関数 |
function宣言 |
シンプルで読みやすい |
| コールバック | アロー関数 | 簡潔に書ける |
| クラスメソッド(thisを使う) | ケースバイケース | ※補足を参照 |
| 配列メソッド | アロー関数 | 短く書ける |
| ユーティリティ関数 |
function宣言 |
型定義が明確 |
| イベントハンドラ | アロー関数 | thisの問題を回避 |
基本的には、トップレベルの関数はfunction宣言、コールバックやthisを扱う場合はアロー関数という使い分けをすると、コードの可読性と保守性が向上します。
ただし、最終的にはチームのコーディング規約に従うことが重要です。一貫性のあるコードスタイルを保つことで、チーム全体の開発効率が向上します。
よくある間違い
❌ 「アロー関数はthisを固定する」
✅ 「アロー関数は自身のthisを持たず、外側のthisを参照する」
補足
メモリ効率とthisの参照のトレードオフを考慮する
クラスメソッドを定義する際は、メモリ効率とthisの安全性のバランスを考慮する必要があります。
上手くthisが取れないなどのバグを考慮するならアロー関数のが良いと思います。
アロー関数とfunctionメソッドの内部的な違い
| 定義方法 | 保存場所 | 関数オブジェクトの数 |
|---|---|---|
method() {} |
プロトタイプ(共有) | 1個(全インスタンスで共有) |
method = () => {} |
各インスタンス | N個(インスタンス数分) |
なぜこの差が生まれるのか?
class Example {
// function版: プロトタイプに1つだけ存在
method1() { }
// アロー関数版: 各インスタンスが独自の関数を持つ
method2 = () => { }
}
// 内部的なイメージ
Example.prototype.method1 = function() { } // ← 全インスタンスで共有
// インスタンス生成時に毎回以下が実行される
const instance = new Example();
instance.method2 = () => { } // ← インスタンスごとに新しい関数が生成
パフォーマンスが問題になるケース
アロー関数はインスタンス毎に作成されるため、大量のインスタンスを生成するとメモリを圧迫します。
ただし、以下のような大規模なケースを除いて、アロー関数のメモリオーバーヘッドは通常問題になりません:
- ゲームエンジンで数万~のエンティティを扱う
- リアルタイムデータ処理で大量のオブジェクトを生成
- メモリ制限の厳しい環境(組み込みやIoT)
一般的なWebアプリケーションでは気にする必要はありませんが、大量のインスタンスを扱う場合やパフォーマンスを意識する場合は考慮する価値があります。
具体例:RPGゲームのキャラクタークラス
// 🎮 100万体のモンスターが登場するRPGゲームを想定
// ❌ 非効率:アロー関数をクラスプロパティとして定義
class Monster_Inefficient {
name: string;
hp = 100;
// 各インスタンスごとに関数が生成される
attack = () => {
console.log(`${this.name} attacks!`);
return Math.floor(Math.random() * 10) + 1;
};
takeDamage = (damage: number) => {
this.hp -= damage;
console.log(`${this.name} takes ${damage} damage!`);
};
}
// ✅ 効率的:メソッドをプロトタイプに定義
class Monster_Efficient {
name: string;
hp = 100;
constructor(name: string) {
this.name = name;
}
// プロトタイプに1つだけ存在(全インスタンスで共有)
attack() {
console.log(`${this.name} attacks!`);
return Math.floor(Math.random() * 10) + 1;
}
takeDamage(damage: number) {
this.hp -= damage;
console.log(`${this.name} takes ${damage} damage!`);
}
}
// 実際のメモリ使用量の比較
// 100万個のインスタンスをそれぞれ作ってみる
const used1 = process.memoryUsage().heapUsed;
const arrows = Array.from({length:1000000},()=> new Monster_Inefficient());
const used2 = process.memoryUsage().heapUsed;
const functions = Array.from({length:1000000},()=> new Monster_Efficient("function"));
const used3 = process.memoryUsage().heapUsed;
console.log(`Arrow instances: ${((used2 - used1) / 1024 / 1024).toFixed(2)} MB`);
console.log(`Function instances: ${((used3 - used2) / 1024 / 1024).toFixed(2)} MB`);
// 結果例:
// Arrow instances: 212.90 MB
// Function instances: 46.24 MB
メモリ使用量の差は約5倍になりました。大規模なシステムでは、この差が積み重なって大きな影響を与える可能性があります。
じゃあクラスメソッドはメモリ効率意識してfunctionをつかうべきか?
結論: ケースバイケース
functionメソッドを使う場合、利用側で必ず()をつけて呼び出す(コンテキストの結合)ことを統一できるなら良い選択です。
class UserService {
users: User[] = [];
// functionメソッド
getUser(id: string) {
return this.users.find(u => u.id === id);
}
}
const service = new UserService();
// ✅ 直接呼び出し: thisが正しく結合される
service.getUser('123');
// ❌ 関数参照として渡す: thisが失われる
const finder = service.getUser;
finder('123'); // エラー: thisがundefined
// ✅ 明示的にthisを結合
setTimeout(() => service.getUser('123'), 1000);
ドメイン駆動設計(DDD)の観点
ドメインオブジェクトのメソッドは「そのエンティティ自身の責務を表す」ため、基本的にthisを内部で使う前提です(ドメイン境界を揃えておくべき)。
そのため、アプリケーション層などでは「文脈ごと呼び出す = ()を使う」設計を統一することが重要になります。
補足まとめ
クラスメソッドを定義する際は、メモリ効率とthisの安全性のバランスを考慮して判断しましょう。
最終的には、プロジェクトの規模・要件・チームの方針に応じて選択することが重要です。
Discussion