JavaScript Mapの実践的な使い方|業務で使えるサンプルコード集

未分類

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 の使用も視野に入れましょう。適切なツール選択こそが、保守性の高いコードの第一歩です。

タイトルとURLをコピーしました