JavaScript Mapの実践的な使い方|業務で使えるサンプルコード集
JavaScriptの開発現場で日々使われるMapオブジェクト。配列の処理と比較されることが多いですが、適切に使い分けることでコードの可読性と処理速度が大きく改善します。本記事では、教科書的な説明ではなく、実務で実際に遭遇するユースケースを中心に、Mapの実践的な活用方法を解説します。
Mapとは|簡易的な解説
JavaScriptのMapは、キーと値のペアを保持するデータ構造です。オブジェクトとの大きな違いは、キーの型に制限がないという点。文字列だけでなく、オブジェクトや関数もキーとして使用できます。
// 基本的な使い方
const map = new Map();
map.set('key1', 'value1');
map.set(123, 'numeric key');
map.set({}, 'object key');
console.log(map.get('key1')); // 'value1'
console.log(map.size); // 3
さらに重要な特性として、Mapは挿入順序を保持します。これは同期的な処理が必要な場合に非常に役立ちます。
業務でのユースケース|実装が必要なシーン
1. APIレスポンスのキャッシング
実務では、同じデータへのリクエストを複数回避けたいシーンが頻繁に発生します。Mapを使ったメモ化パターンは非常に一般的です。
class UserCache {
constructor() {
this.cache = new Map();
this.requestInProgress = new Map();
}
async fetchUser(userId) {
// キャッシュに存在する場合はそれを返す
if (this.cache.has(userId)) {
console.log(`キャッシュから取得: ${userId}`);
return this.cache.get(userId);
}
// リクエスト進行中の場合は、その Promise を返す
if (this.requestInProgress.has(userId)) {
return this.requestInProgress.get(userId);
}
// 新規リクエスト
const promise = fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
this.cache.set(userId, data);
this.requestInProgress.delete(userId);
return data;
})
.catch(err => {
this.requestInProgress.delete(userId);
throw err;
});
this.requestInProgress.set(userId, promise);
return promise;
}
clearCache(userId) {
this.cache.delete(userId);
}
}
// 使用例
const userCache = new UserCache();
userCache.fetchUser(1).then(data => console.log(data));
userCache.fetchUser(1).then(data => console.log(data)); // キャッシュから取得
2. イベントハンドラーの管理
複雑なイベントシステムでは、イベントタイプごとにハンドラーのリストを管理する必要があります。Mapはこの用途に最適です。
class EventEmitter {
constructor() {
this.events = new Map();
}
on(eventName, handler) {
if (!this.events.has(eventName)) {
this.events.set(eventName, []);
}
this.events.get(eventName).push(handler);
}
off(eventName, handler) {
if (!this.events.has(eventName)) return;
const handlers = this.events.get(eventName);
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
}
emit(eventName, data) {
if (!this.events.has(eventName)) return;
this.events.get(eventName).forEach(handler => handler(data));
}
clear(eventName) {
this.events.delete(eventName);
}
}
// 使用例
const emitter = new EventEmitter();
const handler = (data) => console.log('イベント発火:', data);
emitter.on('user-login', handler);
emitter.emit('user-login', { userId: 123 });
emitter.off('user-login', handler);
3. 重複排除と出現頻度カウント
配列データの重複排除やカウントは、配列メソッドより Mapで処理する方が効率的です。
function analyzeUserActions(actions) {
const actionMap = new Map();
actions.forEach(action => {
const key = action.type;
if (actionMap.has(key)) {
actionMap.set(key, actionMap.get(key) + 1);
} else {
actionMap.set(key, 1);
}
});
return actionMap;
}
// 使用例
const actions = [
{ type: 'click', timestamp: Date.now() },
{ type: 'scroll', timestamp: Date.now() },
{ type: 'click', timestamp: Date.now() },
{ type: 'click', timestamp: Date.now() },
];
const result = analyzeUserActions(actions);
console.log(result); // Map { 'click' => 3, 'scroll' => 1 }
実装コード|実務レベルのサンプル
複合的なデータ管理パターン
実務では複数の情報を連動させる必要があります。以下は、ユーザーデータと権限情報を同期管理するパターンです。
class UserPermissionManager {
constructor() {
this.users = new Map(); // userId => userData
this.permissions = new Map(); // userId => Set
this.roles = new Map(); // roleId => Set
}
addRole(roleId, permissions) {
this.roles.set(roleId, new Set(permissions));
}
registerUser(userId, userData, roleIds = []) {
this.users.set(userId, {
...userData,
registeredAt: Date.now(),
});
// ロールから権限を集計
const userPermissions = new Set();
roleIds.forEach(roleId => {
const rolePerms = this.roles.get(roleId);
if (rolePerms) {
rolePerms.forEach(perm => userPermissions.add(perm));
}
});
this.permissions.set(userId, userPermissions);
}
hasPermission(userId, permission) {
const userPerms = this.permissions.get(userId);
return userPerms ? userPerms.has(permission) : false;
}
updateUserRole(userId, newRoleIds) {
if (!this.users.has(userId)) {
throw new Error(`ユーザーが見つかりません: ${userId}`);
}
const userPermissions = new Set();
newRoleIds.forEach(roleId => {
const rolePerms = this.roles.get(roleId);
if (rolePerms) {
rolePerms.forEach(perm => userPermissions.add(perm));
}
});
this.permissions.set(userId, userPermissions);
}
getUserInfo(userId) {
return {
user: this.users.get(userId),
permissions: Array.from(this.permissions.get(userId) || []),
};
}
getAllUsers() {
const result = [];
this.users.forEach((userData, userId) => {
result.push({
userId,
...userData,
permissions: Array.from(this.permissions.get(userId) || []),
});
});
return result;
}
}
// 使用例
const manager = new UserPermissionManager();
manager.addRole('admin', ['read', 'write', 'delete']);
manager.addRole('user', ['read']);
manager.registerUser('user1', { name: '太郎', email: 'taro@example.com' }, ['admin']);
manager.registerUser('user2', { name: '花子', email: 'hanako@example.com' }, ['user']);
console.log(manager.hasPermission('user1', 'write')); // true
console.log(manager.hasPermission('user2', 'write')); // false
console.log(manager.getAllUsers());
TypeScriptでの型安全な実装
実務ではTypeScriptを使う場合も多いです。Mapをジェネリクスで型安全に扱いましょう。
interface CacheEntry {
data: T;
timestamp: number;
ttl: number; // Time to live (ミリ秒)
}
class TTLCache {
private cache: Map>;
private cleanupInterval: NodeJS.Timeout | null = null;
constructor(private defaultTTL: number = 60000) {
this.cache = new Map();
}
set(key: K, value: V, ttl?: number): void {
this.cache.set(key, {
data: value,
timestamp: Date.now(),
ttl: ttl ?? this.defaultTTL,
});
}
get(key: K): V | undefined {
const entry = this.cache.get(key);
if (!entry) return undefined;
const isExpired = Date.now() - entry.timestamp > entry.ttl;
if (isExpired) {
this.cache.delete(key);
return undefined;
}
return entry.data;
}
has(key: K): boolean {
return this.get(key) !== undefined;
}
delete(key: K): boolean {
return this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
startAutoCleanup(interval: number = 30000): void {
this.cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
}
}
}, interval);
}
stopAutoCleanup(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
}
// 使用例
interface User {
id: number;
name: string;
email: string;
}
const userCache = new TTLCache(30000); // 30秒のTTL
userCache.set(1, { id: 1, name: '太郎', email: 'taro@example.com' });
console.log(userCache.get(1)); // User オブジェクト
よくある応用パターン
パターン1: Mapのフィルタリングと変換
const dataMap = new Map([
['user1', { name: '太郎', age: 30, active: true }],
['user2', { name: '花子', age: 25, active: false }],
['user3', { name: '次郎', age: 35, active: true }],
]);
// アクティブなユーザーだけを抽出
function filterMap(map, predicate) {
const result = new Map();
for (const [key, value] of map) {
if (predicate(value, key)) {
result.set(key, value);
}
}
return result;
}
const activeUsers = filterMap(dataMap, user => user.active);
console.log(activeUsers);
// Mapの値を変換
function mapValues(map, transform) {
const result = new Map();
for (const [key, value] of map) {
result.set(key, transform(value, key));
}
return result;
}
const userNames = mapValues(dataMap, user => user.name);
console.log(userNames); // Map { 'user1' => '太郎', 'user2' => '花子', 'user3' => '次郎' }
パターン2: ネストされたMapの管理
class NestedDataStore {
constructor() {
this.store = new Map(); // category => Map(id => data)
}
set(category, id, value) {
if (!this.store.has(category)) {
this.store.set(category, new Map());
}
this.store.get(category).set(id, value);
}
get(category, id) {
const categoryMap = this.store.get(category);
return categoryMap ? categoryMap.get(id) : undefined;
}
getByCategory(category) {
const categoryMap = this.store.get(category);
if (!categoryMap) return [];
return Array.from(categoryMap.values());
}
delete(category, id) {
const categoryMap = this.store.get(category);
if (categoryMap) {
categoryMap.delete(id);
}
}
getStats() {
const stats = {};
for (const [category, dataMap] of this.store) {
stats[category] = dataMap.size;
}
return stats;
}
}
// 使用例
const store = new NestedDataStore();
store.set('products', 1, { name: '商品A', price: 1000 });
store.set('products', 2, { name: '商品B', price: 2000 });
store.set('orders', 1, { orderId: 'ORD001', total: 3000 });
console.log(store.getByCategory('products'));
console.log(store.getStats()); // { products: 2, orders: 1 }
パターン3: Mapをクエリ結果としてDBから取得する
// 実際のDB操作(疑似コード)を想定
class DatabaseAccessor {
async queryToMap(sql, keyField) {
// DBから結果を取得(実際はデータベースドライバを使用)
const results = await this.executeQuery(sql);
const resultMap = new Map();
results.forEach(row => {
resultMap.set(row[keyField], row);
});
return resultMap;
}
async executeQuery(sql) {
// 疑似実装
return [
{ id: 1, name: '商品A', category: 'electronics' },
{ id: 2, name: '商品B', category: 'electronics' },
{ id: 3, name: '商品C', category: 'clothing' },
];
}
}
// 使用例
const db = new DatabaseAccessor();
db.queryToMap('SELECT * FROM products', 'id').then(productMap => {
console.log(productMap.get(1)); // { id: 1, name: '商品A', ... }
});
注意点
1. メモリリークの危険性
Mapに登録したオブジェクトは、削除されるまでメモリに残ります。長時間動作するアプリケーションでは、不要なエントリの削除が重要です。
// 注意が必要な例
const cache = new Map();
let requestCount = 0;
async function fetchData(url) {
requestCount++;
const cacheKey = url + '_' + requestCount;
// 同じURLへの複数リクエストでメモリが増加する
cache.set(cacheKey, await fetch(url).then(r => r.json()));
}
// 改善版:TTLを設定するか定期的にクリア
function setupCacheCleanup(cache, maxSize = 1000) {
setInterval(() => {
if (cache.size > maxSize) {
// 最も古いエントリを削除(Mapは挿入順を保持)
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}
}, 60000);
}
2. キーの比較はReference比較
オブジェクトをキーとして使う場合、同じ内容でも別のインスタンスではマッチしません。
const map = new Map();
const obj1 = { id: 1 };
const obj2 = { id: 1 };
map.set(obj1, 'value1');
console.log(map.get(obj1)); // 'value1'
console.log(map.get(obj2)); // undefined (異なるインスタンス)
// 文字列キーを使う方が安全な場合も多い
const safeMap = new Map();
safeMap.set(JSON.stringify({ id: 1 }), 'value1');
console.log(safeMap.get(JSON.stringify({ id: 1 }))); // 'value1'
3. JSON化できない
Mapは直接JSON化できません。配列に変換してからJSON化する必要があります。
const map = new Map([
['key1', 'value1'],
['key2', 'value2'],
]);
// JSON化する場合
const jsonString = JSON.stringify(Array.from(map));
console.log(jsonString); // [[\"key1\",\"value1\"],[\"key2\",\"value2\"]]
// JSON から Map に復元
const restoredMap = new Map(JSON.parse(jsonString));
console.log(restoredMap.get('key1')); // 'value1'
4. for…of ループの最適化
大量のデータを処理する場合は、forEach よりも for…of の方が高速な場合があります。
const largeMap = new Map();
for (let i = 0; i < 100000; i++) {
largeMap.set(i, `value${i}`);
}
// パターン1: forEach(遅い可能性)
console.time('forEach');
largeMap.forEach((value, key) => {
// 処理
});
console.timeEnd('forEach');
// パターン2: for...of with entries(通常は高速)
console.time('for...of');
for (const [key, value] of largeMap) {
// 処理
}
console.timeEnd('for...of');
Python での参考実装
JavaScriptと同じパターンをPythonで実装する場合のリファレンスです。
from datetime import datetime, timedelta
from typing import TypeVar, Generic, Dict, Any, Optional
T = TypeVar('T')
K = TypeVar('K')
class TTLCache(Generic[K, T]):
def __init__(self, default_ttl: int = 60000):
self.cache: Dict[K, Dict[str, Any]] = {}
self.default_ttl = default_ttl # ミリ秒
def set(self, key: K, value: T, ttl: Optional[int] = None) -> None:
self.cache[key] = {
'data': value,
'timestamp': datetime.now(),
'ttl': ttl or self.default_ttl,
}
def get(self, key: K) -> Optional[T]:
if key not in self.cache:
return None
entry = self.cache[key]
elapsed = (datetime.now() - entry['timestamp']).total_seconds() * 1000
if elapsed > entry['ttl']:
del self.cache[key]
return None
return entry['data']
def has(self, key: K) -> bool:
return self.get(key) is not None
def delete(self, key: K) -> bool:
if key in self.cache:
del self.cache[key]
return True
return False
def clear(self) -> None:
self.cache.clear()
# 使用例
cache = TTLCache[int, str](30000)
cache.set(1, 'value1')
print(cache.get(1)) # 'value1'
まとめ
JavaScriptの Mapは、適切に活用することで以下のメリットが得られます:
- パフォーマンス向上:大量データの検索が O(1) で実行される
- コードの可読性向上:意図が明確な実装が可能
- メモリ効率:任意の型をキーに使え、不要な変換が不要
- データ構造の表現力:複雑な関連性を直感的に表現できる
実務では、単なるデータ保持だけでなく、キャッシング、イベント管理、権限管理など様々なシーンで活躍します。ただし、メモリリークやリファレンス比較の落とし穴に注意が必要です。本記事で紹介したパターンを参考に、あなたのプロジェクトに合わせて応用してください。
最後に、Map の使用を検討する前に、その用途が本当に Map に適しているのかを確認することが重要です。シンプルなキー値マッピングであれば オブジェクトでも十分ですし、複雑なクエリが必要ならば Database の使用も視野に入れましょう。適切なツール選択こそが、保守性の高いコードの第一歩です。

