WeakSet とは何か(まずイメージから)
WeakSet は、
「オブジェクトだけを入れられる、弱い参照を持つ Set」 です。
普通の Set と似ていますが、決定的に違う点がいくつかあります。
1つ目に、WeakSet に入れられるのは「オブジェクトだけ」です。
数値や文字列などのプリミティブ値は入れられません。
2つ目に、WeakSet に入れたオブジェクトが、他のどこからも参照されなくなったとき、
ガベージコレクション(GC)によってそのオブジェクトがメモリから消えると、
WeakSet 内の要素も自動的に消えてくれます。
つまり WeakSet は、
「特定のオブジェクトを“メンバーかどうか”だけ記録しておきたい。
でも、そのオブジェクトが不要になったら、この記録も一緒に消えてほしい」
という場面で使うための仕組みです。
Set と WeakSet の違い(ここをしっかり)
値として入れられるものの違い
普通の Set は、何でも入れられます。数値、文字列、オブジェクト、配列など何でもOKです。
const s = new Set();
s.add(1);
s.add("hello");
s.add({ id: 1 });
JavaScript一方、WeakSet は「オブジェクトだけ」です。
配列や関数も、オブジェクトなのでOKです。
const ws = new WeakSet();
const obj = { id: 1 };
const arr = [1, 2, 3];
ws.add(obj); // OK
ws.add(arr); // OK
ws.add(1); // エラー:WeakSet に数値は入れられない
ws.add("text"); // エラー:文字列もダメ
JavaScriptプリミティブ値(数値、文字列、boolean、null、undefined、シンボルなど)は WeakSet に入れられない、
この制約はかなり重要です。
自動的に要素が消えることがある(弱い参照)
WeakSet に入っているオブジェクトが、他のどこからも参照されなくなると、
JS エンジンのガベージコレクションが走ったときに、そのオブジェクトはメモリから回収されます。
そのとき、WeakSet も「キーとして保持していたオブジェクト」を手放します。
結果として、WeakSet の中からもその要素が消えます。
コードで雰囲気だけ示します。
let obj = { id: 1 };
const ws = new WeakSet();
ws.add(obj);
// どこかで
obj = null;
// これで { id: 1 } への参照がなくなる(他に参照がなければ)
// しばらくして GC が動いたとき、
// { id: 1 } はメモリから回収され、WeakSet からも自動的に消える
JavaScriptここが重要です。
WeakSet は、「オブジェクトが生きている間だけ」「そのオブジェクトがメンバーかどうか」を覚えておくためのコレクションです。
オブジェクトが死んだら(参照がなくなったら)、自動的に“忘れてくれる”仕組みだと考えてください。
ループできない・サイズも分からない
普通の Set は、for…of で回したり、size を取ったりできます。
const s = new Set([1, 2, 3]);
console.log(s.size); // 3
for (const v of s) console.log(v);
JavaScriptしかし WeakSet は違います。
中身を全部なめるような操作は一切できません。
次のようなことは全部できません。
const ws = new WeakSet();
// for (const v of ws) {} // エラー
// ws.size // プロパティ自体がない
// ws.keys(), ws.values() // これもない
// ws.forEach(...) // これもない
JavaScriptなぜかというと、GC のタイミングはエンジンの自由で、
「いつの間にか要素が消えているかもしれない」ものを一覧させるのは
仕様上矛盾が出るからです。
ここが重要です。
WeakSet には「中身を数えたり、一覧したりする用途で使わない」という前提があります。
「既に知っているオブジェクトが WeakSet に含まれているかどうか」だけを見るための構造です。
WeakSet の基本操作(add / has / delete)
作成と add
const ws = new WeakSet();
const obj1 = {};
const obj2 = {};
ws.add(obj1);
ws.add(obj2);
JavaScript同じオブジェクトを何度 add しても、一度だけしか入らない点は Set と同じです。
has で存在チェック
console.log(ws.has(obj1)); // true
console.log(ws.has({})); // false(別オブジェクト)
JavaScriptここで大事なのは、オブジェクトは「同じ中身かどうか」ではなく、「同じもの(参照)かどうか」で判定されるということです。
delete で削除
ws.delete(obj1);
console.log(ws.has(obj1)); // false
JavaScriptdelete を手動で呼ばなくても、
「obj1 をどこからも参照していない」状態になれば、GC によって WeakSet 内のエントリも自動で消えます。
手動で消すのは「このオブジェクトはまだどこかで使うけど、WeakSet からだけ除外しておきたい」ときです。
WeakSet が役立つ典型パターン
パターン1:特定のオブジェクトを「一度処理したかどうか」覚えておく
例えば、複数回渡される可能性のあるオブジェクトに対して、
「まだ処理したことがなければ初回処理をする」「二回目以降ならスキップする」という場面。
const processed = new WeakSet();
function process(obj) {
if (processed.has(obj)) {
console.log("このオブジェクトはすでに処理済みです");
return;
}
console.log("初回処理をします");
processed.add(obj);
// 何か重い処理...
}
const a = {};
const b = {};
process(a); // 初回処理をします
process(a); // このオブジェクトはすでに処理済みです
process(b); // 初回処理をします
// どこかで a をもう参照しなくなったら、
// GC によって a と、それに紐づく WeakSet の情報も消える
JavaScriptSet で同じことをすると、processed がオブジェクトをずっと保持し続けるため、
本当は不要になっているオブジェクトまで残り続けることがあります。
WeakSet なら、「オブジェクトの寿命が終わったら一緒に消えてくれる」のでメモリリークを防ぎやすくなります。
パターン2:DOM 要素が「イベント登録済みかどうか」を覚えておく
DOM 要素にイベントを二重登録したくない場合があります。
const registered = new WeakSet();
function ensureClickHandler(el) {
if (registered.has(el)) {
return; // すでに登録済み
}
el.addEventListener("click", () => {
console.log("クリックされました");
});
registered.add(el);
}
JavaScriptここでも、
ある DOM 要素が削除されて、どこからも参照されなくなったら、
WeakSet の中のエントリも一緒に消えます。
「登録済みかどうか」の情報だけを、DOM 要素の寿命に合わせて持ちたい、
というときに WeakSet がぴったりハマります。
パターン3:すでに見たオブジェクトを記録して「循環参照」を検出する
オブジェクトグラフを再帰的にたどる処理で、
同じオブジェクトにもう一度来たら無限ループになることがあります。
それを避けるために「訪問済み」を記録する役として 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訪問済みオブジェクトを WeakSet に入れておくことで、
循環参照にぶつかったときに検出できます。
Visited 用の Set を普通の Set で持っても動きますが、
処理が終わったあとにオブジェクトを参照し続ける必要がないなら、
WeakSet を使うほうがメモリリークのリスクを下げられます。
初心者向けの「ここだけは押さえたい注意点」
WeakSet は「数えない・一覧しない」
WeakSet の中に「いくつ要素があるか」「何が入っているか」を知る術はありません。
仕様として、そういう使い方をさせない設計になっています。
したがって、
「今何個登録されているかで処理を分けたい」
「中身を全部出して別の場所にコピーしたい」
といった用途には向きません。
そういうときは迷わず普通の Set を使うべきです。
単なる「オブジェクト専用 Set」が欲しいだけなら、WeakSet は選ばない
「オブジェクトだけを入れるセットが欲しいから WeakSet かな?」
という発想になりがちですが、
中身をループしたい、サイズを知りたい、デバッグで中身を見たい、といった願望がある場合、
WeakSet を選ぶとすぐに不便さにぶつかります。
「オブジェクト専用だけど、ふつうに集合として扱いたい」のなら、
普通の Set を使い、自分側で「必ずオブジェクトだけを add する」とルール化したほうが現実的です。
WeakSet は、メモリ管理や GC の挙動まで意識し始めたときに初めて真価を発揮する「玄人向けの道具」に近いです。
初心者のうちは、「そういうものがある」ことを知っておけば十分で、
積極的に使う必要はほとんどありません。
まとめ
WeakSet の核心は、
「オブジェクトだけを要素として持ち、オブジェクトが不要になったら、自動的に WeakSet 側の記録も消えてくれる“弱い集合”」
という点です。
押さえておきたいポイントをまとめると、次のようになります。
WeakSet に入れられるのはオブジェクト(配列・関数含む)だけで、数値や文字列は入れられない。
オブジェクトが他で参照されなくなり GC されたら、WeakSet 内からも自動的に消える。
ループや size、keys/values などの機能は一切なく、「含まれているかどうか」を has で調べる用途に限られる。
「あるオブジェクトがすでに処理済みか」「イベント登録済みか」など、「フラグ的な情報」をオブジェクトの寿命に合わせて持ちたいときに役立つ。
単に「集合として扱いたい」「中身を見たい」という場合は、普通の Set を選ぶべき。
まずは Set をしっかり使えるようになってから、
「このオブジェクトにフラグを付けたいけど、メモリリークさせたくない」という場面に出会ったとき、
WeakSet を思い出してみてください。
そのときは、小さなサンプルを書きながら、
「参照を切ると WeakSet の has がいつ false になるのか」を観察してみると、
GC と WeakSet の関係が体感として掴めてきます。
