新・JavaScript文法（5）：関数の基本とアロー関数

[前回の記事](https://qiita.com/azmnc/items/810f1334d33fccfbf8c1) では、JavaScriptの配列の基本とモダンな操作方法について紹介しました。今回は、JavaScriptの関数について、従来の関数宣言から最新のアロー関数までを扱います。

## 関数の基本

関数は、コードの再利用性と構造化を可能にします。JavaScriptでは関数は「第一級オブジェクト」として扱われるため、変数に代入したり、他の関数の引数として渡したり、関数から返したりできます。

### 関数宣言と関数式

JavaScriptでは主に3つの方法で関数を定義できます。

```javascript
// 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の仕様です。

```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）で導入されたアロー関数は、より簡潔な関数の記述を可能にします。

### 基本的な構文

```javascript
// 通常の関数式
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つの場合は、括弧の省略もできます。

```javascript
// 引数が1つの場合
const double = x => x * 2;
console.log(double(5)); // 10

// 引数がない場合は、空の括弧が必要
const sayHi = () => "こんにちは！";
console.log(sayHi()); // "こんにちは！"
```

### アロー関数が導入された理由

アロー関数が導入された主な理由は以下のとおりです。

1. **より簡潔な構文**
   とくに短い関数やコールバック関数で記述が簡単になる
2. **`this`の束縛に関する問題の解決**
   アロー関数内の`this`は、定義した際のコンテキストを保持する

### this の扱いの違い

アロー関数と通常の関数で最も重要な違いは、`this`キーワードの挙動です。

通常の関数では、`this`は呼び出し元によって決まります（動的バインディング）。一方、アロー関数では、`this`は関数が定義された時点での値をキャプチャします（レキシカルバインディング）。

```javascript
// 通常の関数では、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`をオブジェクト自身に参照させたい場合は、通常の関数を使用します。

```javascript
const counter = {
  count: 0,
  increment: function() {
    this.count++;
  }
};
```

#### コールバック関数の場合

コールバック関数では通常アロー関数が適しています（とくに外側のスコープの`this`にアクセスしたい場合）。

```javascript
const timer = {
  seconds: 0,
  start: function() {
    // setIntervalのコールバックでアロー関数を使うと、thisはtimerオブジェクトを参照する
    setInterval(() => {
      this.seconds++;
      console.log(`${this.seconds}秒経過`);
    }, 1000);
  }
};
```

#### コンストラクタとして使う場合

アロー関数には `prototype` プロパティがなく、`new` 演算子で呼び出せないため、コンストラクタとしては通常の関数を使用します。

#### 短い処理やチェーン内のコールバック

とくに配列メソッドのコールバックなど、短い処理の場合はアロー関数が読みやすくなります。

```javascript
const doubled = [1, 2, 3].map(x => x * 2);
```

## デフォルトパラメータ

ES2015からは、関数のパラメータにデフォルト値を設定できるようになりました。

```javascript
// デフォルトパラメータの基本
function greet(name = "名無し", greeting = "こんにちは") {
  return `${greeting}、${name}さん！`;
}

console.log(greet("田中"));         // "こんにちは、田中さん！"
console.log(greet("鈴木", "おはよう")); // "おはよう、鈴木さん！"
console.log(greet());              // "こんにちは、名無しさん！"
```

デフォルトパラメータは、より複雑な式や関数呼び出しも指定できます。

```javascript
// デフォルト値に式や関数呼び出しを使用
function getTimestamp() {
  return new Date().toLocaleString();
}

function logActivity(activity, timestamp = getTimestamp()) {
  console.log(`[${timestamp}] ${activity}`);
}

logActivity("ログイン");
// 例: [2025/1/15 12:34:56] ログイン
```

デフォルトパラメータは左から右へ評価され、後続のデフォルトパラメータで前のパラメータの参照もできます。

```javascript
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」について解説します。

```javascript
// 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は常に最後のパラメータである必要があり、他のパラメータと組み合わせることもできます。

```javascript
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は純粋な配列になります。

```javascript
// 旧式: 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は使用できます。

```javascript
// これはエラー - アロー関数には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）

複数の関数を組み合わせて新しい関数を作成する例です。

```javascript
// 単一の値を変換する関数
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. メモ化（再帰関数の最適化）

関数の結果をキャッシュして、同じ入力に対する計算を省略する例です。

```javascript
// メモ化関数
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. 部分適用とカリー化

関数の一部の引数を固定して新しい関数を作成する例です。

```javascript
// 部分適用の関数
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. 以下のコードを実行すると、何が表示されるか

```javascript
function greet() {
  console.log("こんにちは");
}

const greet2 = function() {
  console.log("おはよう");
};

greet();
greet2();
```

#### 2. 以下のアロー関数を通常の関数式に書き直す

```javascript
const multiply = (a, b) => a * b;
```

#### 3. 以下のコードを実行すると、何が表示されるか

```javascript
function calculateTotal(price, tax = 0.1) {
  return price + (price * tax);
}

console.log(calculateTotal(1000));
console.log(calculateTotal(1000, 0.08));
```

### 実践問題

#### 4. 以下のオブジェクトと関数で、コンソールに何が出力されるか

```javascript
const user = {
  name: "田中",
  sayHello: function() {
    console.log(`こんにちは、${this.name}さん！`);
  },
  sayHelloArrow: () => {
    console.log(`こんにちは、${this.name}さん！`);
  }
};

user.sayHello();
user.sayHelloArrow();
```

#### 5. Rest parametersを使って、任意の数の文字列を連結する `concat` 関数を作成する（区切り文字として最初の引数を受け取り、残りの引数はすべて連結する）

```javascript
// 使用例:
// concat('-', 'a', 'b', 'c') => "a-b-c"
// concat('/', '2023', '01', '15') => "2023/01/15"

function concat(/* ここにコードを書く */) {
  // ここにコードを書く
}
```

#### 6. 以下の再帰関数を高速化するためにメモ化を適用する

```javascript
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}
```

### 解答例

#### 問題1

```javascript
// このコードを実行すると、以下が順番に表示される
function greet() {
  console.log("こんにちは");
}

const greet2 = function() {
  console.log("おはよう");
};

greet();    // "こんにちは"
greet2();   // "おはよう"
```

#### 問題2

```javascript
// アロー関数
const multiply = (a, b) => a * b;

// 通常の関数式に書き直す
const multiply2 = function(a, b) {
  return a * b;
};
```

#### 問題3

```javascript
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

```javascript
const user = {
  name: "田中",
  sayHello: function() {
    console.log(`こんにちは、${this.name}さん！`);
  },
  sayHelloArrow: () => {
    console.log(`こんにちは、${this.name}さん！`);
  }
};

user.sayHello();      // "こんにちは、田中さん！"
// thisはuserオブジェクトを参照する

user.sayHelloArrow(); // "こんにちは、undefinedさん！"
// アロー関数のthisはオブジェクトのメソッドとしての呼び出し方に影響を受けず、
// 定義時のスコープ（この場合はグローバルスコープ）でのthisを参照する
```

#### 問題5

```javascript
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

```javascript
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` の挙動の違いを理解することです。状況に応じて適切な関数の形式を選ぶことで、より読みやすく保守しやすいコードを書くことができます。

また、関数は単なる処理の集まりではなく、「値」としての性質を持ちます。そのため、関数を引数として渡したり、他の関数から返したりする高階関数のパターンが可能になり、より抽象度の高いプログラミングが実現できます。

[次回](https://qiita.com/azmnc/items/a678261bde849c32c7d1) は、JavaScriptのスコープとクロージャについて解説します。変数の有効範囲と、関数がどのように状態を「記憶」するのかについて詳しく見ていきます。