新・JavaScript文法（4）：配列の基本とモダンな操作

[前回の記事](https://qiita.com/azmnc/items/dbfa749aba34a1ad8fa3) では、JavaScriptの制御構文とループ処理について紹介しました。今回は、JavaScriptにおける配列の基本的な操作から、モダンなES2015以降の配列操作まで説明します。

## 配列の基本

配列は、複数の値を順序つけて格納するためのデータ構造です。JavaScriptでは、異なる型の値を同じ配列内に混在させることができます。

### 配列の作成方法

JavaScriptで配列を作成するには、複数の方法があります。

```javascript
// 1. 配列リテラル（最も一般的な方法）
const fruits = ["りんご", "バナナ", "オレンジ"];

// 2. Arrayコンストラクタ
const numbers = new Array(1, 2, 3, 4, 5);

// 3. Array.ofメソッド
// Array.of() は、単一の数値引数であっても要素として扱われます（new Array()の特殊な挙動を回避）
const colors = Array.of("赤", "青", "緑");

// 4. 空の配列を作成
const emptyArray1 = [];
const emptyArray2 = new Array();

// 5. 特定の長さの配列を作成（すべての要素はundefined）
const fixedArray = new Array(5); // 長さ5の空の配列
console.log(fixedArray.length); // 5
console.log(fixedArray); // [empty × 5]

// 6. 配列を指定の値で初期化
const filledArray = new Array(5).fill(0); // [0, 0, 0, 0, 0]
```

※`new Array(5)` で作成した配列について [補足情報](https://qiita.com/azmnc/items/810f1334d33fccfbf8c1#comment-385461d3bfb029c067c2) をいただきました。参考にしてください。

#### Array()コンストラクタの注意点

`Array()` コンストラクタの引数の扱いに関して、その注意点を下記にまとめます。

```javascript
// 引数が1つの数値の場合：その長さの空の配列を作成
const arr1 = new Array(3);
console.log(arr1); // [empty × 3]

// 引数が1つの数値以外または複数の引数：要素として扱われる
const arr2 = new Array("3");
console.log(arr2); // ["3"]

const arr3 = new Array(1, 2, 3);
console.log(arr3); // [1, 2, 3]
```

### 配列要素へのアクセス

配列の要素には、インデックス（添え字）を使用してアクセスします。

```javascript
const fruits = ["りんご", "バナナ", "オレンジ"];

// インデックスを指定して要素にアクセス（0から始まる）
console.log(fruits[0]); // "りんご"
console.log(fruits[1]); // "バナナ"
console.log(fruits[2]); // "オレンジ"

// 存在しないインデックスにアクセスするとundefinedが返される
console.log(fruits[3]); // undefined

// 配列の長さ
console.log(fruits.length); // 3

// 最後の要素にアクセス
console.log(fruits[fruits.length - 1]); // "オレンジ"

// ES2022以降: at()メソッドを使った要素アクセス（負のインデックスもサポート）
console.log(fruits.at(0));  // "りんご"
console.log(fruits.at(-1)); // "オレンジ" (最後の要素)
console.log(fruits.at(-2)); // "バナナ" (後ろから2番目の要素)
```

### 配列の変更

配列は `const` で宣言しても、その要素は変更できます（ミュータブル）。

```javascript
const fruits = ["りんご", "バナナ", "オレンジ"];

// 要素の変更
fruits[1] = "ぶどう";
console.log(fruits); // ["りんご", "ぶどう", "オレンジ"]

// 要素の追加
fruits[3] = "いちご";
console.log(fruits); // ["りんご", "ぶどう", "オレンジ", "いちご"]

// スパース配列（穴のある配列）
fruits[5] = "メロン";
console.log(fruits); // ["りんご", "ぶどう", "オレンジ", "いちご", empty, "メロン"]
console.log(fruits.length); // 6
console.log(fruits[4]); // undefined

// 配列の長さを直接変更
fruits.length = 3;
console.log(fruits); // ["りんご", "ぶどう", "オレンジ"]
```

## 基本的な配列操作メソッド

JavaScriptの配列には、要素の追加や削除を行なうための基本的なメソッドが用意されています。

### 要素の追加と削除

```javascript
const fruits = ["りんご", "バナナ"];

// 末尾に要素を追加
fruits.push("オレンジ");
console.log(fruits); // ["りんご", "バナナ", "オレンジ"]

// 末尾から要素を削除し、その値を返す
const lastFruit = fruits.pop();
console.log(lastFruit); // "オレンジ"
console.log(fruits); // ["りんご", "バナナ"]

// 先頭に要素を追加
fruits.unshift("いちご");
console.log(fruits); // ["いちご", "りんご", "バナナ"]

// 先頭から要素を削除し、その値を返す
const firstFruit = fruits.shift();
console.log(firstFruit); // "いちご"
console.log(fruits); // ["りんご", "バナナ"]
```

### 配列の一部を取得・置換・削除

配列の一部の取得、置換、削除はすべて `splice()` メソッドで行なえます。

```javascript
const fruits = ["りんご", "バナナ", "オレンジ", "いちご", "メロン"];

// 指定位置から要素を削除
// splice(開始インデックス, 削除する要素数, ...追加する要素)
const removed1 = fruits.splice(2, 1); // インデックス2から1つの要素を削除
console.log(removed1); // ["オレンジ"]
console.log(fruits); // ["りんご", "バナナ", "いちご", "メロン"]

// 要素を削除して新しい要素を追加
const removed2 = fruits.splice(1, 2, "ぶどう", "キウイ");
console.log(removed2); // ["バナナ", "いちご"]
console.log(fruits); // ["りんご", "ぶどう", "キウイ", "メロン"]

// 要素を削除せずに追加のみ
fruits.splice(2, 0, "パイナップル");
console.log(fruits); // ["りんご", "ぶどう", "パイナップル", "キウイ", "メロン"]
```

### 配列の一部を取得：slice

`splice()` は、元の配列を変更するメソッドです（破壊的）。それに対し、`slice()` メソッドも配列の一部を取得して新しい配列で返すメソッドですが、元の配列は変更されません。

```javascript
const fruits = ["りんご", "バナナ", "オレンジ", "いちご", "メロン"];

// slice(開始インデックス, 終了インデックス)
// 終了インデックスの要素は含まれない
const sliced1 = fruits.slice(1, 3);
console.log(sliced1); // ["バナナ", "オレンジ"]
console.log(fruits); // 元の配列は変更されない

// 終了インデックスを省略すると、最後まで取得
const sliced2 = fruits.slice(2);
console.log(sliced2); // ["オレンジ", "いちご", "メロン"]

// 負のインデックスは末尾からの位置
const sliced3 = fruits.slice(-2);
console.log(sliced3); // ["いちご", "メロン"]

// 配列のコピーを作成
const copy = fruits.slice();
console.log(copy); // ["りんご", "バナナ", "オレンジ", "いちご", "メロン"]
```

## モダンな配列操作メソッド

ES2015以降では、配列操作をより簡潔に行なうための多くのメソッドが導入されました。

### map/filter/reduce

これらのメソッドは、for文などを使わずに、配列全体への操作を簡潔に表現できます（関数型プログラミング的パターンとなります）。

```javascript
const numbers = [1, 2, 3, 4, 5];

// map: 各要素に関数を適用し、新しい配列を返す
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// filter: 条件に合う要素だけを抽出して新しい配列を返す
const even = numbers.filter(num => num % 2 === 0);
console.log(even); // [2, 4]

// reduce: 配列の要素を集約して単一の値を返す
// reduce(コールバック, 初期値)
const sum = numbers.reduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15

// reduceの別の例：最大値を求める
const max = numbers.reduce((max, current) =>
  current > max ? current : max, numbers[0]);
console.log(max); // 5
```

※補足：`reduce` メソッドは、第2引数を指定しない場合、配列が空だとエラーになります。

#### メソッドチェーン

これらのメソッドは `.` でつないで、連鎖させて使用できます。このような利用方法を「メソッドチェーン」と呼びます。

```javascript
const data = [
  { id: 1, name: "田中", age: 28, active: true },
  { id: 2, name: "鈴木", age: 35, active: false },
  { id: 3, name: "佐藤", age: 42, active: true },
  { id: 4, name: "山田", age: 31, active: true }
];

// アクティブなユーザーの平均年齢を計算
const averageAge = data
  .filter(user => user.active)
  .map(user => user.age)
  .reduce((sum, age, index, array) => sum + age / array.length, 0); // array.lengthでフィルタリング後の要素数を得て平均を計算

console.log(averageAge); // 33.67 (28 + 42 + 31) / 3
```

### find/findIndex/some/every

要素の検索や条件判定に便利なメソッドを紹介します。

```javascript
const users = [
  { id: 1, name: "田中", age: 28, role: "admin" },
  { id: 2, name: "鈴木", age: 35, role: "user" },
  { id: 3, name: "佐藤", age: 42, role: "user" },
  { id: 4, name: "山田", age: 31, role: "editor" }
];

// find: 条件に合う最初の要素を返す
const admin = users.find(user => user.role === "admin");
console.log(admin); // { id: 1, name: "田中", age: 28, role: "admin" }

// findIndex: 条件に合う最初の要素のインデックスを返す
const editorIndex = users.findIndex(user => user.role === "editor");
console.log(editorIndex); // 3

// some: 少なくとも1つの要素が条件を満たすかチェック
const hasYoungUser = users.some(user => user.age < 30);
console.log(hasYoungUser); // true

// every: すべての要素が条件を満たすかチェック
const allAdults = users.every(user => user.age >= 20);
console.log(allAdults); // true
```

### flat/flatMap

ネストされた配列を平坦化するためのメソッドです。

```javascript
// flat: ネストされた配列を平坦化
const nestedArray = [1, 2, [3, 4, [5, 6]]];
console.log(nestedArray.flat()); // [1, 2, 3, 4, [5, 6]]

// 深さを指定して平坦化
console.log(nestedArray.flat(2)); // [1, 2, 3, 4, 5, 6]

// flatMap: mapと同時に平坦化（1レベルのみ）
const sentences = ["Hello world", "JavaScript is awesome"];
const words = sentences.flatMap(sentence => sentence.split(' '));
console.log(words); // ["Hello", "world", "JavaScript", "is", "awesome"]
```

## スプレッド構文と配列

ES2015で導入されたスプレッド構文（`...`）は、配列操作において非常に便利です。スプレッドは日本語で「展開」を意味するように、スプレッド構文は配列を「展開」するためのものです。

コードで見たほうがわかりやすいので、以下の例を参考にしてください。

### 配列のコピーと結合

```javascript
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// 配列のコピー
const copy = [...arr1];
console.log(copy); // [1, 2, 3]

// 複数の配列を結合
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]

// スプレッド構文（...）を使わないとネストされた配列になる
const nested = [arr1, arr2];
console.log(nested); // [[1, 2, 3], [4, 5, 6]]

// 新しい要素を追加
const withNewItems = [...arr1, 7, 8];
console.log(withNewItems); // [1, 2, 3, 7, 8]

// 配列の先頭に要素を追加
const withItemsAtBeginning = [0, ...arr1];
console.log(withItemsAtBeginning); // [0, 1, 2, 3]
```

### 既存の値を残しつつ新しい配列を作成

```javascript
const original = [1, 2, 3, 4, 5];

// イミュータブルな操作（元の配列を変更しない）
const insertedAt2 = [...original.slice(0, 2), 'a', ...original.slice(2)];
console.log(insertedAt2); // [1, 2, 'a', 3, 4, 5]

// 要素を削除
const removedAt3 = [...original.slice(0, 3), ...original.slice(4)];
console.log(removedAt3); // [1, 2, 3, 5]

// 要素を置換
const replacedAt1 = [...original.slice(0, 1), 'x', ...original.slice(2)];
console.log(replacedAt1); // [1, 'x', 3, 4, 5]
```

### 関数引数への応用

```javascript
// 最大値を求める
const numbers = [5, 2, 8, 1, 4];
const max = Math.max(...numbers);
console.log(max); // 8

// 可変長引数を持つ関数
function sum(...nums) {
  return nums.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(...numbers)); // 20
```

## 配列の分割代入

ES2015の分割代入構文を使用すると、配列の要素を簡潔に取り出すことができます。

### 基本的な分割代入

```javascript
const colors = ["赤", "青", "緑", "黄"];

// 変数への分割代入
const [red, blue, green] = colors;
console.log(red, blue, green); // "赤" "青" "緑"

// 特定の要素のみを取得（スキップ）
const [first, , third] = colors;
console.log(first, third); // "赤" "緑"

// スプレッド構文で残りの要素を取得
const [primary, secondary, ...others] = colors;
console.log(primary); // "赤"
console.log(secondary); // "青"
console.log(others); // ["緑", "黄"]
```

### デフォルト値と入れ子の分割代入

```javascript
// デフォルト値を設定
const [a, b, c = "デフォルト"] = ["値1", "値2"];
console.log(a, b, c); // "値1" "値2" "デフォルト"

// 入れ子の配列の分割代入
const nested = ["a", ["b", "c"], "d"];
const [first, [second, third], fourth] = nested;
console.log(first, second, third, fourth); // "a" "b" "c" "d"
```

### 値の交換

```javascript
// 変数の値を交換
let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
```

### 関数の戻り値の分割代入

```javascript
// 複数の値を返す関数
function getCoordinates() {
  return [10, 20];
}

const [x, y] = getCoordinates();
console.log(x, y); // 10 20

// 戻り値の一部のみを使用
const [lat] = getCoordinates();
console.log(lat); // 10
```

## 実践的なコード例

### 1. データのフィルタリングと変換

```javascript
// 商品データの例
const products = [
  { id: 1, name: "ノートPC", price: 85000, category: "電化製品", stock: 20 },
  { id: 2, name: "デスク", price: 25000, category: "家具", stock: 5 },
  { id: 3, name: "イヤホン", price: 15000, category: "電化製品", stock: 30 },
  { id: 4, name: "チェア", price: 18000, category: "家具", stock: 10 },
  { id: 5, name: "マウス", price: 7000, category: "電化製品", stock: 0 }
];

// 在庫がある電化製品のみを抽出し、税込価格と共に表示
const availableElectronics = products
  .filter(product => product.category === "電化製品" && product.stock > 0)
  .map(product => {
    const taxIncluded = Math.floor(product.price * 1.1);
    return {
      id: product.id,
      name: product.name,
      originalPrice: product.price,
      taxIncludedPrice: taxIncluded,
      stock: product.stock
    };
  });

console.log(availableElectronics);
// [
//   {
//     id: 1,
//     name: "ノートPC",
//     originalPrice: 85000,
//     taxIncludedPrice: 93500,
//     stock: 20
//   },
//   {
//     id: 3,
//     name: "イヤホン",
//     originalPrice: 15000,
//     taxIncludedPrice: 16500,
//     stock: 30
//   }
// ]
```

### 2. データのグループ化と集計

```javascript
// 注文データの例
const orders = [
  { id: 1, product: "ノートPC", category: "電化製品", price: 85000, quantity: 1 },
  { id: 2, product: "デスク", category: "家具", price: 25000, quantity: 2 },
  { id: 3, product: "イヤホン", category: "電化製品", price: 15000, quantity: 3 },
  { id: 4, product: "チェア", category: "家具", price: 18000, quantity: 2 },
  { id: 5, product: "ノートPC", category: "電化製品", price: 85000, quantity: 1 }
];

// カテゴリー別の注文数と総額を計算
const ordersByCategory = orders.reduce((acc, order) => {
  const { category, price, quantity } = order;
  const amount = price * quantity;

  // カテゴリーが存在しない場合は初期化
  if (!acc[category]) {
    acc[category] = { count: 0, total: 0 };
  }

  // カテゴリーの注文数と総額を更新
  acc[category].count += quantity;
  acc[category].total += amount;

  return acc;
}, {});

console.log(ordersByCategory);
// {
//   "電化製品": { count: 5, total: 130000 },
//   "家具": { count: 4, total: 86000 }
// }

// 製品ごとの注文数と総額を計算
const ordersByProduct = orders.reduce((acc, order) => {
  const { product, price, quantity } = order;
  const amount = price * quantity;

  if (!acc[product]) {
    acc[product] = { count: 0, total: 0, unitPrice: price };
  }

  acc[product].count += quantity;
  acc[product].total += amount;

  return acc;
}, {});

console.log(ordersByProduct);
// {
//   "ノートPC": { count: 2, total: 170000, unitPrice: 85000 },
//   "デスク": { count: 2, total: 50000, unitPrice: 25000 },
//   "イヤホン": { count: 3, total: 45000, unitPrice: 15000 },
//   "チェア": { count: 2, total: 36000, unitPrice: 18000 }
// }
```

### 3. 複数の配列の操作

```javascript
// 製品データと在庫データが別々の配列にある場合
const products = [
  { id: 1, name: "ノートPC", price: 85000 },
  { id: 2, name: "デスク", price: 25000 },
  { id: 3, name: "イヤホン", price: 15000 }
];

const inventory = [
  { productId: 1, stock: 5 },
  { productId: 3, stock: 20 }
];

// 商品情報と在庫情報を結合
const productsWithStock = products.map(product => {
  // 在庫情報を検索
  const inventoryItem = inventory.find(item => item.productId === product.id);

  // 在庫情報を結合
  return {
    ...product,
    stock: inventoryItem ? inventoryItem.stock : 0,
    available: inventoryItem ? inventoryItem.stock > 0 : false
  };
});

console.log(productsWithStock);
// [
//   { id: 1, name: "ノートPC", price: 85000, stock: 5, available: true },
//   { id: 2, name: "デスク", price: 25000, stock: 0, available: false },
//   { id: 3, name: "イヤホン", price: 15000, stock: 20, available: true }
// ]
```

## 復習

### 基本問題

#### 1. 以下の配列から、すべての数値を2倍にした新しい配列を作成する

```javascript
const numbers = [1, 2, 3, 4, 5];
```

#### 2. 以下の配列から、20以上の数値のみをフィルタリングした新しい配列を作成する

```javascript
const values = [10, 25, 8, 30, 15, 42];
```

#### 3. 以下の配列の要素の合計を計算する

```javascript
const scores = [85, 92, 78, 90, 88];
```

### 実践問題

#### 4. 以下のユーザーデータから、20歳以上のアクティブなユーザーの名前だけを抽出した配列を作成する

```javascript
const users = [
  { id: 1, name: "田中", age: 18, active: true },
  { id: 2, name: "鈴木", age: 22, active: false },
  { id: 3, name: "佐藤", age: 25, active: true },
  { id: 4, name: "山田", age: 30, active: true }
];
```

#### 5. 以下の注文データから、製品ごとの注文数量を集計する

```javascript
const orders = [
  { product: "りんご", quantity: 3 },
  { product: "バナナ", quantity: 2 },
  { product: "りんご", quantity: 1 },
  { product: "オレンジ", quantity: 4 },
  { product: "バナナ", quantity: 5 }
];
```

#### 6. 配列の分割代入を使って、以下の変数を最小限のコードで入れ替える

```javascript
let a = 1;
let b = 2;
let c = 3;
```

### 解答例

#### 問題1

```javascript
// map()を使用する方法
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
```

#### 問題2

```javascript
// filter()を使用する方法
const filtered = values.filter(value => value >= 20);
console.log(filtered); // [25, 30, 42]
```

#### 問題3

```javascript
// reduce()を使用する方法
const sum = scores.reduce((total, score) => total + score, 0);
console.log(sum); // 433

// for...ofループを使用する方法
let sum2 = 0;
for (const score of scores) {
  sum2 += score;
}
console.log(sum2); // 433
```

#### 問題4

```javascript
const activeAdultNames = users
  .filter(user => user.age >= 20 && user.active)
  .map(user => user.name);

console.log(activeAdultNames); // ["佐藤", "山田"]
```

#### 問題5

```javascript
const productQuantities = orders.reduce((result, order) => {
  const { product, quantity } = order;

  if (!result[product]) {
    result[product] = 0;
  }

  result[product] += quantity;
  return result;
}, {});

console.log(productQuantities);
// { "りんご": 4, "バナナ": 7, "オレンジ": 4 }
```

#### 問題6

```javascript
// 分割代入を使用した変数の入れ替え
[a, b, c] = [c, a, b];
console.log(a, b, c); // 3, 1, 2
```

## まとめ

JavaScriptの配列は、データコレクションを操作するための強力なデータ構造です。従来のfor/whileループに頼るよりも、`map`、`filter`、`reduce`などのモダンな配列メソッドを活用することで、より簡潔で読みやすいコードを書くことができます。

スプレッド構文（`...`）や分割代入は、ES2015以降に導入された機能であり、配列操作をさらに簡単な記述にします。これらを使用することで、イミュータブル（不変）なプログラミングスタイルを採用でき、予測可能性と保守性の高いコードを書くことができます。

実際のプロジェクトでは、これらのメソッドを組み合わせて使用することが多く、メソッドチェーンを活用することで、複雑なデータ変換も簡潔に表現できます。

次回は、関数の基本とアロー関数について解説します。JavaScriptにおける関数のさまざまな宣言方法や、アロー関数の特徴と利点について詳しく見ていきます。