JavaScript Tips | 配列ユーティリティ:メモ化配列

JavaScript JavaScript
スポンサーリンク

テーマの整理:「メモ化配列」とは何か

「メモ化配列」は、配列を入力にする処理の結果を“覚えておいて”、同じ入力に対しては再計算せずに結果を返す仕組みです。 さっきの「配列キャッシュ」とほぼ同じ考え方ですが、より「関数レベル」で使いやすくしたものが「メモ化」です。

業務だと、例えば次のような場面で効いてきます。

同じ一覧データに対して、何度も同じフィルタ・ソート・集計をしている。 画面の再描画のたびに、同じ配列処理が何度も走っていて重い。 「この処理は入力が同じなら結果も同じ」という前提がある。

こういうときに「メモ化配列」を使うと、一度計算した結果を再利用できるので、処理が軽く・安定します。

基本コンセプト:配列を入力にする関数を「メモ化」する

「メモ化」とは何かを一度言葉で整理する

メモ化(memoization)は、 「関数の入力と出力の対応を覚えておいて、同じ入力が来たら計算せずに覚えていた出力を返す」テクニックです。

ポイントは、

入力が同じなら出力も同じ(副作用がない)こと。 覚えておくのは「入力 → 出力」のペア。 覚えておく場所は Map やオブジェクトなど。

配列を入力にする場合は、 「配列の中身をどうやって“同じ”と判定するか」が重要になります。

配列版メモ化の一番シンプルな形

初心者向けに分かりやすい形は、 「配列を JSON.stringify して文字列にし、それをキーにする」メモ化です。

function memoizeArray(fn) {
  const cache = new Map();

  return function memoized(array, options = {}) {
    const key = JSON.stringify({ array, options });

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn(array, options);
    cache.set(key, result);
    return result;
  };
}
JavaScript

ここでの役割を整理すると、

fn は「配列を受け取って結果を返す元の関数」。 cache は「キー → 結果」を覚えておく Map。 key は「配列とオプションをまとめて文字列化したもの」。 memoized は「メモ化された関数」で、呼び出すとキャッシュを見てから必要なら計算する。

これが「メモ化配列」の基本形です。

例題 1:配列の合計をメモ化する

シナリオ

売上データの配列があり、 「合計金額」を出す処理が何度も呼ばれる。 同じ配列に対しては、合計を一度だけ計算して覚えておきたい。

コード

function sumAmounts(items) {
  return items.reduce((acc, item) => acc + item.amount, 0);
}

const sumAmountsMemo = memoizeArray(sumAmounts);

const data = [
  { id: 1, amount: 100 },
  { id: 2, amount: 200 },
  { id: 3, amount: 300 },
];

console.log(sumAmountsMemo(data)); // 初回: 計算してキャッシュ
console.log(sumAmountsMemo(data)); // 2回目: キャッシュから取得
JavaScript

ここで起きていることを丁寧に追うと、

1 回目の呼び出しでは、cache にキーがないので sumAmounts が実行される。 計算結果(600)が cache に保存される。 2 回目の呼び出しでは、同じ配列+同じ options なので、cache から 600 が返る。

「同じ配列に対して同じ処理を何度もする」場面では、こういうメモ化が効いてきます。

例題 2:フィルタ処理をメモ化する(条件付き)

シナリオ

ユーザー一覧の配列があり、 「active なユーザーだけを取り出す」処理を何度も呼び出している。 配列と条件の組み合わせでメモ化したい。

コード

function filterUsers(users, options) {
  const { activeOnly } = options;
  if (activeOnly) {
    return users.filter((u) => u.active);
  }
  return users.slice();
}

const filterUsersMemo = memoizeArray(filterUsers);

const users = [
  { id: 1, name: "A", active: true },
  { id: 2, name: "B", active: false },
  { id: 3, name: "C", active: true },
];

console.log(filterUsersMemo(users, { activeOnly: true }));  // 初回: 計算
console.log(filterUsersMemo(users, { activeOnly: true }));  // 2回目: キャッシュ
console.log(filterUsersMemo(users, { activeOnly: false })); // 別条件: 計算
console.log(filterUsersMemo(users, { activeOnly: false })); // 同条件: キャッシュ
JavaScript

ここでは、

キーに「配列」と「options(activeOnly)」をまとめて入れている。 同じ配列+同じ options なら、結果を再利用できる。 違う options なら別のキーになるので、別の結果が計算される。

という構造になっています。

重要ポイント:メモ化配列は「入力が変わらない」前提で使う

配列を書き換えるとメモ化が破綻する

メモ化は、「同じ入力なら同じ結果」という前提で成り立っています。 配列を直接書き換えると、「同じ配列オブジェクトなのに中身が違う」という状態になり、 JSON.stringify をキーにしている場合でも、意図しない結果になることがあります。

例えば、

配列に新しい要素を push した。 既存要素の値を直接変更した。

こういう「破壊的変更」をすると、 「前に計算した結果」と「今欲しい結果」がズレてしまう可能性があります。

イミュータブルな配列とメモ化は相性が良い

React などのフロントエンドでは、 「配列を更新するときは新しい配列を作る(イミュータブル)」というスタイルがよく使われます。

例えば、

新しい要素を追加するときは newArray = [...oldArray, newItem]。 要素を更新するときは newArray = oldArray.map(...)

このように「配列オブジェクト自体が変わる」前提なら、 メモ化配列はかなり安全に使えます。

初心者向けには、

配列を直接書き換えるより、新しい配列を作る習慣をつける。 メモ化は「入力が変わらない間だけ有効」と意識する。

というところを強く意識してほしいです。

もう一歩進んだ形:非同期処理をメモ化する「メモ化配列+Promise」

シナリオ

配列を入力にして、API を叩いて結果を返すような非同期処理がある。 同じ配列に対しては、API を何度も叩きたくない。 結果の Promise をメモ化したい。

実装例

function memoizeArrayAsync(fn) {
  const cache = new Map();

  return async function memoized(array, options = {}) {
    const key = JSON.stringify({ array, options });

    if (cache.has(key)) {
      return cache.get(key);
    }

    const promise = fn(array, options);
    cache.set(key, promise);
    return promise;
  };
}
JavaScript

ここでは、

fn は「配列を受け取って Promise を返す関数」。 キャッシュには「結果そのもの」ではなく「Promise」を保存する。 呼び出し側は await memoized(...) で結果を受け取る。

という形になっています。

例題:ユーザー一覧を API から取得する処理をメモ化する

async function fetchUsersFromApi(ids) {
  console.log("call API with", ids);
  await new Promise((r) => setTimeout(r, 500));
  return ids.map((id) => ({ id, name: `User-${id}` }));
}

const fetchUsersMemo = memoizeArrayAsync(fetchUsersFromApi);

async function main() {
  const ids = [1, 2, 3];

  const users1 = await fetchUsersMemo(ids); // 初回: API 呼び出し
  const users2 = await fetchUsersMemo(ids); // 2回目: キャッシュされた Promise

  console.log(users1);
  console.log(users2);
}

main();
JavaScript

ここでのポイントは、

1 回目は API を叩いて Promise を作り、それをキャッシュする。 2 回目は同じキーなので、キャッシュされた Promise をそのまま返す。 結果として、API は 1 回しか叩かれない。

という動きになっていることです。

メモ化配列を使うときに意識してほしいこと

「この処理はメモ化しても安全か?」を考える

メモ化は便利ですが、 副作用がある処理(ログを書いたり、外部状態を変えたり)には向きません。

考えてほしいポイントは、

入力が同じなら、毎回同じ結果が欲しい処理か。 外部状態に依存していないか(時間・ランダム・グローバル変数など)。 結果が変わるタイミングを自分でコントロールできるか。

これらに「はい」が多いほど、メモ化配列を入れる価値が高いです。

「キャッシュをいつ捨てるか?」を決める

メモ化は「覚えっぱなし」にすると、 メモリを食い続けたり、古い結果を使い続けたりします。

初心者向けには、

まずはスコープを小さくする(画面単位・処理単位で関数を作る)。 必要になったら「キャッシュをクリアする関数」を追加する。

くらいから始めるのがちょうどいいです。

例えば、こういう形もありです。

function createArrayMemo(fn) {
  const cache = new Map();

  function memoized(array, options = {}) {
    const key = JSON.stringify({ array, options });
    if (cache.has(key)) return cache.get(key);
    const result = fn(array, options);
    cache.set(key, result);
    return result;
  }

  memoized.clear = () => cache.clear();

  return memoized;
}
JavaScript

これなら、必要に応じて memoized.clear() でキャッシュを捨てられます。

手を動かして「メモ化配列」の感覚をつかむ

次のコードを実行して、「メモ化あり/なし」の違いをログで体感してみてください。

function memoizeArray(fn) {
  const cache = new Map();

  return function memoized(array, options = {}) {
    const key = JSON.stringify({ array, options });

    if (cache.has(key)) {
      console.log("use memo");
      return cache.get(key);
    }

    console.log("compute");
    const result = fn(array, options);
    cache.set(key, result);
    return result;
  };
}

function sumAmounts(items) {
  return items.reduce((acc, item) => acc + item.amount, 0);
}

const sumAmountsMemo = memoizeArray(sumAmounts);

const data = [
  { id: 1, amount: 100 },
  { id: 2, amount: 200 },
  { id: 3, amount: 300 },
];

console.log("=== memoized ===");
console.log(sumAmountsMemo(data)); // compute
console.log(sumAmountsMemo(data)); // use memo

console.log("=== normal ===");
console.log(sumAmounts(data));     // 毎回計算
console.log(sumAmounts(data));     // 毎回計算
JavaScript

ログを見れば、

メモ化あり:1 回目だけ「compute」、2 回目は「use memo」。 メモ化なし:毎回計算が走る。

という違いがはっきり分かるはずです。

まとめ:メモ化配列ユーティリティで「配列処理のムダな再計算」を減らす

「メモ化配列」は、 「同じ配列に対して何度も同じ処理をするなら、一度だけ計算して覚えておこう」 という、とても素直な発想をコードにしたものです。

プロジェクトに例えば次のような形で置いておくイメージです。

export function memoizeArray(fn) { ... }
export function memoizeArrayAsync(fn) { ... }
JavaScript

そして、

重い配列処理で、同じ入力が何度も来るところにはメモ化を挟む。 入力が変わるタイミングと、キャッシュを捨てるタイミングを意識する。

こう決めておくと、 パフォーマンスが安定し、 「どこで計算していて、どこで結果を使い回しているか」がコードから読み取りやすくなります。

メモ化は“裏技”ではなく、“ちゃんと覚えておく工夫”です。 配列ユーティリティとしてのメモ化配列をうまく使うと、 業務コードがぐっと落ち着いた動き方をするようになります。

タイトルとURLをコピーしました