JavaScriptの基礎 - 関数宣言と関数式
概要
JavaScriptにおける関数は、ファーストクラスオブジェクト (First-class Object) として扱われる。
変数への代入、他の関数への引数として渡すこと、関数の戻り値として返すことがすべて可能であり、これがJavaScriptの関数型プログラミングの基盤となっている。
関数を定義する方法として、主に関数宣言 (Function Declaration) と関数式 (Function Expression) の2種類がある。
この2つは構文だけでなく、ホイスティング (巻き上げ) の挙動において大きく異なる。
関数宣言は、function キーワードを文の先頭に記述する形式であり、スコープの先頭に関数全体が巻き上げられるため、宣言より前に呼び出すことができる。
一方、関数式は変数に無名関数または名前付き関数を代入する形式であり、変数のホイスティングルールに従う。
ES2015 (ES6) で導入されたデフォルト引数と残余引数も、関数定義の重要な機能である。
デフォルト引数を使用すると、引数が渡されなかった場合の初期値を関数の定義時に指定できる。
残余引数は、可変長の引数を真の配列として受け取る仕組みであり、ES2015以前の arguments オブジェクトに比べて扱いやすい。
Reactをはじめとする現代的なフレームワークでは、コンポーネントや高階関数、イベントハンドラとして関数を頻繁に使用する。
関数宣言と関数式の特性を正確に理解することは、保守性の高いコードを書く上で不可欠である。
関数宣言
基本構文
関数宣言は、function キーワードに続けて関数名を記述する形式である。
関数名は必須であり、省略することはできない。
function <関数名>(<引数1>, <引数2>) {
// 処理
return <戻り値>;
}
使用例を以下に示す。
function add(a, b) {
return a + b;
}
function greet(name) {
return "こんにちは、" + name + "さん";
}
console.log(add(3, 5)); // 8
console.log(greet("Alice")); // "こんにちは、Aliceさん"
引数は複数指定できる。
引数が無い場合は空の括弧 () を記述する。
// 引数無し
function getCurrentTime() {
return new Date().toLocaleTimeString();
}
// 複数の引数
function createFullName(firstName, lastName, separator) {
return firstName + separator + lastName;
}
console.log(getCurrentTime());
console.log(createFullName("太郎", "山田", " ")); // "太郎 山田"
戻り値
return文は、関数の実行を終了して呼び出し元に値を返す。
return文を記述しない場合 または 値を指定しない return; のみを記述した場合、関数は undefined を返す。
function noReturn() {
const x = 1 + 2;
// return文なし
}
function emptyReturn() {
return; // 値なし
}
console.log(noReturn()); // undefined
console.log(emptyReturn()); // undefined
早期リターン (Early Return) を使用すると、条件に応じて関数を早期に終了させることができる。
この手法は、ネストを浅く保ち可読性を向上させる際に有効である。
function divide(a, b) {
if (b === 0) {
return null; // ゼロ除算の場合は早期リターン
}
return a / b;
}
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // null
ホイスティング
関数宣言は、そのスコープ (関数スコープ または グローバルスコープ) の先頭に関数全体が巻き上げられる。
そのため、関数宣言より前のソースコードから呼び出すことができる。
// 宣言より前に呼び出しても正常に動作する
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}
上記の例では、JavaScriptエンジンにより内部的に以下に示すように処理される。
// JavaScriptエンジンが内部的に処理するイメージ
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
var によるホイスティングとの対比を以下に示す。
var 宣言はホイスティング時に undefined で初期化されるのに対し、関数宣言は関数本体ごと巻き上げられる。
console.log(typeof myFunc); // "function" (関数宣言: 関数本体が巻き上げられる)
console.log(typeof myVar); // "undefined" (var: undefinedで初期化)
function myFunc() {}
var myVar = "hello";
関数式
基本構文
関数式は、変数に関数を代入する形式である。
代入する関数は、名前を持たない無名関数式と名前を持つ名前付き関数式の2種類がある。
無名関数式の構文を以下に示す。
// 無名関数式
const <変数名> = function(<引数1>, <引数2>) {
return <戻り値>;
};
名前付き関数式の構文を以下に示す。
// 名前付き関数式
const <変数名> = function <関数名>(<引数1>, <引数2>) {
return <戻り値>;
};
使用例を以下に示す。
// 無名関数式
const multiply = function(a, b) {
return a * b;
};
// 名前付き関数式
const factorial = function calcFactorial(n) {
if (n <= 1) return 1;
return n * calcFactorial(n - 1); // 関数名で再帰呼び出し
};
console.log(multiply(4, 5)); // 20
console.log(factorial(5)); // 120
関数式は変数に代入されるため、const または let で宣言することが推奨される。
再代入の必要がない場合は const を使用する。
ホイスティングの違い
関数式のホイスティング挙動は、代入先の変数の宣言キーワードに依存する。
const または let で宣言した関数式は、Temporal Dead Zone (TDZ) の影響を受ける。
宣言前にアクセスすると ReferenceError が発生する。
// const / let での宣言: TDZによりReferenceError
console.log(greet("Alice")); // ReferenceError: Cannot access 'greet' before initialization
const greet = function(name) {
return "こんにちは、" + name;
};
var で宣言した関数式は、変数自体は undefined で初期化される。
宣言前に呼び出すと undefined を関数として呼び出そうとするため、TypeError が発生する。
// var での宣言: undefinedで初期化されるためTypeError
console.log(greet); // undefined (変数自体はホイスティングされる)
console.log(greet("Alice")); // TypeError: greet is not a function
var greet = function(name) {
return "こんにちは、" + name;
};
関数宣言と関数式のホイスティング挙動を比較した例を以下に示す。
// 関数宣言: 宣言前に呼び出し可能
console.log(declaredFunc()); // "関数宣言"
function declaredFunc() {
return "関数宣言";
}
// 関数式 (const): 宣言前の呼び出しはReferenceError
// console.log(expressionFunc()); // ReferenceError
const expressionFunc = function() {
return "関数式";
};
// 宣言後は通常通り呼び出せる
console.log(expressionFunc()); // "関数式"
名前付き関数式
名前付き関数式は、関数式に名前を付けた形式である。
この名前は関数式の外部からはアクセスできず、関数自身のスコープ内でのみ参照可能である。
名前付き関数式が有用な場面として、再帰呼び出しとデバッグの2つが挙げられる。
再帰呼び出しでの使用例を以下に示す。
const fibonacci = function calcFibonacci(n) {
if (n <= 1) return n;
return calcFibonacci(n - 1) + calcFibonacci(n - 2); // 内部名で再帰呼び出し
};
console.log(fibonacci(10)); // 55
// 関数名は外部からはアクセスできない
// console.log(calcFibonacci(10)); // ReferenceError
デバッグでの有用性を以下に示す。
無名関数式の場合、スタックトレースに関数名が表示されず、デバッグが困難になることがある。
名前付き関数式を使用すると、スタックトレースに関数名が表示されて問題箇所の特定が容易になる。
// 無名関数式: スタックトレースに "<anonymous>" と表示される
const anonymousFunc = function() {
throw new Error("エラー発生");
};
// 名前付き関数式: スタックトレースに "namedFunc" と表示される
const namedFunc = function namedFunc() {
throw new Error("エラー発生");
};
関数宣言と関数式の比較
下表に、関数宣言と関数式の主な違いを示す。
| 観点 | 関数宣言 | 関数式 |
|---|---|---|
| ホイスティング | スコープ先頭に関数全体が巻き上げられる | 変数のホイスティングルールに従う |
| 宣言前の呼び出し | 可能 | 不可 (ReferenceError または TypeError) |
| 関数名 | 必須 | 省略可能 (無名関数式) |
| 構文の位置 | 文として記述 | 式として記述 (変数代入など) |
| 使用場面 | スコープ全体で使用する汎用的な関数 | 条件付き定義、コールバック、高階関数 |
| スコープ | 関数スコープ / グローバルスコープ | 変数の宣言キーワードに依存 |
デフォルト引数
基本構文
デフォルト引数は、ES2015で導入された機能であり、引数が渡されなかった場合に使用する初期値を関数の定義時に指定できる。
function <関数名>(<引数1> = <デフォルト値1>, <引数2> = <デフォルト値2>) {
// 処理
}
使用例を以下に示す。
function greet(name = "ゲスト", greeting = "こんにちは") {
return greeting + "、" + name + "さん";
}
console.log(greet("Alice", "おはよう")); // "おはよう、Aliceさん"
console.log(greet("Bob")); // "こんにちは、Bobさん"
console.log(greet()); // "こんにちは、ゲストさん"
デフォルト引数がトリガーされるのは、引数に undefined が渡された場合のみである。
null を渡した場合はデフォルト値が適用されず、null がそのまま使用される。
function showValue(value = "デフォルト") {
console.log(value);
}
showValue(undefined); // "デフォルト" (undefinedはデフォルト値をトリガー)
showValue(null); // null (nullはデフォルト値をトリガーしない)
showValue(0); // 0 (0もデフォルト値をトリガーしない)
showValue(""); // "" (空文字もデフォルト値をトリガーしない)
式を使用したデフォルト値
デフォルト引数には、リテラル値だけでなく任意の式を使用できる。
前に定義したパラメータを参照することも可能である。
// 前のパラメータを参照
function createRectangle(width = 100, height = width * 2) {
return { width, height };
}
console.log(createRectangle()); // { width: 100, height: 200 }
console.log(createRectangle(50)); // { width: 50, height: 100 }
console.log(createRectangle(50, 80)); // { width: 50, height: 80 }
関数呼び出しをデフォルト値として使用することも可能である。
デフォルト値の式は、関数が呼び出されるたびに評価される。(定義時ではなく呼び出し時に評価)
function getDefaultId() {
return Math.floor(Math.random() * 1000);
}
function createUser(name, id = getDefaultId()) {
return { name, id };
}
console.log(createUser("Alice")); // { name: "Alice", id: <ランダムな値> }
console.log(createUser("Bob")); // { name: "Bob", id: <別のランダムな値> }
デフォルト引数導入前のパターン
ES2015以前は、デフォルト引数を実現するために論理OR演算子 (||) を用いたフォールバックパターンが広く使われていた。
// ES2015以前のパターン (||演算子によるフォールバック)
function greet(name, greeting) {
name = name || "ゲスト";
greeting = greeting || "こんにちは";
return greeting + "、" + name + "さん";
}
しかし、このパターンには問題がある。
|| 演算子は引数がFalsyな値 (0、""、false 等) の場合もデフォルト値を適用してしまう。
function setCount(count) {
count = count || 10; // 問題: count が 0 の場合もデフォルト値が使われる
console.log(count);
}
setCount(5); // 5
setCount(0); // 10 (意図しない動作: 0 は Falsy なので 10 が使われる)
setCount(undefined); // 10
// ES2015のデフォルト引数を使用した場合
function setCountFixed(count = 10) {
console.log(count);
}
setCountFixed(0); // 0 (正しく動作する: 0 はundefinedではないため)
setCountFixed(undefined); // 10
残余引数
基本構文
残余引数 (Rest Parameters) は、ES2015で導入された機能であり、可変長の引数を真の配列として受け取る仕組みである。
パラメータ名の前に ... (スプレッド構文) を付けて定義する。
function 関数名(...引数名) {
// 引数名は真の配列として使用できる
}
残余引数は、必ず最後のパラメータとして定義しなければならない。
残余引数の後にさらに引数を定義すると SyntaxError となる。
// 正しい例: 最後のパラメータとして定義
function fn(a, b, ...rest) {
console.log(a); // 最初の引数
console.log(b); // 2番目の引数
console.log(rest); // 3番目以降の引数が配列として格納される
}
fn(1, 2, 3, 4, 5);
// 1
// 2
// [3, 4, 5]
// 誤りの例: 残余引数の後にパラメータを定義 (SyntaxError)
// function badFn(...rest, last) { }
使用例
残余引数を使用した合計値の計算例を以下に示す。
残余引数は真の配列であるため、map、filter、reduce 等のArrayメソッドを直接使用できる。
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum()); // 0
- 通常のパラメータと残余引数を組み合わせた例
function logMessage(level, ...messages) { const prefix = "[" + level.toUpperCase() + "]"; messages.forEach(function(msg) { console.log(prefix + " " + msg); }); } logMessage("info", "サーバ起動", "ポート3000で待機中"); // [INFO] サーバ起動 // [INFO] ポート3000で待機中 logMessage("error", "接続失敗"); // [ERROR] 接続失敗
- 配列操作との組み合わせ例
function filterAndDouble(threshold, ...numbers) { return numbers .filter(function(n) { return n > threshold; }) .map(function(n) { return n * 2; }); } console.log(filterAndDouble(3, 1, 2, 3, 4, 5)); // [8, 10]
argumentsオブジェクトとの比較
ES2015以前は、関数に渡された全ての引数にアクセスするために arguments オブジェクトを使用していた。
残余引数は arguments オブジェクトの問題を解消する目的でも導入されている。
arguments オブジェクトの使用例を以下に示す。
function oldSum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(oldSum(1, 2, 3)); // 6
// argumentsはArrayではないため、map等は直接使えない
// arguments.reduce(...); // TypeError
下表に、残余引数と arguments オブジェクトの違いを示す。
| 観点 | 残余引数 | argumentsオブジェクト |
|---|---|---|
| 型 | 真のArrayインスタンス | 配列風オブジェクト (Array-like) |
| Arrayメソッド | map / filter / reduce 等を直接使用可能 | 直接使用不可 (Array.fromで変換が必要) |
| 対象の引数 | 名前付きパラメータを除いた残りの引数 | 関数に渡された全ての引数 |
| アロー関数 | 使用可能 | 使用不可 (アロー関数はargumentsを持たない) |
| 名前 | 任意の名前を付けられる | 常にargumentsという名前 |
| 導入バージョン | ES2015 (ES6) | ES1 (初期) |
arguments オブジェクトを配列に変換する方法と残余引数の比較を以下に示す。
// argumentsを配列に変換する方法 (ES2015以前)
function oldStyle() {
const args = Array.prototype.slice.call(arguments);
// または
const args2 = Array.from(arguments);
return args.reduce(function(total, n) { return total + n; }, 0);
}
// 残余引数を使用した方法 (推奨)
function newStyle(...numbers) {
return numbers.reduce(function(total, n) { return total + n; }, 0);
}
console.log(oldStyle(1, 2, 3)); // 6
console.log(newStyle(1, 2, 3)); // 6
関連情報
- JavaScriptの基礎 - 変数宣言
- let / const / varの宣言方法、スコープ、ホイスティング
- JavaScriptの基礎 - アロー関数
- アロー関数の構文、暗黙のreturn、レキシカルthis
- JavaScriptの基礎 - クロージャ
- レキシカルスコープ、クロージャの仕組みと実用例
- JavaScriptの基礎 - コールバック関数
- コールバック関数、高階関数、タイマ、イベントリスナー