JavaScript WeakMapの実践的な使い方|メモリ効率を改善する実装パターン
JavaScriptのWeakMapは、多くの開発者にとって理解が難しく、実際の業務で活用する機会が少ないデータ構造です。しかし、正しく理解して活用すると、メモリ効率の改善やセキュリティの向上につながる強力なツールになります。本記事では、WeakMapの基本から実務レベルのサンプルコード、応用パターンまで、実際のプロジェクトで使える知識をお伝えします。
1. WeakMapの基本的な特徴
WeakMapを理解する前に、通常のMapとの違いを押さえておくことが重要です。
WeakMapの3つの主な特徴:
- キーはオブジェクトのみ(プリミティブ値は不可)
- WeakMapが保持するキーへの参照は「弱参照」である
- イテレーション(forEach、keys()など)ができない
弱参照とは、オブジェクトをメモリ上に留めておかない参照方式を意味します。つまり、キーとなるオブジェクトが他の場所で参照されていなくなると、ガベージコレクションの対象となり、自動的にメモリから削除されます。
// 通常のMap - キーが残っているとメモリを圧迫
const map = new Map();
let obj = { id: 1 };
map.set(obj, 'data');
obj = null; // objを削除しても、Mapがキーを保持しているため、メモリに残る
// WeakMap - キーが参照されなくなると自動でメモリから削除
const weakMap = new WeakMap();
let obj2 = { id: 2 };
weakMap.set(obj2, 'data');
obj2 = null; // objへの参照がなくなると、WeakMapのキーも自動で削除される
2. 業務でのユースケース
ユースケース1: DOMノードのプライベートデータ管理
React やVueなどのフレームワークを使わない古いプロジェクトでは、DOM要素に関連するデータを管理する必要があります。このような場合、WeakMapは非常に有効です。
従来の方法では、DOM要素に直接プロパティを追加したり、グローバルなオブジェクトで管理していました。WeakMapを使うことで、DOM要素が削除されると自動的にデータもメモリから解放されます。
ユースケース2: ユーザーメタデータのキャッシング
Webアプリケーション内で、ユーザーオブジェクトに対するメタデータ(最終アクセス日時、ロール情報など)をキャッシュする場合、WeakMapが有効です。ユーザーがシステムから削除されれば、自動的にメタデータも消去されます。
ユースケース3: クラスインスタンスのプライベート状態管理
ECMAScript 2022で#によるプライベートフィールドが導入される前は、WeakMapを使ってクラスの真のプライベートデータを管理するパターンが一般的でした。今でも、外部からアクセスできないデータ構造が必要な場合に使用されます。
3. 実装コード|実務レベルのサンプル
サンプル1: DOMノード関連データの管理
// DOMノードにメタデータを紐付ける実装
class DOMMetadataManager {
constructor() {
this.metadata = new WeakMap();
}
// ノードにメタデータを設定
setMetadata(node, data) {
if (!(node instanceof Node)) {
throw new Error('キーはDOMノードである必要があります');
}
this.metadata.set(node, {
createdAt: new Date(),
...data
});
}
// ノードのメタデータを取得
getMetadata(node) {
return this.metadata.get(node);
}
// ノードのメタデータが存在するか確認
hasMetadata(node) {
return this.metadata.has(node);
}
}
// 使用例
const manager = new DOMMetadataManager();
const button = document.querySelector('button');
if (button) {
manager.setMetadata(button, {
clickCount: 0,
owner: 'admin'
});
const data = manager.getMetadata(button);
console.log(data); // { createdAt: Date, clickCount: 0, owner: 'admin' }
}
サンプル2: ユーザーセッション情報のキャッシング
// 実際の業務で使える、ユーザーセッション管理
class UserSessionCache {
constructor() {
this.sessions = new WeakMap();
}
// ユーザーオブジェクトのセッション情報を設定
createSession(userObject, sessionData) {
this.sessions.set(userObject, {
sessionId: this.generateSessionId(),
createdAt: Date.now(),
lastAccessed: Date.now(),
data: sessionData
});
}
// セッション情報を更新
updateSession(userObject, updates) {
const session = this.sessions.get(userObject);
if (session) {
session.lastAccessed = Date.now();
Object.assign(session.data, updates);
}
}
// セッション情報を取得
getSession(userObject) {
return this.sessions.get(userObject);
}
// セッションの有効期限チェック
isSessionValid(userObject, timeoutMs = 30 * 60 * 1000) {
const session = this.sessions.get(userObject);
if (!session) return false;
return Date.now() - session.lastAccessed < timeoutMs;
}
generateSessionId() {
return 'sess_' + Math.random().toString(36).substr(2, 9);
}
}
// 使用例
const cache = new UserSessionCache();
const user = { id: 1, name: 'Taro Yamada' };
cache.createSession(user, { theme: 'dark', language: 'ja' });
cache.updateSession(user, { language: 'en' });
const session = cache.getSession(user);
console.log(session);
// {
// sessionId: 'sess_xxx...',
// createdAt: 1234567890,
// lastAccessed: 1234567890,
// data: { theme: 'dark', language: 'en' }
// }
サンプル3: クラスのプライベート状態管理(従来パターン)
// WeakMapを使ったプライベート状態管理
// (現在は#を使うべきだが、レガシーコードや特殊な用途では有効)
const privateData = new WeakMap();
class BankAccount {
constructor(accountNumber, initialBalance) {
// プライベート状態をWeakMapに保存
privateData.set(this, {
accountNumber: accountNumber,
balance: initialBalance,
transactions: []
});
}
deposit(amount) {
const data = privateData.get(this);
if (amount > 0) {
data.balance += amount;
data.transactions.push({
type: 'deposit',
amount: amount,
timestamp: new Date()
});
return true;
}
return false;
}
withdraw(amount) {
const data = privateData.get(this);
if (amount > 0 && data.balance >= amount) {
data.balance -= amount;
data.transactions.push({
type: 'withdraw',
amount: amount,
timestamp: new Date()
});
return true;
}
return false;
}
getBalance() {
return privateData.get(this).balance;
}
getTransactionHistory() {
return privateData.get(this).transactions;
}
}
// 使用例
const account = new BankAccount('123456789', 100000);
account.deposit(50000);
account.withdraw(20000);
console.log(account.getBalance()); // 130000
console.log(account.getTransactionHistory());
// [
// { type: 'deposit', amount: 50000, timestamp: Date },
// { type: 'withdraw', amount: 20000, timestamp: Date }
// ]
サンプル4: APIリクエストのキャッシング(実務的)
// 実務で使える、API呼び出し結果のキャッシング機構
class APIRequestCache {
constructor() {
// キーをリクエストオブジェクト、値をレスポンスキャッシュ
this.cache = new WeakMap();
// キャッシュの有効期限を管理
this.timestamps = new WeakMap();
}
// キャッシュにデータを保存
set(requestKey, response, ttlMs = 5 * 60 * 1000) {
this.cache.set(requestKey, response);
this.timestamps.set(requestKey, {
createdAt: Date.now(),
ttl: ttlMs
});
}
// キャッシュからデータを取得(有効期限チェック付き)
get(requestKey) {
const timestamp = this.timestamps.get(requestKey);
if (!timestamp) return null;
const age = Date.now() - timestamp.createdAt;
if (age > timestamp.ttl) {
// 有効期限切れ - WeakMapなので手動削除は不要だが、明示的に処理
return null;
}
return this.cache.get(requestKey);
}
has(requestKey) {
const cached = this.get(requestKey);
return cached !== null;
}
}
// 使用例
const apiCache = new APIRequestCache();
// リクエスト用のオブジェクトキー
const userListRequest = {
endpoint: '/api/users',
params: { page: 1 }
};
// キャッシュに保存
const mockResponse = {
status: 200,
data: [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }
]
};
apiCache.set(userListRequest, mockResponse, 10 * 60 * 1000); // 10分有効
// キャッシュから取得
const cachedData = apiCache.get(userListRequest);
console.log(cachedData); // mockResponse
4. TypeScriptでの型安全な実装
TypeScriptを使う場合は、型定義をすることでより安全なWeakMapの使用が可能です。
// TypeScriptでの型安全なWeakMap実装
interface SessionData {
sessionId: string;
theme: 'light' | 'dark';
language: string;
preferences: Record<string, unknown>;
}
interface UserObject {
id: number;
email: string;
[key: string]: any;
}
class TypeSafeSessionManager {
private sessions: WeakMap<UserObject, SessionData>;
constructor() {
this.sessions = new WeakMap();
}
createSession(user: UserObject, data: Omit<SessionData, 'sessionId'>): void {
this.sessions.set(user, {
sessionId: this.generateId(),
...data
});
}
getSession(user: UserObject): SessionData | undefined {
return this.sessions.get(user);
}
hasSession(user: UserObject): boolean {
return this.sessions.has(user);
}
private generateId(): string {
return 'sess_' + Math.random().toString(36).substr(2, 9);
}
}
// 使用例
const user: UserObject = { id: 1, email: 'user@example.com' };
const manager = new TypeSafeSessionManager();
manager.createSession(user, {
theme: 'dark',
language: 'ja',
preferences: { fontSize: 14 }
});
const session = manager.getSession(user);
// TypeScriptがSessionData型を推論できる
5. よくある応用パターン
パターン1: 複数のWeakMapを組み合わせた多層キャッシュ
// 複数のメタデータを階層的に管理
class MultiLayerCache {
constructor() {
this.primaryCache = new WeakMap(); // メインデータ
this.metadataCache = new WeakMap(); // メタデータ
this.statsCache = new WeakMap(); // アクセス統計
}
set(key, data, metadata = {}, stats = {}) {
this.primaryCache.set(key, data);
this.metadataCache.set(key, {
createdAt: new Date(),
...metadata
});
this.statsCache.set(key, {
accessCount: 0,
lastAccessed: null,
...stats
});
}
get(key) {
// アクセス統計を更新
const stats = this.statsCache.get(key);
if (stats) {
stats.accessCount++;
stats.lastAccessed = new Date();
}
return {
data: this.primaryCache.get(key),
metadata: this.metadataCache.get(key),
stats: stats
};
}
}
// 使用例
const cache = new MultiLayerCache();
const resourceKey = { resourceId: 'res123' };
cache.set(resourceKey,
{ content: 'important data' },
{ owner: 'admin', isPublic: false },
{}
);
const result = cache.get(resourceKey);
console.log(result);
// {
// data: { content: 'important data' },
// metadata: { createdAt: Date, owner: 'admin', isPublic: false },
// stats: { accessCount: 1, lastAccessed: Date }
// }
パターン2: WeakMapとProxyを組み合わせた監視機構
// 実務的な例:オブジェクトの変更を監視しながらデータを管理
class ObservableDataManager {
constructor() {
this.data = new WeakMap();
this.observers = new WeakMap();
}
create(key, initialData = {}) {
const handlers = {
get: (target, property) => {
console.log(`Getting property: ${String(property)}`);
return target[property];
},
set: (target, property, value) => {
const oldValue = target[property];
target[property] = value;
// オブザーバーに通知
const observerList = this.observers.get(key) || [];
observerList.forEach(callback => {
callback({
property,
oldValue,
newValue: value
});
});
return true;
}
};
const proxy = new Proxy(initialData, handlers);
this.data.set(key, proxy);
this.observers.set(key, []);
return proxy;
}
subscribe(key, callback) {
const observerList = this.observers.get(key) || [];
observerList.push(callback);
this.observers.set(key, observerList);
}
getData(key) {
return this.data.get(key);
}
}
// 使用例
const manager = new ObservableDataManager();
const user = { id: 1, name: 'Taro' };
const userData = manager.create(user, { age: 30, city: 'Tokyo' });
manager.subscribe(user, (change) => {
console.log(`Changed: ${change.property} from ${change.oldValue} to ${change.newValue}`);
});
userData.age = 31; // Changed: age from 30 to 31
userData.city = 'Osaka'; // Changed: city from Tokyo to Osaka
6. WeakMapの注意点と落とし穴
注意点1: デバッグが難しい
WeakMapはイテレーション機能がないため、デバッグ時に内容を確認できません。開発時は通常のMapを使い、本番環境でWeakMapに切り替えるアプローチも検討してください。
// デバッグ用のラッパークラス
class DebugWeakMap {
constructor(enableDebug = false) {
this.weakMap = new WeakMap();
this.debugMap = enableDebug ? new Map() : null;
}
set(key, value) {
this.weakMap.set(key, value);
if (this.debugMap) {
this.debugMap.set(key, value);
}
}
get(key) {
return this.weakMap.get(key);
}
// デバッグ用:内容を確認
debugLog() {
if (this.debugMap) {
console.table([...this.debugMap.entries()]);
}
}
}
const store = new DebugWeakMap(true); // 開発環境ではtrue
const obj = { id: 1 };
store.set(obj, 'data');
store.debugLog(); // テーブル形式で出力
注意点2: ガベージコレクションのタイミングは予測不可
WeakMapのキーがいつメモリから削除されるかは、JavaScriptエンジンの実装によって異なります。メモリ削除のタイミングに依存した処理は避けましょう。
注意点3: キーはオブジェクトのみ
プリミティブ値(文字列、数値など)をキーにしたい場合は、WeakMapではなく通常のMapを使う必要があります。
// ❌ エラー:プリミティブ値はWeakMapのキーにできない
const weakMap = new WeakMap();
weakMap.set('key', 'value'); // TypeError: Invalid value used as weak map key
// ✅ 正しい:オブジェクトをキーとする
const obj = { id: 'key' };
weakMap.set(obj, 'value'); // OK
注意点4: WeakMapはシリアライズできない
WeakMapの内容をJSON化したり、外部に送信したりすることはできません。データの永続化が必要な場合は、別の方法を検討してください。
7. 実務での実装チェックリスト
WeakMapを業務コードに導入する際のチェックリストです。
- ✅ キーとなるオブジェクトの生存期間を理解しているか
- ✅ メモリリークの防止が目的か、それともプライベートデータ隔離か、目的が明確か
- ✅ チーム内でWeakMapの使用規約を定めているか
- ✅ デバッグやテスト時の対応は済んでいるか
- ✅ パフォーマンス測定は実施したか
- ✅ TypeScriptの型定義は適切か
- ✅ ドキュメント(なぜWeakMapを使ったか)は残しているか
8. WeakMapとの比較:他のデータ構造
| 特性 | WeakMap | Map | Object |
|---|---|---|---|
| キーの種類 | オブジェクトのみ | 任意の値 | 文字列/Symbol |
| 弱参照 | はい | いいえ | いいえ |
| イテレーション | 不可 | 可能 | 可能 |
| シリアライズ | 不可 | 不可 | 可能 |
| プライベート性 | 高い | 低い | 中程度 |
まとめ
WeakMapは、単なる「メモリ効率がよいMap」ではなく、特定のユースケースに最適化されたデータ構造です。実務での活用シーンは限定的ですが、DOMノード管理、セッションキャッシング、プライベートデータの隔離など、適切な場面で使用するとコードの品質と安全性が大幅に向上します。
重要なのは、「なぜWeakMapを使うのか」という目的を明確にすることです。パフォーマンス最適化、メモリ管理、セキュリティ強化など、目的に応じて適切に活用してください。
特にレガシーコード(ES2022の#プライベートフィールドが使えない環境)やDOM操作が中心のプロジェクトでは、WeakMapの価値が大きいでしょう。本記事で紹介したサンプルコードを参考に、実際のプロジェクトで検討してみてください。

