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 オブジェクト |
あり | あり | なし |
| コンストラクタ利用 | 可 | 可 | 不可 |
| 主な用途 | 汎用・トップレベル | 変数への代入・条件分岐 | コールバック・短い処理 |
引数の柔軟な扱い方
デフォルト引数
引数が渡されなかった場合や 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)
);
配列メソッドとコールバックの組み合わせ
map・filter・reduce・sort はすべてコールバックを受け取る高階関数です。
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"
// 同じ引数で再度呼ばれたときにキャッシュから返す
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
純粋関数と副作用
純粋関数とは、次の2つの条件を満たす関数です。
- 同じ引数を渡すと必ず同じ値を返す
- 外部の状態を変更する副作用がない
let total = 0;
// 外部変数を変更している → 副作用あり
function addToTotal(value) {
total += value; // 副作用:外部の状態を変えている
return total;
}
console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 (同じ引数なのに結果が変わる)
// 外部状態に依存せず、引数だけで結果が決まる
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(現代的な書き方)
const result = (() => {
const x = 10;
const y = 20;
return x + y;
})();
console.log(result); // 30
// x, y はこのスコープの外からアクセスできない
よくある質問(FAQ)
this の扱いが異なります。通常の関数は呼び出し方によって this が変わりますが、アロー関数は定義されたスコープの this をそのまま使います(レキシカル this)。そのためクラスのメソッド内でコールバックを書くときにアロー関数を使うと this のバインディング問題を避けられます。...args)を使うと可変個の引数を配列として受け取れます。またデフォルト引数で省略可能にもできます。引数が多くなってきたらオブジェクトの分割代入でオプションオブジェクトパターンにすると呼び出し元が読みやすくなります。#)が使えるモダンな環境では、そちらを使うケースも増えています。まとめ
JavaScriptの関数に関する要点を整理します。
| トピック | ポイント |
|---|---|
| 定義方法 | 関数宣言(ホイスティングあり)・関数式・アロー関数(thisレキシカル) |
| 引数 | デフォルト引数・残余引数(...rest)・分割代入 |
| 戻り値 | return 文・アロー関数の暗黙的 return・オブジェクト/配列で複数返却 |
| 高階関数 | 関数を引数・戻り値として使う。map/filter/reduce の基盤 |
| クロージャ | 外側スコープの変数を記憶。状態の隠蔽やメモ化に活用 |
| 純粋関数 | 同じ入力 → 同じ出力・副作用なし。テスタブルなコードの基礎 |
関数の基礎を押さえたら、return で値を返す方法や即時実行関数(IIFE)の詳細もあわせて確認すると理解がより深まります。