ジャンルから探す

Web Design English

JavaScriptの新しいメソッドWeakMapの新機能から活用例まで

WeakMap は、「オブジェクトに付属情報を持たせたいが、そのオブジェクトが不要になったら情報も一緒に消えてほしい」ときに使うコレクションです。メソッドは set / get / has / delete の4つだけで、これは今も変わっていません。

この記事は2024年の公開後、2026年9月に全面的に書き直しました。 公開当時に「新しいメソッド」として紹介していた3つは、いずれも存在しないメソッドでした。実行すると TypeError になります。

  • WeakMap.prototype.merge は存在しません
  • WeakMap.prototype.hasKey は存在しません(存在確認は has())
  • WeakMap.prototype.entries は存在しません。 そもそも WeakMap は原理的に列挙できません(理由は後述します)
  • あわせて、公開当時に載せていた「メモリ使用量の比較表」は出典を確認できなかったため削除しました

実際にこの数年で加わったのは ES2023 の「シンボルをキーにできる」変更だけです。この記事では、正確な仕様と、WeakMap が本当に効く場面を解説します。

スポンサーリンク

WeakMap とは何か

キーへの参照が「弱い」マップです。 WeakMap がキーを持っていても、それだけではガベージコレクションを妨げません。

const meta = new WeakMap();

let element = document.createElement('div');
meta.set(element, { clickCount: 0 });

meta.get(element);   // { clickCount: 0 }

element = null;
// この時点で element を指すものが他に無ければ、
// div も meta 内のエントリもまとめて回収される

これが通常の Map との決定的な違いです。Map にキーとして入れると、そのオブジェクトは永久に回収されません。長時間動くアプリケーションでは、これがそのままメモリリークになります。

メソッドは4つだけ

メソッド 動作
set(key, value) 登録する(自身を返すので連結できる)
get(key) 取得する。無ければ undefined
has(key) 存在するかを返す
delete(key) 削除する。削除できたかを boolean で返す

これで全部です。 size プロパティも、keys() / values() / entries() / forEach() もありません。for...of でも回せません。

const wm = new WeakMap();

wm.size;              // undefined
wm.entries();         // TypeError: wm.entries is not a function
for (const x of wm) {} // TypeError: wm is not iterable

スポンサーリンク

なぜ列挙できないのか

これは制限ではなく、設計上の必然です。

WeakMap のエントリは、キーが他から参照されなくなった時点でいつでもエンジンが削除できます。回収のタイミングは実装依存で、いつ起きるかは決まっていません。

もし列挙できてしまうと、「ガベージコレクションがいつ走ったか」をスクリプトから観測できることになります。同じコードが実行タイミングによって違う結果を返す、再現性のないプログラムになってしまいます。

そのため、件数(size)も順序も内容も、外から知る手段が意図的に用意されていません。「デバッグやデータ分析のために中身をループしたい」という用途には、そもそも WeakMap は使えません。

中身を見たいなら Map を使ってください。 それは弱参照が要らないということです。

ES2023:シンボルをキーにできるようになった

この数年で加わった唯一の変更点です。 従来キーはオブジェクトだけでしたが、登録されていないシンボルも使えるようになりました。

const wm = new WeakMap();

const key = Symbol('token');
wm.set(key, 'value');       // ES2023 から可能
wm.get(key);                // 'value'

// 文字列や数値は今も不可
wm.set('str', 1);           // TypeError
wm.set(1, 'x');             // TypeError

// Symbol.for() で登録したシンボルは不可
wm.set(Symbol.for('app'), 1); // TypeError

Symbol.for() で作るシンボルが除外されているのは、グローバルレジストリに登録されるため実質的に文字列と同じで、回収される見込みが無いからです。

対応は Chrome / Safari が済んでおり、Firefox は執筆時点で未対応です。使う場合は確認してください。

スポンサーリンク

Map との使い分け

WeakMap Map
キーに使えるもの オブジェクト、未登録のシンボル 任意の値
キーへの参照 弱参照(回収を妨げない) 強参照(回収されない)
列挙 できない できる
size 無い ある
clear() 無い ある
向く場面 オブジェクトへの付属情報 普通のキー・値の管理

判断基準はひとつです。「キーになるオブジェクトの寿命を、このマップが決めてよいか」。 決めてはいけない(勝手に生かし続けたくない)なら WeakMap です。

使いどころ1:DOM要素に付属情報を持たせる

最も実用的な使い方です。要素がDOMから消えたら、付属情報も自動的に消えます。

const state = new WeakMap();

function initCounter(el) {
  state.set(el, { count: 0 });

  el.addEventListener('click', () => {
    const s = state.get(el);
    s.count += 1;
    el.textContent = `${s.count} 回`;
  });
}

document.querySelectorAll('.counter').forEach(initCounter);

要素を remove() したあと、その要素を指すものが他に無ければ、要素と { count: n } がまとめて回収されます。Map で同じことをすると、削除した要素がずっとメモリに残り続けます。

誤解を1つ正しておきます

「WeakMap を使えばイベントリスナーのメモリリークが防げる」わけではありません。

上のコードで、リスナーは el 自身に登録されています。要素がDOMから外れて誰からも参照されなくなれば、要素・リスナー・WeakMapのエントリはまとめて回収されます。逆に、要素がどこかから参照され続けていれば、WeakMap を使っていても何も回収されません。

WeakMap が保証するのは「マップの存在が回収を妨げないこと」だけです。リスナーの解除が必要な場面では、素直に removeEventListener するか、AbortSignal でまとめて外す方が確実です。

const controller = new AbortController();

el.addEventListener('click', handler, { signal: controller.signal });
el.addEventListener('keydown', handler2, { signal: controller.signal });

// 1行で全部外れる
controller.abort();

AbortSignal の使い方はfetch() APIの使い方でも扱っています。

使いどころ2:計算結果のキャッシュ

オブジェクトごとに重い計算をキャッシュするとき、元のオブジェクトが消えたらキャッシュも消えてほしいという要求にぴったり合います。

const cache = new WeakMap();

function getExpensiveResult(obj) {
  if (cache.has(obj)) {
    return cache.get(obj);
  }

  const result = heavyCalculation(obj);
  cache.set(obj, result);
  return result;
}

Map でこれを書くと、キャッシュが増え続けて解放されません。件数の上限管理やLRUの実装が必要になります。WeakMap ならエンジンに任せられます。

ただしキャッシュのヒット率は保証されません。回収されたかどうかは観測できないので、「必ず残っていてほしい」データには使えません。

使いどころ3:プライベートデータ(現在は #private を推奨)

クラスの内部データを外から触れないようにするパターンとして、かつて WeakMap がよく使われました。

const privateData = new WeakMap();

class Account {
  constructor(secret) {
    privateData.set(this, { secret });
  }
  getSecret() {
    return privateData.get(this).secret;
  }
}

現在は、プライベートフィールド # を使うのが素直です。 ES2022で標準化され、全モダンブラウザで使えます。

class Account {
  #secret;

  constructor(secret) {
    this.#secret = secret;
  }

  getSecret() {
    return this.#secret;
  }
}

const a = new Account('xxx');
a.secret;    // undefined
a.#secret;   // SyntaxError(クラスの外からは構文的にアクセスできない)

# の方が意図が明確で、外部の WeakMap を持ち回らずに済みます。 WeakMap版が今も有効なのは、クラス構文を使わない場合や、複数のクラスで共通のメタデータを管理したい場合くらいです。

WeakRef と FinalizationRegistry

弱参照の仲間として、ES2021で WeakRef と FinalizationRegistry が追加されています。

const ref = new WeakRef(someObject);

// 取り出す。回収済みなら undefined
const obj = ref.deref();
if (obj) {
  obj.doSomething();
}

ただし、これらは基本的に使うべきではありません。 MDNや仕様の注記でも慎重な使用が推奨されています。

  • 回収のタイミングは実装依存で予測できません。ブラウザやバージョンによって挙動が変わります
  • FinalizationRegistry のコールバックは呼ばれない可能性があります。タブを閉じた場合など、実行されない前提で設計する必要があります
  • 「後始末」の仕組みとしては信頼できません。明示的な close() や dispose() を用意する方が確実です

「弱参照が必要」と思ったときは、まず WeakMap で足りないかを検討してください。 ほとんどの場合は足ります。

使う前に確認したいこと

  • キーはオブジェクトか:文字列や数値をキーにしたいなら Map です
  • 中身を数えたり一覧したりしたいか:したいなら Map です。WeakMap ではできません
  • キーになるオブジェクトの寿命を、他のコードが管理しているか:管理しているなら WeakMap が適しています
  • データが消えると困るか:困るなら WeakMap は使えません。いつ消えるか分かりません

まとめ

  • merge / hasKey / entries というWeakMapのメソッドは存在しません
  • メソッドは set / get / has / delete の4つだけ。size も列挙もない
  • 列挙できないのは設計上の必然。 できてしまうとGCのタイミングが観測できてしまう
  • この数年の変更は ES2023の「未登録シンボルをキーにできる」のみ(Firefoxは未対応)
  • 本領はDOM要素への付属情報とオブジェクト単位のキャッシュ
  • WeakMap を使ってもイベントリスナーは勝手に外れません。 解除には AbortSignal を使う
  • プライベートデータはクラスの # フィールドを優先する
  • WeakRef / FinalizationRegistry は挙動が予測できない。安易に使わない

「メモリ効率が良い」という表現だけが独り歩きしやすい機能ですが、実態は「寿命を他に委ねられる」ことが本質です。配列操作の新機能についてはJavaScriptの新しいArrayメソッドにまとめています。