JavaScript | 配列・オブジェクト:パフォーマンス・設計 – メモリ参照の注意点

JavaScript JavaScript
スポンサーリンク

メモリ参照とは何か

JavaScript の配列・オブジェクトは「参照型」です。ここが重要です:変数には“値そのもの”ではなく“値への参照(ポインタのようなもの)”が入ります。参照が同じだと、片方を変更したときもう片方も変わって見えます。これが「共有参照」で、初心者が最初に踏みがちな落とし穴です。

const a = { n: 1 };
const b = a;        // a と b は“同じオブジェクト”を参照
b.n = 2;
console.log(a.n);   // 2(bの変更がaにも見える)
JavaScript

浅いコピーと深いコピー(どこまで独立させるか)

浅いコピー(外側だけ新しく、内側は共有)

スプレッドや Object.assign は“1階層目”だけ新しくします。ネスト内は参照共有のままです。

const orig = { user: { name: "Alice" }, tags: ["js"] };
const shallow = { ...orig };        // 浅いコピー
shallow.user.name = "Bob";
console.log(orig.user.name);        // "Bob"(内側は共有だった)
JavaScript

ここが重要です:浅いコピーは「外側の箱を新しくするだけ」。ネストに入ると同じ中身を指します。

深いコピー(最深部まで完全に独立)

structuredClone はネスト全体を複製します。元とコピーを完全に切り離せます。

const orig = { user: { name: "Alice" }, tags: ["js"] };
const deep = structuredClone(orig); // 深いコピー
deep.user.name = "Carol";
console.log(orig.user.name);        // "Alice"(独立)
JavaScript

ここが重要です:深いコピーは安全ですが、重い処理です。常時使うのではなく「本当に元と切り離したい場面」に限定するのが設計のコツです。


共有参照によるバグ(エイリアシング問題)

“別名”で同じものを触ってしまう

別変数・別モジュール・別関数から“同じ実体”に触ると予期せぬ影響が出ます。これがエイリアシング(別名)問題。

function addTag(item, tag) {
  item.tags.push(tag);   // 破壊的(他所でも影響)
}
const a = { tags: ["a"] };
const b = a;
addTag(b, "b");
console.log(a.tags);     // ["a","b"](aにも反映)
JavaScript

ここが重要です:関数は“引数を壊さない”契約にする(非破壊更新)と、参照共有の副作用を抑えられます。

function addTagSafe(item, tag) {
  return { ...item, tags: [...(item.tags ?? []), tag] };
}
JavaScript

値の同一性と等価(=== と “同じ中身”の違い)

同一性(参照が同じか)と値等価(中身が同じか)

=== は「同じ参照か」を見ます。中身が同じでも“別インスタンス”なら === は false です。

const x = { n: 1 };
const y = { n: 1 };
console.log(x === y); // false(別インスタンス)
const z = x;
console.log(x === z); // true(同じ参照)
JavaScript

ここが重要です:差分検知(UI)では“参照が変わったか”が有効です。中身の比較は重いので、基本は“必要箇所だけ新インスタンス”にして同一性の変化で検知します。


安全な更新の型(必要階層だけコピーする)

ネストの部分更新(階層ごとに新しくする)

更新対象の階層だけスプレッドで再構築し、その他の参照は保つと効率と安全のバランスが取れます。

const state = { user: { profile: { city: "Tokyo" }, flags: { vip: false } } };
const next = {
  ...state,
  user: {
    ...state.user,
    flags: { ...state.user.flags, vip: true } // ここだけ更新
  }
};
JavaScript

ここが重要です:一段でもコピーを省くと参照共有が残ります。必要な階層“だけ”新しくするのがコツ。


パフォーマンス視点の参照管理(コピーしすぎない)

毎回全体を深コピーしない

深コピーは重く、メモリも増えます。変更が必要な部分のみ再構築し、残りは参照を保つことで速度・メモリを節約します。

// 変更箇所だけ再構築して他は参照維持
const next = { 
  ...state, 
  prefs: { ...state.prefs, theme: "dark" } 
};
JavaScript

中間配列を減らす(1パスで出力を作る)

filter→map のチェーンは中間配列を生みます。大規模では 1 回の走査で結果だけ作ると GC の負荷が減ります。

function filterMap(arr, pred, mapFn) {
  const out = [];
  for (const x of arr) if (pred(x)) out.push(mapFn(x));
  return out;
}
JavaScript

参照を前提にした設計(辞書化・正規化)

検索多発は辞書化して参照一本化

配列から辞書(ID→レコード)を作ると、更新は一点集中・参照は O(1) になります。表示時に必要なら配列化。

const dict = Object.fromEntries(list.map(x => [String(x.id), x]));
const nextDict = { ...dict, ["123"]: { ...dict["123"], active: true } };
const nextList = Object.values(nextDict);
JavaScript

ここが重要です:同じレコードを複数箇所に持たず、“唯一の更新源”に集約すると参照の不整合が起きにくい。


参照とガベージコレクション(メモリリーク防止)

参照が残る限りメモリは解放されない

不要になったオブジェクトへの参照を持ち続けると GC されず、メモリリークになります。長寿コレクション(キャッシュ・グローバル配列)に“古い参照”を残さない設計が必要です。

// 悪い例:使い終わったら辞書から消さない
cache["old"] = bigObject;  // ずっと参照が残る

// 良い例:期限や条件で明示的に参照を切る
delete cache["old"];       // 参照がなくなれば GC 対象になる
JavaScript

WeakMap / WeakSet で“弱い参照”を使う

キーが GC で消えると自動的に解放される“弱い参照”は、付随情報のメモ化や訪問済み管理に向きます。

const seen = new WeakSet();
function walk(obj) {
  if (obj == null || typeof obj !== "object") return;
  if (seen.has(obj)) return;
  seen.add(obj);
  for (const v of Object.values(obj)) walk(v);
}
JavaScript

ここが重要です:WeakMap/WeakSet は“キーがオブジェクトに限る”“列挙できない”という制約がありますが、リーク防止に強い味方です。


クロージャと参照(関数が“環境”を持つ)

閉じ込めた参照は生き続ける

関数が外のオブジェクトをキャプチャすると、その関数が生きている限り参照も生きます。大きなデータを不用意に閉じ込めない。

function makeCounter(state) {
  let count = 0;                 // 小さい環境なら安全
  return () => ++count;
}
JavaScript

ここが重要です:イベントリスナーなど長寿命の関数に“大きい配列やDOM”を閉じ込めるとリークの原因になります。必要データだけ渡す、解除する(removeEventListener)などの運用が大切です。


等価チェックとキャッシュ(参照で速くする)

参照同一性でメモ化・差分検知

参照が変わらなければ「内容が変わっていない」とみなせる設計にすると、高速な === 比較で再計算・再描画を省けます。

const cache = new Map();
function expensive(input) {
  const hit = cache.get(input);
  if (hit) return hit;
  const out = compute(input);
  cache.set(input, out);
  return out;
}
JavaScript

ここが重要です:非破壊更新で“変わった所だけ新インスタンス”にすると、この戦略が機能します。


まとめ

メモリ参照の核心は「配列・オブジェクトは参照型で、共有参照が副作用の源」という理解です。浅いコピーは内側が共有、深いコピーは安全だが重い。更新は“必要階層だけコピー”し、辞書化で唯一の更新源に集約、filter→map は 1 パスで中間配列を減らす。不要参照は切って GC を促し、弱い参照(WeakMap/WeakSet)で長寿構造のリークを防ぐ。参照同一性を活かした差分検知・メモ化を設計に織り込めば、初心者でも安全で速いメモリ運用ができます。

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