JavaScript | WeakMap / WeakSet の動作(GCで自動削除)を視覚化

javascrpit JavaScript
スポンサーリンク

実際の ガベージコレクション(GC)をブラウザで強制実行することはできないため、このデモは 「WeakMap / WeakSet の挙動を分かりやすく視覚化して理解するためのシミュレーション」 として作っています。
重要:実際の WeakMap / WeakSet は列挙できず、GC の発生時期も不定です。本デモは「外部参照を切ると WeakMap/WeakSet のエントリは最終的に消える」という概念を 安全に・分かりやすく示すためのものです(ブラウザで確実に消えたことを検証するものではありません)。

以下をブラウザで保存して開いてください — UI でオブジェクトを作り、外部参照を「切る」と(シミュレーションで)自動的にエントリが消えます。右側の「実際の WeakMap/WeakSet」には実際の WeakMap/WeakSet への格納操作も行いますが、可視化は並行して保持する visualMap / visualSet で行っています。

See the Pen WeakMap / WeakSet Behavior (Conceptual Simulation) by MONO365 -Color your days- (@monoqlo365) on CodePen.

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>WeakMap / WeakSet 動作(概念シミュレーション)</title>
<style>
  body { font-family: "Segoe UI",sans-serif; background:#f7fbff; color:#222; padding:20px; }
  h1 { color:#0078d7; }
  .panels { display:flex; gap:20px; flex-wrap:wrap; }
  .panel { flex:1 1 320px; background:#fff; border:2px solid #dceffd; border-radius:10px; padding:12px; box-shadow:0 2px 6px rgba(0,0,0,0.06); }
  .controls { display:flex; gap:8px; margin-bottom:10px; }
  button { background:#0078d7; color:#fff; border:none; padding:8px 10px; border-radius:8px; cursor:pointer; }
  input { padding:8px; border:1px solid #cfe8ff; border-radius:8px; }
  .list { margin-top:8px; min-height:60px; }
  .entry { background:#eef8ff; margin:6px 0; padding:8px; border-radius:8px; display:flex; justify-content:space-between; align-items:center; }
  .muted { color:#666; font-size:0.9em; }
  .badge { padding:4px 8px; border-radius:12px; background:#0078d7; color:#fff; font-size:0.85em; }
  .log { background:#f0fbff; padding:8px; border-left:4px solid #0078d7; max-height:140px; overflow:auto; margin-top:8px; border-radius:6px; }
  .note { margin-top:10px; font-size:0.95em; background:#fffceb; padding:8px; border-left:4px solid #ffcc00; border-radius:6px; }
</style>
</head>
<body>
  <h1>🔎 WeakMap / WeakSet 挙動(概念シミュレーション)</h1>
  <p class="muted">注:実際のガベージコレクションは非決定的で強制できません。このデモは概念を視覚化するためのシミュレーションです。</p>

  <div class="panels">
    <!-- コントロール -->
    <div class="panel">
      <h2>操作パネル</h2>
      <div style="margin-bottom:8px;">
        <input id="objName" placeholder="オブジェクト名(例: domNode1)" />
        <button id="create">オブジェクト作成</button>
      </div>

      <div class="muted">作成したオブジェクト(外部参照):</div>
      <div id="handles" class="list"></div>

      <div style="margin-top:10px;">
        <button id="dropAll">すべての外部参照を切る(simulate)</button>
        <button id="runSimGC">シミュレートされたGCを実行</button>
      </div>

      <div class="note">
        <strong>説明:</strong>
        <ul>
          <li>左の「handles」は<strong>外部参照を保持する変数</strong>の一覧です。ここをクリックするとその参照を切れます( = オブジェクトが他で参照されていない状態をシミュレート)。</li>
          <li>実際の WeakMap / WeakSet は列挙できないため、可視化用に <code>visualMap</code> / <code>visualSet</code> を別に持ち、"GC実行" でそれらを消します。</li>
        </ul>
      </div>
    </div>

    <!-- 可視化パネル -->
    <div class="panel">
      <h2>可視化(visualMap / visualSet)</h2>
      <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
        <input id="vmKey" placeholder="キー (for Map)" />
        <input id="vmVal" placeholder="値 (for Map)" />
        <button id="vmAdd">visualMap に set()</button>
      </div>
      <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px;">
        <input id="vsVal" placeholder="値 (for Set)" />
        <button id="vsAdd">visualSet に add()</button>
      </div>

      <div style="display:flex; gap:12px;">
        <div style="flex:1">
          <div class="muted">visualMap(可視化用・列挙可) <span class="badge">Map</span></div>
          <div id="visualMapList" class="list"></div>
        </div>
        <div style="flex:1">
          <div class="muted">visualSet(可視化用・列挙可) <span class="badge">Set</span></div>
          <div id="visualSetList" class="list"></div>
        </div>
      </div>

      <div style="margin-top:8px;">
        <strong>実際の WeakMap / WeakSet (非列挙)</strong>
        <div class="muted">※内部は列挙不可のため表示できません。操作の様子はログで確認できます。</div>
        <div class="log" id="actionLog"></div>
      </div>
    </div>
  </div>

  <div class="note">
    <strong>重要な注意:</strong>
    <ul>
      <li>本デモでは <code>weakMap</code> / <code>weakSet</code> に実際にオブジェクトを格納しますが、JS 実行環境がオブジェクトを GC するかどうかは制御できません。</li>
      <li>そのため UI 上は <code>visualMap</code> / <code>visualSet</code> を「GC発生で削除される」ものとして扱い、ユーザーが「参照を切る」→「シミュレーションGC」を押すと視覚的に消えます。</li>
    </ul>
  </div>

<script>
/*
  実際の WeakMap / WeakSet と、可視化用 Map / Set を並行して扱う。
  - handles[]: 外部参照を保持する配列。null にすると参照を切ったことになる(シミュレーション)。
  - weakMap / weakSet: 実際にオブジェクトを格納(ただし列挙不可)。
  - visualMap / visualSet: 可視化用にキー/値を保持し、"simulateGC()" で外部参照が切れているものを削除する。
*/

const handles = []; // {id, name, obj}
const weakMap = new WeakMap();
const weakSet = new WeakSet();

const visualMap = new Map(); // keyStr -> {keyRefId, value}
const visualSet = new Map(); // store value -> {refId}

const handlesEl = document.getElementById('handles');
const visualMapList = document.getElementById('visualMapList');
const visualSetList = document.getElementById('visualSetList');
const actionLog = document.getElementById('actionLog');

function log(msg) {
  const now = new Date().toLocaleTimeString();
  actionLog.innerHTML = `[${now}] ${msg}<br>` + actionLog.innerHTML;
}

// オブジェクト作成(外部参照を保持)
document.getElementById('create').addEventListener('click', () => {
  const name = document.getElementById('objName').value.trim() || `obj${handles.length+1}`;
  // 実際のオブジェクト(ここに何でも入れられる)
  const obj = { __demoId: handles.length+1, name };
  const id = handles.length;
  handles.push({ id, name, obj });
  renderHandles();
  log(`Created external ref "${name}" (id=${id})`);
  document.getElementById('objName').value = '';
});

// handles 表示(クリックで参照を切る/復元する)
function renderHandles(){
  handlesEl.innerHTML = '';
  handles.forEach(h => {
    const div = document.createElement('div');
    div.className = 'entry';
    const left = document.createElement('div');
    left.innerHTML = `<strong>${h.name}</strong> <div class="muted">id:${h.id}</div>`;
    const btn = document.createElement('button');
    btn.textContent = h.obj ? '参照を切る (drop)' : '参照を復元 (keep)';
    btn.style.background = h.obj ? '#d9534f' : '#28a745';
    btn.addEventListener('click', () => {
      if (h.obj) {
        // 参照を切る(シミュレーション)
        h.obj = null;
        log(`User dropped external reference for id=${h.id}`);
      } else {
        // 復元(実際は新しいオブジェクトを作り直す)
        const newObj = { __demoId: h.id, name: h.name + '_restored' };
        h.obj = newObj;
        log(`User restored external reference for id=${h.id}`);
      }
      renderHandles();
    });
    div.appendChild(left);
    div.appendChild(btn);
    handlesEl.appendChild(div);
  });
}

// visualMap に set(キーは外部参照の id を使う or 任意文字列)
document.getElementById('vmAdd').addEventListener('click', () => {
  const keyText = document.getElementById('vmKey').value.trim();
  const val = document.getElementById('vmVal').value.trim() || '(empty)';
  if (!keyText) return;
  // キーとして handles の id を指定できる(例: "h:0")
  let keyRef = null;
  const m = keyText.match(/^h:(\d+)$/);
  if (m) {
    const id = Number(m[1]);
    const h = handles[id];
    if (!h) { alert('そのidのハンドルがありません'); return; }
    if (!h.obj) { alert('そのハンドルは参照切り状態です:先に復元するか別キーを使ってください'); return; }
    keyRef = h.obj;
  } else {
    // 文字列キー:文字列をそのままキーにする(可視化用)
    keyRef = keyText;
  }
  // 実際に WeakMap に set(キーがオブジェクトのときのみ)
  if (typeof keyRef === 'object') {
    weakMap.set(keyRef, val);
    log(`weakMap.set(objectKey, "${val}")`);
  } else {
    // object でないキーは WeakMap に入れられない -> skip real weakMap
    log(`(note) non-object key; real WeakMap cannot store primitive key. Only visualMap updated.`);
  }
  // visualMap 用にはキーの文字列化を使って表示管理
  const displayKey = (typeof keyRef === 'object') ? `h:${keyRef.__demoId}` : String(keyRef);
  visualMap.set(displayKey, { keyRefId: (typeof keyRef === 'object' ? keyRef.__demoId : null), value: val });
  renderVisualMap();
  document.getElementById('vmKey').value = '';
  document.getElementById('vmVal').value = '';
});

// visualSet に add(値は任意。外部参照ハンドルをrefIdとして持てる)
document.getElementById('vsAdd').addEventListener('click', () => {
  const v = document.getElementById('vsVal').value.trim();
  if (!v) return;
  // 値として handles のハンドル参照を使う記法: "h:0"
  const m = v.match(/^h:(\d+)$/);
  if (m) {
    const id = Number(m[1]);
    const h = handles[id];
    if (!h) { alert('そのidのハンドルがありません'); return; }
    if (!h.obj) { alert('そのハンドルは参照切り状態です:先に復元するか別値を使ってください'); return; }
    weakSet.add(h.obj);
    visualSet.set(`h:${id}`, { refId: id });
    log(`weakSet.add(object from handle h:${id})`);
  } else {
    // primitive 値は WeakSet に入れられない -> only visualSet
    visualSet.set(v, { refId: null });
    log(`visualSet.add("${v}") (primitive values not allowed in real WeakSet)`);
  }
  renderVisualSet();
  document.getElementById('vsVal').value = '';
});

function renderVisualMap(){
  visualMapList.innerHTML = '';
  for (const [k, v] of visualMap.entries()) {
    const div = document.createElement('div');
    div.className = 'entry';
    const left = document.createElement('div');
    left.innerHTML = `<strong>${k}</strong><div class="muted">${v.value}</div>`;
    const btnWrap = document.createElement('div');
    const btn = document.createElement('button');
    btn.textContent = '削除 (visual)';
    btn.addEventListener('click', () => {
      visualMap.delete(k);
      renderVisualMap();
      log(`visualMap: removed ${k}`);
    });
    btnWrap.appendChild(btn);
    div.appendChild(left);
    div.appendChild(btnWrap);
    visualMapList.appendChild(div);
  }
}

function renderVisualSet(){
  visualSetList.innerHTML = '';
  for (const [k, v] of visualSet.entries()) {
    const div = document.createElement('div');
    div.className = 'entry';
    const left = document.createElement('div');
    left.innerHTML = `<strong>${k}</strong>`;
    const btn = document.createElement('button');
    btn.textContent = '削除 (visual)';
    btn.addEventListener('click', () => {
      visualSet.delete(k);
      renderVisualSet();
      log(`visualSet: removed ${k}`);
    });
    div.appendChild(left);
    div.appendChild(btn);
    visualSetList.appendChild(div);
  }
}

// ユーザー操作:すべての外部参照を切る(handles の obj を null にする)
document.getElementById('dropAll').addEventListener('click', () => {
  handles.forEach(h => { h.obj = null; });
  renderHandles();
  log('User dropped ALL external references (simulated)');
});

// シミュレーションGC:参照が切れた handles に対応する visualMap/visualSet のエントリを削除
document.getElementById('runSimGC').addEventListener('click', () => {
  // visualMap のうち、keyRefId を持つものは handles[id].obj が null なら削除対象
  for (const [k, v] of Array.from(visualMap.entries())) {
    if (v.keyRefId != null) {
      const h = handles[v.keyRefId];
      if (!h || !h.obj) {
        visualMap.delete(k);
        log(`SimGC: visualMap entry ${k} removed (no external refs)`);
      }
    }
  }
  // visualSet similarly
  for (const [k, v] of Array.from(visualSet.entries())) {
    if (v.refId != null) {
      const h = handles[v.refId];
      if (!h || !h.obj) {
        visualSet.delete(k);
        log(`SimGC: visualSet entry ${k} removed (no external refs)`);
      }
    }
  }
  renderVisualMap();
  renderVisualSet();
  log('Simulated GC completed (visualMap/visualSet updated).');
});

</script>
</body>
</html>
HTML

使い方(短く)

  1. オブジェクトを作成(オブジェクト作成)。作成すると handles に外部参照を保持します。
  2. visualMap / visualSet に要素を追加(キーに h:0 のようにハンドル指定するとそのオブジェクトをキー/値として使う)。
  3. ハンドルのボタンで 参照を切る(drop) すると、そのオブジェクトは「外部参照を持たない状態」をシミュレート。
  4. シミュレートされたGCを実行 を押すと、参照が切れた項目が visualMap / visualSet から消えます(これが「GCで自動削除されるイメージ」の視覚化です)。
  5. 実際の WeakMap / WeakSet にはすでに対応する set / add を行っているログも残りますが、実際にブラウザが GC を走らせるかどうかは別問題です。

⚠️ 技術的な注意(改めて)

  • 実際の WeakMap / WeakSet は列挙不可なので、要素の一覧表示はできません(だから visualMap / visualSet を別に持っています)。
  • 実際の GC 発生は JS エンジン任せで、当デモが実際の削除を保証するわけではありません。デモは「概念(参照が切れる → 最終的に消える)」を直感的に学ぶための教材です。
タイトルとURLをコピーしました