前回の記事 では、JavaScriptの配列の基本とモダンな操作方法について紹介しました。今回は、JavaScriptの関数について、従来の関数宣言から最新のアロー関数までを扱います。
関数の基本
関数は、コードの再利用性と構造化を可能にします。JavaScriptでは関数は「第一級オブジェクト」として扱われるため、変数に代入したり、他の関数の引数として渡したり、関数から返したりできます。
関数宣言と関数式
JavaScriptでは主に3つの方法で関数を定義できます。
// 1. 関数宣言
function sayHello(name) {
return `こんにちは、${name}さん!`;
}
// 2. 関数式
const greet = function(name) {
return `こんにちは、${name}さん!`;
};
// 3. アロー関数(ES2015以降)
const welcome = (name) => {
return `こんにちは、${name}さん!`;
};
// 使い方は同じ
console.log(sayHello("田中")); // "こんにちは、田中さん!"
console.log(greet("佐藤")); // "こんにちは、佐藤さん!"
console.log(welcome("鈴木")); // "こんにちは、鈴木さん!"
関数宣言と関数式の違い:ホイスティング
関数宣言と関数式の主な違いは「ホイスティング」の挙動です。ホイスティングとは、変数や関数の宣言が、スコープの先頭に「巻き上げ」られるJavaScriptの仕様です。
// 関数宣言は、定義の前に呼び出せる(ホイスティングされる)
console.log(sayHello("山田")); // "こんにちは、山田さん!"
function sayHello(name) {
return `こんにちは、${name}さん!`;
}
// 関数式は、定義の前に呼び出すとエラーになる
// エラー: Cannot access 'greet' before initialization
// (または ReferenceError: greet is not defined)
console.log(greet("小林"));
const greet = function(name) {
return `こんにちは、${name}さん!`;
};
関数宣言は、JavaScriptエンジンによって実行前にスコープの先頭に「巻き上げ」られ、関数全体が利用可能になります。
一方、関数式は変数の宣言部分のみがスコープの先頭にホイスティングされますが、constで宣言された変数は宣言行に到達するまで「一時的デッドゾーン(Temporal Dead Zone: TDZ)」にあり、参照できません。そのため、関数の代入が実行される前に呼び出すとエラーになります。
どちらを使うべきか
実務でコードを書く際は、以下のような基準で選択するとよいでしょう。
-
予測可能性
関数式を使うと、宣言と実行の順序が明確になり、コードの読みやすさが向上する -
コードの構造化
大きな関数を先に宣言し、ヘルパー関数を後に記述したい場合は、関数宣言を使うのが便利 -
条件付き関数定義
条件分岐内で関数を定義する場合は、関数式を使うのが一般的(関数宣言を条件分岐内で使うと、ブラウザによって異なる挙動となる場合がある)
アロー関数
ES2015(ES6)で導入されたアロー関数は、より簡潔な関数の記述を可能にします。
基本的な構文
// 通常の関数式
const add = function(a, b) {
return a + b;
};
// 同じ機能をアロー関数で記述
const addArrow = (a, b) => {
return a + b;
};
// 処理が1行の場合は、さらに簡略化可能
const addArrowConcise = (a, b) => a + b;
console.log(add(2, 3)); // 5
console.log(addArrow(2, 3)); // 5
console.log(addArrowConcise(2, 3)); // 5
引数が1つの場合は、括弧の省略もできます。
// 引数が1つの場合
const double = x => x * 2;
console.log(double(5)); // 10
// 引数がない場合は、空の括弧が必要
const sayHi = () => "こんにちは!";
console.log(sayHi()); // "こんにちは!"
アロー関数が導入された理由
アロー関数が導入された主な理由は以下のとおりです。
-
より簡潔な構文
とくに短い関数やコールバック関数で記述が簡単になる -
this
の束縛に関する問題の解決
アロー関数内のthis
は、定義した際のコンテキストを保持する
this の扱いの違い
アロー関数と通常の関数で最も重要な違いは、this
キーワードの挙動です。
通常の関数では、this
は呼び出し元によって決まります(動的バインディング)。一方、アロー関数では、this
は関数が定義された時点での値をキャプチャします(レキシカルバインディング)。
// 通常の関数では、thisは呼び出し元によって決まる
const person = {
name: "田中",
sayNameFunction: function() {
console.log(this.name);
}
};
person.sayNameFunction(); // "田中" (thisはpersonオブジェクト)
// アロー関数では、thisは定義時のコンテキストを維持する
const person2 = {
name: "佐藤",
sayNameArrow: () => {
// ここでの this は、person2 オブジェクトが定義された「グローバルスコープ」の this を参照します。
// ブラウザ環境では通常 window オブジェクト、Node.jsでは global オブジェクト(厳密モードでは undefined)です。
// このため、this.name はグローバルスコープに name プロパティがない限り undefined となります。
console.log(this.name);
},
sayNameWithinArrow: function() {
// ここでのthisはperson2オブジェクト
const innerArrow = () => {
console.log(this.name); // innerArrow の this は sayNameWithinArrow の外側の this(すなわちperson2)を継承する
};
innerArrow();
}
};
person2.sayNameArrow(); // undefined(thisはグローバルコンテキストを指すため、nameプロパティがない)
person2.sayNameWithinArrow(); // "佐藤"(innerArrowのthisはsayNameWithinArrowのthisを継承)
実務での選択基準
実務において「関数式 vs アロー関数」を選ぶ際の一般的な基準は、以下のとおりです。
メソッドとして定義する場合
オブジェクトのメソッドとしてthis
をオブジェクト自身に参照させたい場合は、通常の関数を使用します。
const counter = {
count: 0,
increment: function() {
this.count++;
}
};
コールバック関数の場合
コールバック関数では通常アロー関数が適しています(とくに外側のスコープのthis
にアクセスしたい場合)。
const timer = {
seconds: 0,
start: function() {
// setIntervalのコールバックでアロー関数を使うと、thisはtimerオブジェクトを参照する
setInterval(() => {
this.seconds++;
console.log(`${this.seconds}秒経過`);
}, 1000);
}
};
コンストラクタとして使う場合
アロー関数には prototype
プロパティがなく、new
演算子で呼び出せないため、コンストラクタとしては通常の関数を使用します。
短い処理やチェーン内のコールバック
とくに配列メソッドのコールバックなど、短い処理の場合はアロー関数が読みやすくなります。
const doubled = [1, 2, 3].map(x => x * 2);
デフォルトパラメータ
ES2015からは、関数のパラメータにデフォルト値を設定できるようになりました。
// デフォルトパラメータの基本
function greet(name = "名無し", greeting = "こんにちは") {
return `${greeting}、${name}さん!`;
}
console.log(greet("田中")); // "こんにちは、田中さん!"
console.log(greet("鈴木", "おはよう")); // "おはよう、鈴木さん!"
console.log(greet()); // "こんにちは、名無しさん!"
デフォルトパラメータは、より複雑な式や関数呼び出しも指定できます。
// デフォルト値に式や関数呼び出しを使用
function getTimestamp() {
return new Date().toLocaleString();
}
function logActivity(activity, timestamp = getTimestamp()) {
console.log(`[${timestamp}] ${activity}`);
}
logActivity("ログイン");
// 例: [2025/1/15 12:34:56] ログイン
デフォルトパラメータは左から右へ評価され、後続のデフォルトパラメータで前のパラメータの参照もできます。
function calculatePrice(price, tax = 0.1, total = price + (price * tax)) {
return total;
}
console.log(calculatePrice(1000)); // 1100
console.log(calculatePrice(1000, 0.08)); // 1080
Rest Parameters(残余引数)
ES2015で導入されたRest parameters(...
構文)は、関数の引数において、任意の数の引数をまとめて配列として扱うことができます。この ...
構文は「スプレッド構文」とも呼ばれ、配列やオブジェクトの展開など、引数以外でも多岐にわたる用途で利用されますが、ここでは関数引数としての「Rest parameters」について解説します。
// Rest parameters
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4, 5)); // 15
Rest parametersは常に最後のパラメータである必要があり、他のパラメータと組み合わせることもできます。
function filterAndSum(threshold, ...numbers) {
return numbers
.filter(num => num > threshold)
.reduce((total, num) => total + num, 0);
}
console.log(filterAndSum(3, 1, 2, 3, 4, 5)); // 9 (3より大きい4と5の合計)
argumentsオブジェクトとの違い
古いJavaScriptコードでは、arguments
オブジェクトを使って可変長引数を扱っていましたが、arguments
はレガシーな機能であり、モダンなJavaScriptではRest parametersを使用することが強く推奨されます。Rest parametersには以下のような利点があります。
(1)配列のメソッドが使える
arguments
はarray-likeオブジェクトであり、配列のメソッドを直接使用できませんが、Rest parametersは純粋な配列になります。
// 旧式: arguments
function oldSum() {
let total = 0;
// argumentsオブジェクトはmap, reduceなどの配列メソッドが使えない
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// 配列メソッドを使いたい場合は変換が必要
function oldSum2() {
// Array.from()やスプレッド構文でargumentsを配列に変換
const args = Array.from(arguments);
// または const args = [...arguments];
return args.reduce((total, num) => total + num, 0);
}
// モダン: Rest parameters
function newSum(...numbers) {
// numbersは最初から配列なので、配列メソッドが直接使える
return numbers.reduce((total, num) => total + num, 0);
}
(2)アロー関数内で使える
アロー関数内では arguments
オブジェクトにアクセスできませんが、Rest parametersは使用できます。
// これはエラー - アロー関数にはargumentsがない
const arrowSum = () => {
return Array.from(arguments).reduce((total, num) => total + num, 0);
};
// Rest parametersなら問題なく動作
const arrowSum2 = (...numbers) => {
return numbers.reduce((total, num) => total + num, 0);
};
(3)より明示的
コードの意図が明確になり、可読性が向上します。
実践的なコード例
1. 関数合成(Function Composition)
複数の関数を組み合わせて新しい関数を作成する例です。
// 単一の値を変換する関数
const double = x => x * 2;
const square = x => x * x;
const addOne = x => x + 1;
// 関数合成のヘルパー関数
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
// 合成関数: 値を2倍にして、2乗して、1を足す
const doubleThenSquareThenAddOne = compose(addOne, square, double);
console.log(doubleThenSquareThenAddOne(3));
// double(3) => 6
// square(6) => 36
// addOne(36) => 37
// 結果: 37
2. メモ化(再帰関数の最適化)
関数の結果をキャッシュして、同じ入力に対する計算を省略する例です。
// メモ化関数
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log(`キャッシュから取得: ${key}`);
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
// 非効率な再帰フィボナッチ関数
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// メモ化されたフィボナッチ関数
const memoFibonacci = memoize(function(n) {
if (n <= 1) return n;
return memoFibonacci(n - 1) + memoFibonacci(n - 2);
});
console.time('Regular');
console.log(fibonacci(30)); // 非常に時間がかかる
console.timeEnd('Regular');
console.time('Memoized');
console.log(memoFibonacci(30)); // 非常に高速
console.timeEnd('Memoized');
3. 部分適用とカリー化
関数の一部の引数を固定して新しい関数を作成する例です。
// 部分適用の関数
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
// 元の関数
function greet(greeting, name) {
return `${greeting}、${name}さん!`;
}
// 部分適用で「こんにちは」を固定した新しい関数
const sayHelloTo = partial(greet, "こんにちは");
console.log(sayHelloTo("田中")); // "こんにちは、田中さん!"
// カリー化の関数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return function(...moreArgs) {
return curried(...args, ...moreArgs);
};
};
}
// greet関数をカリー化
const curriedGreet = curry(greet);
const sayHello = curriedGreet("こんにちは");
console.log(sayHello("鈴木")); // "こんにちは、鈴木さん!"
復習
基本問題
1. 以下のコードを実行すると、何が表示されるか
function greet() {
console.log("こんにちは");
}
const greet2 = function() {
console.log("おはよう");
};
greet();
greet2();
2. 以下のアロー関数を通常の関数式に書き直す
const multiply = (a, b) => a * b;
3. 以下のコードを実行すると、何が表示されるか
function calculateTotal(price, tax = 0.1) {
return price + (price * tax);
}
console.log(calculateTotal(1000));
console.log(calculateTotal(1000, 0.08));
実践問題
4. 以下のオブジェクトと関数で、コンソールに何が出力されるか
const user = {
name: "田中",
sayHello: function() {
console.log(`こんにちは、${this.name}さん!`);
},
sayHelloArrow: () => {
console.log(`こんにちは、${this.name}さん!`);
}
};
user.sayHello();
user.sayHelloArrow();
5. Rest parametersを使って、任意の数の文字列を連結する concat
関数を作成する(区切り文字として最初の引数を受け取り、残りの引数はすべて連結する)
// 使用例:
// concat('-', 'a', 'b', 'c') => "a-b-c"
// concat('/', '2023', '01', '15') => "2023/01/15"
function concat(/* ここにコードを書く */) {
// ここにコードを書く
}
6. 以下の再帰関数を高速化するためにメモ化を適用する
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
解答例
問題1
// このコードを実行すると、以下が順番に表示される
function greet() {
console.log("こんにちは");
}
const greet2 = function() {
console.log("おはよう");
};
greet(); // "こんにちは"
greet2(); // "おはよう"
問題2
// アロー関数
const multiply = (a, b) => a * b;
// 通常の関数式に書き直す
const multiply2 = function(a, b) {
return a * b;
};
問題3
function calculateTotal(price, tax = 0.1) {
return price + (price * tax);
}
console.log(calculateTotal(1000)); // 1100 (1000 + 1000 * 0.1)
console.log(calculateTotal(1000, 0.08)); // 1080 (1000 + 1000 * 0.08)
問題4
const user = {
name: "田中",
sayHello: function() {
console.log(`こんにちは、${this.name}さん!`);
},
sayHelloArrow: () => {
console.log(`こんにちは、${this.name}さん!`);
}
};
user.sayHello(); // "こんにちは、田中さん!"
// thisはuserオブジェクトを参照する
user.sayHelloArrow(); // "こんにちは、undefinedさん!"
// アロー関数のthisはオブジェクトのメソッドとしての呼び出し方に影響を受けず、
// 定義時のスコープ(この場合はグローバルスコープ)でのthisを参照する
問題5
function concat(separator, ...strings) {
return strings.join(separator);
}
// 使用例
console.log(concat('-', 'a', 'b', 'c')); // "a-b-c"
console.log(concat('/', '2023', '01', '15')); // "2023/01/15"
問題6
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
const factorial = memoize(function(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
});
// テスト
console.time('Factorial');
console.log(factorial(20)); // 2432902008176640000
console.timeEnd('Factorial');
// 2回目の呼び出しは非常に速い(キャッシュから結果を取得)
console.time('Factorial cached');
console.log(factorial(20)); // 2432902008176640000
console.timeEnd('Factorial cached');
まとめ
JavaScriptの関数は非常に柔軟で強力です。従来の関数宣言や関数式に加え、ES2015で導入されたアロー関数、デフォルトパラメータ、Rest parametersなどの新機能により、より簡潔なコードが書けるようになりました。
重要なのは、アロー関数と従来の関数の違い、とくに this
の挙動の違いを理解することです。状況に応じて適切な関数の形式を選ぶことで、より読みやすく保守しやすいコードを書くことができます。
また、関数は単なる処理の集まりではなく、「値」としての性質を持ちます。そのため、関数を引数として渡したり、他の関数から返したりする高階関数のパターンが可能になり、より抽象度の高いプログラミングが実現できます。
次回 は、JavaScriptのスコープとクロージャについて解説します。変数の有効範囲と、関数がどのように状態を「記憶」するのかについて詳しく見ていきます。