JavaScript | ES6+ 文法:新データ構造 – GC と Weak 構造

JavaScript JavaScript
スポンサーリンク

まず「GC」とは何か(やさしくイメージから)

JavaScript の世界では、
「もう使われなくなったデータを、自動で片付けてくれる仕組み」 が動いています。
これを GC(Garbage Collection / ガベージコレクション)と呼びます。

たとえば:

let user = { name: "Alice" };
user = null; // もうこのオブジェクトを指している変数はない
JavaScript

{ name: "Alice" } というオブジェクトは、
どこからも参照されていない「ゴミ」になります。

すると、JavaScript エンジンの GC が「こいつ、もう誰からも使われてないな」と判断して、
メモリから自動的に回収してくれます。

ここが重要です。
「どこからも参照されていないもの」は、GC によって勝手に消える
開発者が deletefree みたいなことをする必要はありません。

Strong な参照と Weak な参照の違い

Strong(強い)参照とは

普通に変数に代入したり、配列やオブジェクトの中に入れたりする参照は、
全部「強い参照」です。

const obj = { id: 1 };     // ここで強い参照
const arr = [obj];         // 配列の中からも強い参照
const holder = { value: obj }; // プロパティからも強い参照
JavaScript

どこか 1 つでも「強い参照」が残っている限り、
そのオブジェクトは GC によって消されません。

たとえ「もう使わないつもり」でも、
どこかの変数や構造が参照し続けていると、メモリには残り続けます。
これが「メモリリーク」の原因になります。

Weak(弱い)参照とは

Weak な参照は、
「このオブジェクト、ここからは参照してるけど、もし他から参照がなくなったら、遠慮なく GC で消していいよ」
という性質を持っています。

ES6 の世界で「Weak」という名前がついているのは、

  • WeakMap
  • WeakSet

の 2 つです。

これらの構造は、
「中にオブジェクトを入れておいても、それが 唯一の 参照だとしたら、GC が普通に回収してしまう」
という性質を持っています。

つまり:

  • Strong な構造(配列、オブジェクト、Map、Set):
    参照がある限り GC されない
  • Weak な構造(WeakMap、WeakSet):
    「他からの参照がなくなったオブジェクト」は、
    WeakMap / WeakSet の中にあっても GC されてよい

ここが重要です。
Weak な構造は、「オブジェクトの寿命を一緒に伸ばさないための構造」
「つい保持し続けて、メモリリークの原因になる」のを避けるために存在します。

WeakMap と GC(具体的なイメージ)

WeakMap の基本

WeakMap は「キーをオブジェクトに限定した、弱い Map」です。

const wm = new WeakMap();

let obj = { id: 1 };

wm.set(obj, "追加情報");
JavaScript

ここで wmobj をキーとして何かを覚えています。
でも、これは「弱い参照」です。

obj = null; // これで { id: 1 } への強い参照がなくなった
JavaScript

どこにも { id: 1 } への強い参照が残っていなければ、
GC はこのオブジェクトを回収してOKです。

そのとき、WeakMap の中の entry(キーと値のペア)も、
一緒に「なかったこと」にされます。

なぜこれがうれしいのか

もし同じことを「普通の Map」でやるとどうなるでしょう?

const m = new Map();

let obj = { id: 1 };
m.set(obj, "追加情報");
obj = null;
JavaScript

ここでも obj = null; としていますが、
m の中からは依然として { id: 1 } を参照しているので、
GC はこのオブジェクトを「まだ使われている」と判断します。

つまり、Map に入れている限り、
そのオブジェクトは生き続けてしまうわけです(メモリリークの可能性)。

一方、WeakMap なら「他の参照がなくなれば、WeakMap の中にいても GC される」ため、
そのオブジェクトと紐づいていた値も自動的に消えてくれます。

ここが重要です。
WeakMap は、「オブジェクトに追加情報を紐づけたいけれど、そのオブジェクトの寿命を伸ばしたくはない」場面で使う
だからこそ GC とセットで理解する必要があります。

WeakSet と GC(同じ考え方)

WeakSet の基本

WeakSet は「オブジェクトだけを要素に持つ弱い Set」です。

const ws = new WeakSet();

let obj = { id: 1 };

ws.add(obj); // obj がメンバー

obj = null;  // 他からの参照がなくなると
// GC によって { id: 1 } は回収され、WeakSet からも自動的に消える
JavaScript

WeakSet に入れたオブジェクトも、
他に強い参照がなければ GC の対象になります。

どんなときに使うか(イメージ)

例えば、「あるオブジェクトが既に処理済みかどうか」を覚えておきたいとします。

const processed = new WeakSet();

function process(obj) {
  if (processed.has(obj)) {
    console.log("もう処理済みです");
    return;
  }

  console.log("初回処理をします");
  processed.add(obj);
}
JavaScript

このとき、obj が他から参照されなくなれば、
processed の中の記録も GC によっていつか消えます。

もしこれを普通の Set でやると、
Set がオブジェクトを参照し続けるので、
オブジェクトは永遠にメモリに残り続けるかもしれません。

Weak 構造の「できないこと」=GC と仲良しな代償

ループできない・size もない

WeakMap / WeakSet は、GC と連携するために
「いつ要素が消えるか分からない」前提で設計されています。

そのため、

  • for...of で全部なめる
  • keys() / values() / entries() を呼ぶ
  • size で個数を数える

といった操作は、一切できません。

const wm = new WeakMap();
const ws = new WeakSet();

// これらは全部 NG(メソッドが存在しない or エラー)
// for (const v of ws) {}
// ws.size
// wm.keys()
// wm.forEach(...)
JavaScript

なぜかというと、

「要素の数や一覧を、信頼できる形で提供する」
ということをしてしまうと、

「GC がいつ動くか分からない」という性質と矛盾してしまうからです。

ここが重要です。
Weak 構造は「中身を一覧したり、個数を数えたりするためのものではない」。
あくまで「既知のオブジェクトに対して、付随情報やフラグをくっつけるためのもの」

と理解しておくと、迷いが減ります。

「普通の辞書や集合」としては向いていない

「キーにオブジェクトを使いたいから WeakMap」
「オブジェクトの集合が欲しいから WeakSet」

と安直に選ぶと、すぐに「size がない」「ループできない」といった不便さにぶつかります。

「中身を全部見たい」「何件あるか数えたい」「順番に処理したい」というニーズがあるなら、
普通の Map / Set を使うべきです。

Weak な構造は、「GC と仲良くしたい」特殊な用途にだけ使うものだと思ってください。

具体例で GC と Weak 構造の関係を体感する

例1:DOM 要素に追加情報を紐づける(WeakMap)

const elementData = new WeakMap();

function setElementData(el, data) {
  elementData.set(el, data);
}

function getElementData(el) {
  return elementData.get(el);
}

// どこかで
const div = document.createElement("div");
setElementData(div, { clicked: 0 });

div.addEventListener("click", () => {
  const info = getElementData(div);
  info.clicked++;
  console.log("クリック回数:", info.clicked);
});

// 後で DOM を削除
document.body.removeChild(div);
// div への他の参照がなければ、
// div も、それに紐づく elementData 内のデータも GC によって回収される
JavaScript

ここで WeakMap ではなく Map を使うと、
「DOM は削除されたのに、Map が参照を持ち続けていて GC されない」
というメモリリークが起こり得ます。

例2:循環参照をめぐる訪問済みフラグ(WeakSet)

function printObject(obj, visited = new WeakSet()) {
  if (visited.has(obj)) {
    console.log("[循環参照]");
    return;
  }
  visited.add(obj);

  for (const key in obj) {
    const value = obj[key];
    if (typeof value === "object" && value !== null) {
      printObject(value, visited);
    } else {
      console.log(key, ":", value);
    }
  }
}
JavaScript

visited に「訪問済みのオブジェクト」を入れておけば、
循環参照があっても無限ループを防げます。

この visited が WeakSet であることで、
探索が終わったあと、訪問済み情報が自然に GC の対象になります。

まとめ:GC と Weak 構造の関係を一言でいうと

GC(ガベージコレクション)は、
「どこからも参照されなくなったオブジェクトを、自動的に片付ける仕組み」

Strong な構造(配列 / オブジェクト / Map / Set)は、
参照している限りオブジェクトを生かし続けるので、
油断するとメモリリークの原因になります。

Weak な構造(WeakMap / WeakSet)は、
オブジェクトに「弱い紐づけ」で情報を付けるための道具で、
そのオブジェクトが他から参照されなくなれば、
弱い紐づけごと GC によって回収されます。

押さえておきたいポイントを整理すると:

WeakMap / WeakSet は「GC によって中身が勝手に消えてよい」前提で設計されている
その代わり、sizefor...of など、「全部を見る」機能は提供されない
「オブジェクトに追加情報やフラグを付けたいが、そのせいで寿命を伸ばしたくない」ときに使う
普通の辞書・集合として使いたいなら Map / Set を選ぶ

初心者のうちは、

「まずは Map / Set をちゃんと使いこなす」
「メモリリークや GC に意識が向き始めたら WeakMap / WeakSet を思い出す」

くらいの距離感でちょうどいいです。

そのときは、

「Strong な参照はオブジェクトを生かし続ける」
「Weak な参照は、他に誰もいなければ GC によって消される」

というこの 2 本柱を思い出して、
小さいサンプルで実験してみてください。
GC と Weak 構造の関係が、感覚としてスッと腑に落ちてくるはずです。

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