WeakMap
Baseline
Widely available
*
This feature is well established and works across many devices and browser versions. It’s been available across browsers since 2015年7月.
* Some parts of this feature may have varying levels of support.
WeakMap はキーと値の組の集合ですが、キーはオブジェクトまたは非登録シンボルでなければならず、値は任意の JavaScript 型で、キーへの強い参照を生成しません。つまり、WeakMap 内のキーとしてのオブジェクトが存在しても、そのオブジェクトがガベージコレクションされる可能性があります。キーとして使用されたオブジェクトが回収されると、そのオブジェクトに対応する値は、他の場所で強く参照されていない限り、どの WeakMap においても同様にガベージコレクションの対象となります。WeakMap のキーとして使用することができる唯一のプリミティブ型はシンボル(正確に言えば、非登録シンボル)です。非登録シンボルは一意性が保証され、再生成されないためです。
WeakMapは、値がキーを参照している場合でも、キーとなるオブジェクトがガベージコレクションされるのを妨げない方法で、データとオブジェクトを関連付けることができます。ただし、WeakMapはキーの生存状態を監視できないため、列挙をすることができません。WeakMap がキーを列挙するメソッドを公開した場合、そのリストはガベージコレクションの状態に依存するため、不確定性が生じます。キーの列挙を行いたいのであれば、WeakMap ではなく Map を使用してください。
WeakMap について詳しく知るには、 WeakMap オブジェクトのガイド(キー付きコレクション内)を参照してください。
解説
WeakMap のキーはガベージコレクション可能でなければなりません。ほとんどのプリミティブデータ型は任意に作成でき、寿命を持たないため、キーとして使用できません。オブジェクトと非登録シンボルは、ガベージコレクション可能であるためキーとして使用できます。
キーの等価性
通常の Map と同様に、値の等価性は SameValueZero アルゴリズムに基づきます。これは === 演算子と同じです。WeakMap がオブジェクトとシンボルのキーしか保持できないためです。つまり、オブジェクトキーの場合、等価性はオブジェクトが同一のものであるかに基づきます。これらは値ではなく参照によって比較されます。
なぜ WeakMap なのか
マップ API は、4 つの API メソッドで共有される 2 つの配列(1 つはキー用、もう 1 つは値用)を用いることで、JavaScript に実装することができました。このマップに要素を設定すると、それぞれの配列の最後に同時にキーと値を追加することになります。その結果、両方の配列でキーと値のインデックスは対応がとれています。マップから値を取得するには、すべてのキーを操作して一致するものを見つけ、見つかったキーのインデックスを使用して値の配列から対応する値を取り出します。
そうした実装では、主に 2 つの不都合が生じることとなります。
- 第一に、設定や探索の計算量が
O(n)となること (n はマップ中におけるキーの数) で、どちらの操作も一致する値を見つけるためにキーのリストを走査しなければならないからです。 - 第二の不都合は、配列が各キーと各値への参照を無期限に維持することを保証しているため、メモリーリークが発生することです。これらの参照は、オブジェクトへの他の参照がない場合でも、キーがガベージコレクションされるのを防ぎます。これにより、対応する値がガベージコレクションされるのを防ぐことにもなります。
これに対し、WeakMap では、キーオブジェクトは、そのキーがガベージコレクションされない限り、そのコンテンツに対して強く参照しますが、その後は弱く参照します。したがって、WeakMap は次のように動作します。
- キーオブジェクトへの参照が最終的に除去されるガベージコレクションを妨げません。
WeakMap以外の場所から参照されていないキーオブジェクトを持つ値は、ガベージコレクションの対象となることがあります。
WeakMap は、キーがガベージコレクションされていない場合にのみ有益な、キーに関する情報をキーに割り当てる際に特に有益なデータ構造です。
しかし、WeakMapはキーの生存状態を監視できないため、キーは列挙可能ではありません。キーのリストを取得するメソッドは存在しません。仮に存在した場合、そのリストはガベージコレクションの状態に依存し、不確定性が生じることになります。キーのリストが必要な場合は、 Map を使用してください。
コンストラクター
WeakMap()-
新しい
WeakMapオブジェクトを生成します。
インスタンスプロパティ
これらのプロパティは WeakMap.prototype に定義され、すべての WeakMap インスタンスで共有されます。
WeakMap.prototype.constructor-
このインスタンスオブジェクトを作成したコンストラクター関数。
WeakMapインスタンスの場合、初期値はWeakMapコンストラクターです。 WeakMap.prototype[Symbol.toStringTag]-
[Symbol.toStringTag]プロパティの初期値は文字列"WeakMap"です。このプロパティはObject.prototype.toString()で使用されます。
インスタンスメソッド
WeakMap.prototype.delete()-
keyに関連した値を削除します。その後WeakMap.prototype.has(key)はfalseを返します。 WeakMap.prototype.get()-
keyに関連した値を返します。見つからない場合、undefinedを返します。 WeakMap.prototype.getOrInsert()-
この
WeakMap内で指定されたキーに対応する値を返します。キーが存在しない場合、指定されたデフォルト値を持つ新しい項目を挿入し、挿入された値を返します。 WeakMap.prototype.getOrInsertComputed()-
この
WeakMapで指定されたキーに対応する値を返します。キーが存在しない場合、指定されたキーと、与えられたコールバックから計算されたデフォルト値を持つ新しい項目を挿入し、挿入された値を返します。 WeakMap.prototype.has()-
この
WeakMapオブジェクト内に、指定されたキーに関連付けられた値があるかどうか示す論理値を返します。 WeakMap.prototype.set()-
この
WeakMapオブジェクト内に、指定されたキーと値を持つ新しい項目を追加します。すでにそのキーが存在する場合は、既存の項目を更新します。
例
>WeakMap の使用
const wm1 = new WeakMap();
const wm2 = new WeakMap();
const wm3 = new WeakMap();
const o1 = {};
const o2 = () => {};
const o3 = window;
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // 値は(オブジェクトまたは関数を含む)何であってもかまいません
wm2.set(o2, undefined);
wm2.set(wm1, wm2); // キーも値もどんなオブジェクトでもかまいません。 WeakMap であってもよいのです!
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined が設定されているので undefined
wm2.get(o3); // o3 のキーが wm2 に存在しないので undefined
wm1.has(o2); // true
wm2.has(o2); // true(値自体が 'undefied' あっても)
wm2.has(o3); // false
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
.clear() メソッドを持つ WeakMap 風のクラスの実装
class ClearableWeakMap {
#wm;
constructor(init) {
this.#wm = new WeakMap(init);
}
clear() {
this.#wm = new WeakMap();
}
delete(k) {
return this.#wm.delete(k);
}
get(k) {
return this.#wm.get(k);
}
has(k) {
return this.#wm.has(k);
}
set(k, v) {
this.#wm.set(k, v);
return this;
}
}
プライベートメンバーのエミュレーション
開発者はWeakMapを使用してオブジェクトにプライベートデータを関連付けることができ、これには次のような利点があります。
Mapと比べると、WeakMap はキーとして使用されるオブジェクトへの強い参照を保持しないため、メタデータの存続期間がオブジェクト自体と同じになり、メモリーリークを避けることができます。- 列挙不可能プロパティや
Symbolプロパティを使用する場合と比べ、WeakMap はオブジェクトの外部に存在するため、ユーザーコードがObject.getOwnPropertySymbolsのような反射メソッドを通じてメタデータを取得する方法がありません。 - クロージャと比べると、同じ WeakMap をコンストラクターから生成されたすべてのインスタンスで再利用できるため、メモリー効率が向上します。また、同じクラスの異なるインスタンスが互いのプライベートメンバーを読み取ることができます。
let Thing;
{
const privateScope = new WeakMap();
let counter = 0;
Thing = function () {
this.someProperty = "foo";
privateScope.set(this, {
hidden: ++counter,
});
};
Thing.prototype.showPublic = function () {
return this.someProperty;
};
Thing.prototype.showPrivate = function () {
return privateScope.get(this).hidden;
};
}
console.log(typeof privateScope);
// "undefined"
const thing = new Thing();
console.log(thing);
// Thing {someProperty: "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
これは、プライベートフィールドを使用した次のコードとほぼ同等です。
class Thing {
static #counter = 0;
#hidden;
constructor() {
this.someProperty = "foo";
this.#hidden = ++Thing.#counter;
}
showPublic() {
return this.someProperty;
}
showPrivate() {
return this.#hidden;
}
}
console.log(thing);
// Thing {someProperty: "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
メタデータの関連付け
WeakMapは、オブジェクト自体の存続期間に影響を与えずに、オブジェクトにメタデータを関連付けるために使用できます。これはプライベートメンバーの例と非常に似ています。プライベートメンバーもまた、プロトタイプ継承に参加しない外部メタデータとしてモデル化されるためです。
この使い方は、既に生成されたオブジェクトにも拡張できます。例えば、ウェブ上では、DOM 要素に追加データを関連付け、後でその DOM 要素がアクセスできるようにしたい場合があります。一般的な手法は、データをプロパティとして添付することです。
const buttons = document.querySelectorAll(".button");
buttons.forEach((button) => {
button.clicked = false;
button.addEventListener("click", () => {
button.clicked = true;
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => button.clicked)) {
console.log("すべてのボタンがクリックされました!");
}
});
});
このアプローチは有効ですが、いくつか落とし穴があります。
clickedプロパティは列挙可能であるため、Object.keys(button)、for...inループなどで対象として扱われます。この問題はObject.defineProperty()を使用することで軽減できますが、コードが冗長になります。clickedプロパティは通常の文字列プロパティであるため、他のコードからアクセスおよび上書きが可能です。この問題はSymbolキーを使用することで軽減できますが、そのキーもObject.getOwnPropertySymbols()経由でアクセス可能となります。
WeakMap を使用してこれらを修正してみます。
const buttons = document.querySelectorAll(".button");
const clicked = new WeakMap();
buttons.forEach((button) => {
clicked.set(button, false);
button.addEventListener("click", () => {
clicked.set(button, true);
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => clicked.get(button))) {
console.log("すべてのボタンがクリックされました!");
}
});
});
ここでは、clicked にアクセスできるコードのみがそれぞれのボタンのクリック状態を把握しており、外部コードは状態を変更できません。さらに、いずれかのボタンが DOM から除去されると、関連付けられたメタデータは自動的にガベージコレクションの対象となります。
キャッシュ
関数に渡されたオブジェクトを関数の結果に関連付けることができます。これにより、同じオブジェクトが再度渡された場合、関数を再実行せずにキャッシュされた結果を返すことが可能です。この機能は、関数が純粋(つまり、外部オブジェクトを変更したり他の観測可能な副作用を引き起こしたりしない)である場合に有用です。
const cache = new WeakMap();
function handleObjectValues(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const result = Object.values(obj).map(heavyComputation);
cache.set(obj, result);
return result;
}
この方法は、関数の入力がオブジェクトである場合にのみ有効です。さらに、入力が二度と渡されなくても、キー(入力)が存続している限り、結果はキャッシュに永久に残ります。より効果的な方法は、Map と WeakRef オブジェクトを組み合わせて使用することです。これにより、あらゆる型の入力値を、それに対応する(潜在的に大規模な)計算結果に関連付けることができるようになります。詳細は WeakRef と FinalizationRegistry の例を参照してください。
仕様書
| Specification |
|---|
| ECMAScript® 2026 Language Specification> # sec-weakmap-objects> |