T-CREATOR

javascript関数型で利用できる配列操作メソッドまとめ

javascript関数型で利用できる配列操作メソッドまとめ

モダンな JavaScript 開発において、配列操作は日常的なタスクです。データの加工、検索、集計など、さまざまな操作が必要になりますが、これらを関数型プログラミングのアプローチで行うことで、コードの可読性や保守性が大きく向上します。

今回は、JavaScript で利用できる関数型スタイルの配列操作メソッドを機能別に分類して詳しく解説します。命令型から関数型へのシフトで、あなたのコードがどれだけエレガントになるかを体感してください。

背景:JavaScript における関数型プログラミングの発展

JavaScript は元々、手続き型と関数型の両方の特性を持つマルチパラダイム言語として設計されていました。しかし、初期の JavaScript には関数型プログラミングをサポートする機能は限られていました。

ES5(ECMAScript 5)の登場により、forEachmapfilterreduce などの高階関数が標準ライブラリに追加され、関数型プログラミングの基盤が整いました。

javascript// ES5以前の命令型スタイル
var numbers = [1, 2, 3, 4, 5];
var doubled = [];
for (var i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}

// ES5以降の関数型スタイル
var doubled = numbers.map(function (num) {
  return num * 2;
});

さらに ES6(ECMAScript 2015)ではアロー関数の導入により、より簡潔な関数表現が可能になりました。

javascript// ES6のアロー関数を使った関数型スタイル
const doubled = numbers.map((num) => num * 2);

これらの進化により、JavaScript での関数型プログラミングはより自然で実用的なものになってきました。

課題:従来の命令型プログラミングの限界

従来の命令型プログラミング(手続き型)には、いくつかの課題があります。

  1. 状態の管理が複雑になりがち

    ループや条件分岐が多くなると、変数の状態管理が複雑になり、バグの温床となります。

  2. 副作用が生じやすい

    外部の状態を変更する操作が多いと、予測不能な動作を引き起こします。

  3. テストの難しさ

    状態変更が多いコードはテストが難しく、カバレッジを確保するのが大変です。

  4. 可読性の低下

    複雑なループやネストした条件分岐は、コードの可読性を低下させます。

例えば、以下のようなコードを見てみましょう:

javascript// 命令型:年齢が20以上のユーザーを取得し、名前だけの配列にする
const users = [
  { name: '田中', age: 28 },
  { name: '佐藤', age: 18 },
  { name: '鈴木', age: 32 },
  { name: '高橋', age: 19 },
];

const adultNames = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].age >= 20) {
    adultNames.push(users[i].name);
  }
}
console.log(adultNames); // ['田中', '鈴木']

これが関数型アプローチではどう変わるのか、次のセクションから見ていきましょう。

変換系メソッド

map

map メソッドは、配列の各要素に関数を適用し、その結果から新しい配列を作成します。元の配列は変更されません。

基本構文:

javascriptconst newArray = array.map((element, index, array) => {
  /* ... */
});

実践例:

javascript// 数値の配列を2倍にする
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((num) => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// オブジェクトから特定のプロパティだけを抽出
const users = [
  { id: 1, name: '田中', age: 28 },
  { id: 2, name: '佐藤', age: 18 },
  { id: 3, name: '鈴木', age: 32 },
];
const names = users.map((user) => user.name);
console.log(names); // ['田中', '佐藤', '鈴木']

flatMap

flatMapmapflat を組み合わせたメソッドで、各要素をマッピング関数で処理した結果を 1 レベル平坦化します。配列の配列を扱う際に特に便利です。

基本構文:

javascriptconst newArray = array.flatMap((element, index, array) => {
  /* ... */
});

実践例:

javascript// 文章を単語に分割し、さらに平坦化
const sentences = ['こんにちは 世界', 'JavaScript 最高'];
const words = sentences.flatMap((sentence) =>
  sentence.split(' ')
);
console.log(words); // ['こんにちは', '世界', 'JavaScript', '最高']

// 配列内の奇数を2倍にし、偶数は取り除く
const numbers = [1, 2, 3, 4, 5];
const result = numbers.flatMap((num) =>
  num % 2 === 0 ? [] : [num * 2]
);
console.log(result); // [2, 6, 10]

Array.from

Array.from はイテラブルや array-like オブジェクトから新しい配列インスタンスを生成します。第二引数にマッピング関数を渡すこともできます。

基本構文:

javascriptconst newArray = Array.from(
  iterableOrArrayLike,
  mapFn,
  thisArg
);

実践例:

javascript// 文字列から配列を作成
const str = 'こんにちは';
const chars = Array.from(str);
console.log(chars); // ['こ', 'ん', 'に', 'ち', 'は']

// 同時にマッピング関数を適用
const numbers = '12345';
const digits = Array.from(numbers, (n) => parseInt(n, 10));
console.log(digits); // [1, 2, 3, 4, 5]

// Set から配列を作成
const set = new Set([1, 2, 2, 3, 4, 4]);
const uniqueArray = Array.from(set);
console.log(uniqueArray); // [1, 2, 3, 4]

選別系メソッド

filter

filter メソッドは、指定された条件を満たす要素だけを含む新しい配列を作成します。

基本構文:

javascriptconst newArray = array.filter((element, index, array) => {
  /* 条件式 */
});

実践例:

javascript// 偶数だけをフィルタリング
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter((num) => num % 2 === 0);
console.log(evenNumbers); // [2, 4, 6]

// 成人だけをフィルタリング
const users = [
  { name: '田中', age: 28 },
  { name: '佐藤', age: 18 },
  { name: '鈴木', age: 32 },
];
const adults = users.filter((user) => user.age >= 20);
console.log(adults); // [{ name: '田中', age: 28 }, { name: '鈴木', age: 32 }]

find / findIndex

find メソッドは条件を満たす最初の要素を返し、findIndex はその要素のインデックスを返します。

基本構文:

javascriptconst element = array.find((element, index, array) => {
  /* 条件式 */
});
const index = array.findIndex((element, index, array) => {
  /* 条件式 */
});

実践例:

javascript// 特定のIDを持つユーザーを検索
const users = [
  { id: 1, name: '田中' },
  { id: 2, name: '佐藤' },
  { id: 3, name: '鈴木' },
];
const user = users.find((user) => user.id === 2);
console.log(user); // { id: 2, name: '佐藤' }

// 特定の値のインデックスを検索
const fruits = ['りんご', 'バナナ', 'オレンジ', 'イチゴ'];
const index = fruits.findIndex(
  (fruit) => fruit === 'オレンジ'
);
console.log(index); // 2

some / every

some は少なくとも 1 つの要素が条件を満たすかを判定し、every はすべての要素が条件を満たすかを判定します。

基本構文:

javascriptconst hasAny = array.some((element, index, array) => {
  /* 条件式 */
});
const hasAll = array.every((element, index, array) => {
  /* 条件式 */
});

実践例:

javascript// 配列に偶数が含まれているか確認
const numbers = [1, 3, 5, 6, 7];
const hasEven = numbers.some((num) => num % 2 === 0);
console.log(hasEven); // true

// すべてのユーザーが成人か確認
const users = [
  { name: '田中', age: 28 },
  { name: '佐藤', age: 18 },
  { name: '鈴木', age: 32 },
];
const allAdults = users.every((user) => user.age >= 20);
console.log(allAdults); // false

集約系メソッド

reduce / reduceRight

reduce は配列の全要素に対して関数を実行し、単一の結果値にまとめます。reduceRight は右から左に処理する点が異なります。

基本構文:

javascriptconst result = array.reduce(
  (accumulator, currentValue, index, array) => {
    /* 処理 */
    return updatedAccumulator;
  },
  initialValue
);

実践例:

javascript// 合計値の計算
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce(
  (acc, current) => acc + current,
  0
);
console.log(sum); // 15

// オブジェクトへの変換(IDをキーにする)
const users = [
  { id: 1, name: '田中' },
  { id: 2, name: '佐藤' },
  { id: 3, name: '鈴木' },
];
const userMap = users.reduce((acc, user) => {
  acc[user.id] = user;
  return acc;
}, {});
console.log(userMap);
/* 出力:
{
  1: { id: 1, name: '田中' },
  2: { id: 2, name: '佐藤' },
  3: { id: 3, name: '鈴木' }
}
*/

// グループ化
const people = [
  { name: '田中', age: 28 },
  { name: '佐藤', age: 18 },
  { name: '鈴木', age: 32 },
  { name: '高橋', age: 28 },
];
const groupByAge = people.reduce((acc, person) => {
  const age = person.age;
  if (!acc[age]) {
    acc[age] = [];
  }
  acc[age].push(person);
  return acc;
}, {});
console.log(groupByAge);
/* 出力:
{
  18: [{ name: '佐藤', age: 18 }],
  28: [{ name: '田中', age: 28 }, { name: '高橋', age: 28 }],
  32: [{ name: '鈴木', age: 32 }]
}
*/

join

join メソッドは配列のすべての要素を指定した区切り文字で連結した文字列を返します。

基本構文:

javascriptconst str = array.join(separator);

実践例:

javascript// 単純な連結
const fruits = ['りんご', 'バナナ', 'オレンジ'];
const fruitList = fruits.join(', ');
console.log(fruitList); // 'りんご, バナナ, オレンジ'

// パスの生成
const pathParts = ['users', 'profile', '123'];
const path = pathParts.join('/');
console.log(path); // 'users/profile/123'

// CSV形式データの生成
const rows = [
  ['名前', '年齢', '職業'],
  ['田中', 28, 'エンジニア'],
  ['佐藤', 35, 'デザイナー'],
];
const csv = rows.map((row) => row.join(',')).join('\n');
console.log(csv);
/* 出力:
名前,年齢,職業
田中,28,エンジニア
佐藤,35,デザイナー
*/

探索系メソッド

indexOf / lastIndexOf

indexOf は配列内で指定された要素が最初に現れるインデックスを返し、lastIndexOf は最後に現れるインデックスを返します。

基本構文:

javascriptconst firstIndex = array.indexOf(searchElement, fromIndex);
const lastIndex = array.lastIndexOf(
  searchElement,
  fromIndex
);

実践例:

javascript// 要素の検索
const fruits = [
  'りんご',
  'バナナ',
  'オレンジ',
  'バナナ',
  'イチゴ',
];
const firstBanana = fruits.indexOf('バナナ');
const lastBanana = fruits.lastIndexOf('バナナ');
console.log(firstBanana); // 1
console.log(lastBanana); // 3

// 存在確認(古い方法)
const hasGrape = fruits.indexOf('ぶどう') !== -1;
console.log(hasGrape); // false

includes

includes メソッドは配列に特定の要素が含まれているかどうかを判定します。

基本構文:

javascriptconst hasElement = array.includes(searchElement, fromIndex);

実践例:

javascript// 要素の存在確認
const fruits = ['りんご', 'バナナ', 'オレンジ'];
const hasBanana = fruits.includes('バナナ');
const hasGrape = fruits.includes('ぶどう');
console.log(hasBanana); // true
console.log(hasGrape); // false

// 複数の条件の存在確認
const checkFruits = (fruit) => {
  const availableFruits = ['りんご', 'バナナ', 'オレンジ'];
  return availableFruits.includes(fruit)
    ? `${fruit}は在庫があります`
    : `${fruit}は在庫がありません`;
};
console.log(checkFruits('バナナ')); // 'バナナは在庫があります'
console.log(checkFruits('ぶどう')); // 'ぶどうは在庫がありません'

イミュータブル操作

concat

concat メソッドは、呼び出し元の配列に引数で指定された配列や値を結合した新しい配列を返します。

基本構文:

javascriptconst newArray = array.concat(value1, value2, ..., valueN);

実践例:

javascript// 配列の結合
const fruits1 = ['りんご', 'バナナ'];
const fruits2 = ['オレンジ', 'イチゴ'];
const allFruits = fruits1.concat(fruits2);
console.log(allFruits); // ['りんご', 'バナナ', 'オレンジ', 'イチゴ']

// 値と配列の結合
const numbers = [1, 2];
const moreNumbers = numbers.concat(3, [4, 5], [[6]]);
console.log(moreNumbers); // [1, 2, 3, 4, 5, [6]]

slice

slice メソッドは、配列の一部を抽出して新しい配列として返します。

基本構文:

javascriptconst newArray = array.slice(beginIndex, endIndex);

実践例:

javascript// 配列の一部を抽出
const fruits = [
  'りんご',
  'バナナ',
  'オレンジ',
  'イチゴ',
  'ぶどう',
];
const someFruits = fruits.slice(1, 4);
console.log(someFruits); // ['バナナ', 'オレンジ', 'イチゴ']

// 配列のコピー
const fruitsCopy = fruits.slice();
console.log(fruitsCopy); // ['りんご', 'バナナ', 'オレンジ', 'イチゴ', 'ぶどう']

// 末尾からの抽出
const lastTwo = fruits.slice(-2);
console.log(lastTwo); // ['ぶどう', 'イチゴ']

スプレッド演算子

スプレッド演算子 (...) は、イテラブルを個々の要素に展開します。

基本構文:

javascriptconst newArray = [...iterableObj, ...iterableObj2];

実践例:

javascript// 配列の結合
const fruits1 = ['りんご', 'バナナ'];
const fruits2 = ['オレンジ', 'イチゴ'];
const allFruits = [...fruits1, ...fruits2];
console.log(allFruits); // ['りんご', 'バナナ', 'オレンジ', 'イチゴ']

// 配列のコピー
const fruitsCopy = [...fruits1];
console.log(fruitsCopy); // ['りんご', 'バナナ']

// 配列の途中に要素を挿入
const inserting = [
  ...fruits1.slice(0, 1),
  'マンゴー',
  ...fruits1.slice(1),
];
console.log(inserting); // ['りんご', 'マンゴー', 'バナナ']

// オブジェクト配列の深いコピー(要注意:1レベルのみの浅いコピー)
const users = [
  { id: 1, name: '田中' },
  { id: 2, name: '佐藤' },
];
const usersCopy = users.map((user) => ({ ...user }));
usersCopy[0].name = '山田';
console.log(users[0].name); // '田中'(変更されていない)
console.log(usersCopy[0].name); // '山田'

実践的な活用例

命令型と関数型の比較

先ほどの「20 歳以上のユーザーの名前だけを取り出す」処理を関数型で書き直すと:

javascript// 命令型
const users = [
  { name: '田中', age: 28 },
  { name: '佐藤', age: 18 },
  { name: '鈴木', age: 32 },
  { name: '高橋', age: 19 },
];
const adultNames = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].age >= 20) {
    adultNames.push(users[i].name);
  }
}

// 関数型
const adultNames = users
  .filter((user) => user.age >= 20)
  .map((user) => user.name);
console.log(adultNames); // ['田中', '鈴木']

関数型では、コードが宣言的になり、「何をするか」だけが表現されています。

複雑なデータ変換

実際のアプリケーションでは、より複雑なデータ変換が必要になることが多いです。

javascript// ユーザーデータを年齢グループ別にまとめ、各グループの平均年齢を計算する
const users = [
  { id: 1, name: '田中', age: 28, gender: '男性' },
  { id: 2, name: '佐藤', age: 18, gender: '女性' },
  { id: 3, name: '鈴木', age: 32, gender: '男性' },
  { id: 4, name: '高橋', age: 45, gender: '女性' },
  { id: 5, name: '山田', age: 22, gender: '女性' },
  { id: 6, name: '井上', age: 37, gender: '男性' },
];

// ステップ1: 年齢グループごとに分類(20代、30代、40代)
const ageGroups = users.reduce((acc, user) => {
  const decade = Math.floor(user.age / 10) * 10;
  const group = `${decade}代`;

  if (!acc[group]) {
    acc[group] = [];
  }

  acc[group].push(user);
  return acc;
}, {});

// ステップ2: 各グループの平均年齢とメンバー数を計算
const groupStats = Object.entries(ageGroups).map(
  ([group, members]) => {
    const totalAge = members.reduce(
      (sum, user) => sum + user.age,
      0
    );
    const averageAge =
      Math.round((totalAge / members.length) * 10) / 10;
    const maleCount = members.filter(
      (user) => user.gender === '男性'
    ).length;
    const femaleCount = members.filter(
      (user) => user.gender === '女性'
    ).length;

    return {
      group,
      memberCount: members.length,
      averageAge,
      genderRatio: {
        male: maleCount,
        female: femaleCount,
      },
      members: members.map((m) => m.name),
    };
  }
);

console.log(groupStats);
/* 出力例:
[
  {
    group: '10代',
    memberCount: 1,
    averageAge: 18,
    genderRatio: { male: 0, female: 1 },
    members: ['佐藤']
  },
  {
    group: '20代',
    memberCount: 2,
    averageAge: 25,
    genderRatio: { male: 1, female: 1 },
    members: ['田中', '山田']
  },
  // ...以下同様
]
*/

パイプライン処理

複数の操作を連鎖させることで、データの流れを作ることができます。

javascript// 購入データから月別の売上レポートを生成する
const purchases = [
  {
    id: 1,
    product: 'ノートPC',
    price: 85000,
    date: '2023-01-15',
  },
  {
    id: 2,
    product: 'スマートフォン',
    price: 62000,
    date: '2023-01-28',
  },
  {
    id: 3,
    product: 'ヘッドフォン',
    price: 25000,
    date: '2023-02-03',
  },
  {
    id: 4,
    product: 'キーボード',
    price: 8500,
    date: '2023-02-14',
  },
  {
    id: 5,
    product: 'マウス',
    price: 4300,
    date: '2023-03-01',
  },
  {
    id: 6,
    product: 'モニター',
    price: 45000,
    date: '2023-03-15',
  },
  {
    id: 7,
    product: 'タブレット',
    price: 42000,
    date: '2023-01-05',
  },
];

// 月別にデータを加工するパイプライン
const monthlySalesReport = purchases
  // 日付を月情報に変換
  .map((purchase) => ({
    ...purchase,
    month: purchase.date.substring(0, 7), // 'YYYY-MM'形式に
  }))
  // 月ごとにグループ化
  .reduce((acc, purchase) => {
    if (!acc[purchase.month]) {
      acc[purchase.month] = [];
    }
    acc[purchase.month].push(purchase);
    return acc;
  }, {})
  // 月ごとの集計を行う
  .map(([month, items]) => {
    const totalSales = items.reduce(
      (sum, item) => sum + item.price,
      0
    );
    const itemCount = items.length;
    const averagePrice = Math.round(totalSales / itemCount);

    return {
      month,
      totalSales,
      itemCount,
      averagePrice,
      items: items.map((item) => item.product),
    };
  })
  // 売上高の降順でソート
  .sort((a, b) => b.totalSales - a.totalSales);

console.log(monthlySalesReport);
/* 出力例:
[
  {
    month: '2023-01',
    totalSales: 189000,
    itemCount: 3,
    averagePrice: 63000,
    items: ['ノートPC', 'スマートフォン', 'タブレット']
  },
  // ...以下同様
]
*/

まとめ

JavaScript の関数型配列操作メソッドは、コードをより宣言的で理解しやすく、メンテナンスしやすくするための強力なツールです。

本記事で紹介したメソッドを整理すると:

  • 変換系map, flatMap, Array.from - データを新しい形式に変換
  • 選別系filter, find​/​findIndex, some​/​every - 条件に基づいてデータを選別
  • 集約系reduce​/​reduceRight, join - データを集約して単一の結果にまとめる
  • 探索系indexOf​/​lastIndexOf, includes - データ内の要素を探索
  • イミュータブル操作concat, slice, スプレッド演算子 - 元のデータを変更せずに新しいデータを生成

これらのメソッドを組み合わせることで、複雑なデータ変換も宣言的に、そして元のデータを変更せずに行うことができます。それは堅牢なアプリケーション開発において非常に重要です。

関数型アプローチを採用することで、副作用を減らし、テストしやすく、理解しやすいコードを書くことができます。特に複雑なデータ処理が必要なモダンな Web アプリケーション開発において、こうした関数型のパターンは非常に価値があります。

ぜひ日常のコーディングで、これらのメソッドを積極的に活用してみてください。命令型から関数型へのシフトは、コードの質と開発者体験を大きく向上させるでしょう。

関連リンク