【JavaScript】関数の基本と使い方|宣言・式・アロー関数の違い・引数・コールバック・クロージャまで解説

JavaScriptにおいて関数はコードの再利用・整理の基本単位です。「関数の書き方は知っている」という方も、3種類の構文の違いや引数の応用、コールバック・クロージャの仕組みまでを体系的に理解すると、コードの品質が大きく変わります。

この記事では関数の定義方法の比較から始め、引数の柔軟な扱い方、高階関数・コールバック、クロージャ、純粋関数まで実践コードとともに解説します。

スポンサーリンク

関数の定義方法 3パターン

JavaScriptには関数を定義する方法が主に3種類あります。それぞれに特徴があるため、使い分けを理解しましょう。

① 関数宣言

関数宣言
function greet(name) {
  return `こんにちは、${name}さん!`;
}

console.log(greet('田中')); // "こんにちは、田中さん!"
関数宣言はホイスティングされるため、宣言より前の行でも呼び出せます。ファイルの先頭でいきなり呼び出しても動作します。

② 関数式

関数式
const greet = function(name) {
  return `こんにちは、${name}さん!`;
};

console.log(greet('田中')); // "こんにちは、田中さん!"
// ※ 宣言より前に呼ぶと TypeError になる(ホイスティングされない)

③ アロー関数(ES2015〜)

アロー関数
// 基本形
const greet = (name) => {
  return `こんにちは、${name}さん!`;
};

// 引数が1つのときは () を省略可
const double = x => x * 2;

// 式がそのまま戻り値のときは {} と return を省略可(暗黙的 return)
const add = (a, b) => a + b;

console.log(double(5));  // 10
console.log(add(3, 4));  // 7

3種類の主な違いを表にまとめます。

特徴 関数宣言 関数式 アロー関数
ホイスティング あり なし なし
自身の this あり あり なし(外側を参照)
arguments オブジェクト あり あり なし
コンストラクタ利用 不可
主な用途 汎用・トップレベル 変数への代入・条件分岐 コールバック・短い処理
どれを使うべきか:現代のJavaScriptではコールバックや短い関数にアロー関数、モジュールのメイン処理に関数宣言を使うのが一般的です。関数式は条件によって異なる関数を代入したいときに重宝します。

引数の柔軟な扱い方

デフォルト引数

引数が渡されなかった場合や undefined が渡された場合に使われるデフォルト値を設定できます。

デフォルト引数
function greet(name = 'ゲスト', greeting = 'こんにちは') {
  return `${greeting}、${name}さん!`;
}

console.log(greet());              // "こんにちは、ゲストさん!"
console.log(greet('田中'));         // "こんにちは、田中さん!"
console.log(greet('田中', 'おはよう')); // "おはよう、田中さん!"

// undefined を渡してもデフォルトが適用される
console.log(greet(undefined, 'こんばんは')); // "こんばんは、ゲストさん!"

残余引数(…rest)

引数の個数が不定のときは残余引数を使います。arguments オブジェクトとは異なり、本物の配列として扱えます。

残余引数
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3));       // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

// 先頭の引数は通常通り受け取り、残りをまとめる
function log(level, ...messages) {
  console.log(`[${level}]`, messages.join(' '));
}

log('INFO', 'サーバー', '起動完了'); // "[INFO] サーバー 起動完了"

分割代入で引数を受け取る

オブジェクトや配列を引数にするとき、分割代入を使うと呼び出し側が読みやすくなります。

引数の分割代入
// オブジェクトの分割代入(名前付き引数のような使い方)
function createUser({ name, age = 0, role = 'user' }) {
  return { name, age, role };
}

const user = createUser({ name: '田中', age: 30 });
console.log(user); // { name: '田中', age: 30, role: 'user' }

// 配列の分割代入
function getFirstTwo([first, second]) {
  return { first, second };
}

console.log(getFirstTwo([10, 20, 30])); // { first: 10, second: 20 }
注意:分割代入を使った引数は、呼び出し時に undefined を渡すと TypeError になります。function f({ x } = {}) のようにデフォルト値 {} を設定しておくと安全です。

戻り値の基本

return で呼び出し元に値を返します。複数の値を返したいときは配列またはオブジェクトにまとめます。

複数の値を返す
// オブジェクトで返す(名前付きで分かりやすい)
function divide(a, b) {
  if (b === 0) return { result: null, error: 'ゼロ除算エラー' };
  return { result: a / b, error: null };
}

const { result, error } = divide(10, 3);
if (error) {
  console.error(error);
} else {
  console.log(result.toFixed(2)); // "3.33"
}
戻り値と return の詳しい使い方はreturnで呼び出し元に値を返す(戻り値)で解説しています。

高階関数とコールバック関数

JavaScriptでは関数は「第一級オブジェクト」です。変数に代入したり、他の関数に引数として渡したり(コールバック)、戻り値として返すことができます。

「関数を引数に受け取る関数」を高階関数と呼びます。

コールバック関数の基本

コールバック関数
// 関数を引数として受け取る高階関数
function applyTwice(fn, value) {
  return fn(fn(value));
}

const double = x => x * 2;
console.log(applyTwice(double, 3)); // 12(3 → 6 → 12)

// 処理タイミングをコールバックで制御
function fetchData(onSuccess, onError) {
  // ... 非同期処理のイメージ
  const success = true;
  if (success) {
    onSuccess({ id: 1, name: '田中' });
  } else {
    onError(new Error('取得失敗'));
  }
}

fetchData(
  (data) => console.log('成功:', data.name),
  (err)  => console.error('失敗:', err.message)
);

配列メソッドとコールバックの組み合わせ

mapfilterreducesort はすべてコールバックを受け取る高階関数です。

配列メソッド × コールバック
const products = [
  { name: 'りんご',   price: 120, inStock: true  },
  { name: 'バナナ',   price: 80,  inStock: false },
  { name: 'みかん',   price: 100, inStock: true  },
  { name: 'ぶどう',   price: 300, inStock: true  },
];

// 在庫ありの商品だけ抽出(filter)
const available = products.filter(p => p.inStock);

// 価格を10%引きにした名前リストを作成(filter + map)
const salePrices = available
  .map(p => ({ name: p.name, price: Math.floor(p.price * 0.9) }));

console.log(salePrices);
// [{ name: 'りんご', price: 108 }, { name: 'みかん', price: 90 }, ...]

// 価格合計(reduce)
const total = available.reduce((sum, p) => sum + p.price, 0);
console.log(total); // 520

// 価格昇順ソート(sort)
const sorted = [...available].sort((a, b) => a.price - b.price);
console.log(sorted.map(p => p.name)); // ['みかん', 'りんご', 'ぶどう']

関数を返す(関数ファクトリ)

関数を返す例
// 特定の倍率をかける関数を生成するファクトリ
function makeMultiplier(factor) {
  return (value) => value * factor;
}

const triple = makeMultiplier(3);
const quadruple = makeMultiplier(4);

console.log(triple(5));    // 15
console.log(quadruple(5)); // 20

// よく使うバリデーターをファクトリで生成
function makeRangeChecker(min, max) {
  return (value) => value >= min && value <= max;
}

const isAdult      = makeRangeChecker(18, 120);
const isValidScore = makeRangeChecker(0, 100);

console.log(isAdult(25));       // true
console.log(isValidScore(105)); // false

クロージャ(Closure)

クロージャとは、関数が定義されたスコープの変数を「閉じ込めて」記憶する仕組みです。外側の関数が終了した後も、内側の関数はその変数にアクセスできます。

クロージャの基本:カウンター
function createCounter(initialValue = 0) {
  let count = initialValue; // 外側の関数のローカル変数

  return {
    increment() { return ++count; },
    decrement() { return --count; },
    reset()     { count = initialValue; return count; },
    getCount()  { return count; },
  };
}

const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.reset());     // 10

// count は外から直接アクセス・変更できない(プライベート変数)
console.log(typeof count); // "undefined"
クロージャの実用例:メモ化(memoize)
// 同じ引数で再度呼ばれたときにキャッシュから返す
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key); // キャッシュヒット
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  console.log(`計算中: ${n}`);
  return n * n;
});

console.log(expensiveCalc(5)); // "計算中: 5" → 25
console.log(expensiveCalc(5)); // キャッシュヒット → 25(再計算なし)
console.log(expensiveCalc(6)); // "計算中: 6" → 36
クロージャが活躍する場面:カウンターやタイマー、イベントハンドラの状態保持、部分適用(Partial Application)、プライベート変数の模擬など、状態を持ちながら外部からのアクセスを制限したいケースで使います。

純粋関数と副作用

純粋関数とは、次の2つの条件を満たす関数です。

  1. 同じ引数を渡すと必ず同じ値を返す
  2. 外部の状態を変更する副作用がない
NG: 副作用あり(非純粋関数)
let total = 0;

// 外部変数を変更している → 副作用あり
function addToTotal(value) {
  total += value; // 副作用:外部の状態を変えている
  return total;
}

console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 (同じ引数なのに結果が変わる)
OK: 純粋関数
// 外部状態に依存せず、引数だけで結果が決まる
function add(a, b) {
  return a + b;
}

console.log(add(3, 5)); // 8
console.log(add(3, 5)); // 8 (何度呼んでも同じ)

// 配列を変更せずに新しい配列を返す(純粋関数)
function appendItem(array, item) {
  return [...array, item]; // 元の配列を変えない
}

const original = [1, 2, 3];
const newArr = appendItem(original, 4);
console.log(original); // [1, 2, 3](変更なし)
console.log(newArr);   // [1, 2, 3, 4]
純粋関数のメリット:テストが書きやすい(モックが不要)、再利用しやすい、バグが追いやすい。すべての関数を純粋にはできませんが、副作用を持つ処理を関数の端に追い出す設計(副作用の局所化)が保守性を高めます。

即時実行関数(IIFE)

関数を定義と同時に実行するパターンをIIFE(Immediately Invoked Function Expression)と呼びます。スコープを汚染せずに初期化処理を実行するときに使います。詳しくはvoid演算子とIIFEの使い方で解説しています。

IIFE の基本形
// アロー関数 IIFE(現代的な書き方)
const result = (() => {
  const x = 10;
  const y = 20;
  return x + y;
})();

console.log(result); // 30
// x, y はこのスコープの外からアクセスできない

よくある質問(FAQ)

Q関数宣言と関数式はどちらを使えばいいですか?
Aファイルのトップレベルに書く汎用的な関数は関数宣言が読みやすいです。変数に代入したり条件によって処理を切り替えたりするケースは関数式(またはアロー関数)を使います。一貫性があれば混在しても問題ありません。
Qアロー関数と通常の関数の最大の違いは何ですか?
Athis の扱いが異なります。通常の関数は呼び出し方によって this が変わりますが、アロー関数は定義されたスコープの this をそのまま使います(レキシカル this)。そのためクラスのメソッド内でコールバックを書くときにアロー関数を使うと this のバインディング問題を避けられます。
Q関数の引数の個数は固定しないといけませんか?
Aいいえ。残余引数(...argsを使うと可変個の引数を配列として受け取れます。またデフォルト引数で省略可能にもできます。引数が多くなってきたらオブジェクトの分割代入でオプションオブジェクトパターンにすると呼び出し元が読みやすくなります。
Qクロージャはどんなときに使いますか?
A主に「状態を外部から隠して持ち続けたい」ときに使います。カウンター・トグルフラグ・メモ化キャッシュ・イベントハンドラの状態保持などが典型例です。クラスのプライベートフィールド(#)が使えるモダンな環境では、そちらを使うケースも増えています。
Q純粋関数にできない処理はどう扱えばいいですか?
ADOM 操作・API 呼び出し・ログ出力などは本質的に副作用です。「純粋な計算ロジック」と「副作用を持つI/O処理」を別々の関数に分けて副作用を関数の端に追い出す設計が有効です。計算部分だけを純粋関数にすることでテストが書きやすくなります。

まとめ

JavaScriptの関数に関する要点を整理します。

トピック ポイント
定義方法 関数宣言(ホイスティングあり)・関数式・アロー関数(thisレキシカル)
引数 デフォルト引数・残余引数(...rest)・分割代入
戻り値 return 文・アロー関数の暗黙的 return・オブジェクト/配列で複数返却
高階関数 関数を引数・戻り値として使う。map/filter/reduce の基盤
クロージャ 外側スコープの変数を記憶。状態の隠蔽やメモ化に活用
純粋関数 同じ入力 → 同じ出力・副作用なし。テスタブルなコードの基礎

関数の基礎を押さえたら、return で値を返す方法即時実行関数(IIFE)の詳細もあわせて確認すると理解がより深まります。