← JavaScript/WeakMap и WeakSet#109 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

WeakMap и WeakSet

В React каждый DOM-элемент может иметь связанные данные (обработчики, состояние). Хранить их в обычном Map опасно — элемент удалится из DOM, но данные останутся в памяти навсегда. WeakMap решает это: когда элемент удаляется, связанные данные автоматически очищаются сборщиком мусора.

Какую проблему решает

Если объект хранится в обычном Map или Set, он никогда не будет удалён сборщиком мусора, даже если нигде больше не используется. WeakMap и WeakSet хранят «слабые» ссылки — они не препятствуют сборке мусора.

На основе предыдущих уроков

  • Map/Set — обычные коллекции, их API
  • Классы — классы и экземпляры
  • Замыкания — замыкания для скрытия данных
  • Проблема обычных коллекций

    let user = { name: 'Иван' }
    const cache = new Map()
    cache.set(user, { data: 'кешированные данные' })
    
    user = null  // хотим удалить объект
    // НО: Map хранит сильную ссылку — объект остаётся в памяти!
    // cache ссылается на { name: 'Иван' } — GC не удалит его

    WeakMap — ключи только объекты

    В WeakMap ключами могут быть только объекты (не примитивы). Ссылка «слабая» — если объект-ключ нигде больше не используется, GC удаляет его вместе с данными в WeakMap:

    let user = { name: 'Иван' }
    const cache = new WeakMap()
    cache.set(user, { data: 'некие данные' })
    
    user = null
    // Теперь объект может быть удалён GC
    // Запись в WeakMap исчезнет автоматически — утечки памяти нет!

    WeakMap API

    const wm = new WeakMap()
    const key = {}
    
    wm.set(key, 'value')
    wm.get(key)          // 'value'
    wm.has(key)          // true
    wm.delete(key)       // true
    
    // Чего НЕТ в WeakMap:
    // wm.size      — нет (нельзя знать сколько записей)
    // wm.keys()    — нельзя итерировать
    // wm.forEach() — нет перебора

    Отсутствие итерации — намеренное: нельзя получить список ключей, которые уже могут быть удалены GC.

    Применение 1: приватные данные класса

    const _private = new WeakMap()
    
    class BankAccount {
      constructor(owner, balance) {
        _private.set(this, { balance, transactions: [] })
        this.owner = owner
      }
    
      deposit(amount) {
        const data = _private.get(this)
        data.balance += amount
        data.transactions.push({ type: 'deposit', amount })
      }
    
      get balance() {
        return _private.get(this).balance
      }
    }
    
    const acc = new BankAccount('Иван', 1000)
    acc.deposit(500)
    console.log(acc.balance)  // 1500
    console.log(acc._private) // undefined — данные недоступны снаружи!

    Применение 2: кеш без утечек памяти

    const cache = new WeakMap()
    
    function getMetadata(element) {
      if (!cache.has(element)) {
        cache.set(element, computeExpensiveData(element))
      }
      return cache.get(element)
    }
    // Когда element удалится из DOM — данные автоматически освободятся

    WeakSet

    WeakSet хранит только объекты, без дублей, не итерируем:

    const processed = new WeakSet()
    
    function processRequest(req) {
      if (processed.has(req)) return 'уже обработан'
      processed.add(req)
      // ... обработка
      return 'обработан'
    }
    // Когда объект req удаляется — запись в WeakSet исчезает сама

    Типичные ошибки

    Ошибка 1: попытка использовать примитив как ключ WeakMap

    const wm = new WeakMap()
    wm.set('строка', 'значение')  // TypeError: Invalid value used as weak map key
    wm.set(42, 'значение')        // TypeError!
    wm.set({}, 'значение')        // OK — объект
    
    // WeakMap принимает только объекты как ключи

    Ошибка 2: попытка проитерировать WeakMap

    const wm = new WeakMap()
    wm.set({}, 1)
    
    wm.size         // undefined — не существует
    for (const k of wm) {}  // TypeError: wm is not iterable
    
    // Это намеренное ограничение — используй Map если нужна итерация

    Ошибка 3: использование WeakMap вместо Map когда нужны примитивные ключи

    // Неправильно — userId это строка, не объект
    const userCache = new WeakMap()
    userCache.set('user-123', data)  // TypeError!
    
    // Правильно — для примитивных ключей используй Map
    const userCache2 = new Map()
    userCache2.set('user-123', data)  // OK

    В реальных проектах

  • React: хранение метаданных DOM-элементов (Fiber nodes)
  • Vue.js: реактивная система отслеживает объекты через WeakMap
  • MobX: WeakMap для хранения реактивного состояния объектов
  • Memoization: кеш результатов вычислений с объектами как ключами
  • Примеры

    WeakMap для приватных данных банковского счёта и кеш вычислений

    // 1. WeakMap как хранилище приватных данных экземпляров
    const _private = new WeakMap()
    
    class BankAccount {
      constructor(owner, balance) {
        _private.set(this, { balance, transactions: [] })
        this.owner = owner
      }
    
      deposit(amount) {
        if (amount <= 0) throw new Error('Сумма должна быть положительной')
        const data = _private.get(this)
        data.balance += amount
        data.transactions.push({ type: 'deposit', amount, date: new Date().toLocaleDateString() })
      }
    
      withdraw(amount) {
        const data = _private.get(this)
        if (amount > data.balance) throw new Error('Недостаточно средств')
        data.balance -= amount
        data.transactions.push({ type: 'withdraw', amount, date: new Date().toLocaleDateString() })
      }
    
      get balance() { return _private.get(this).balance }
      get transactions() { return [..._private.get(this).transactions] }
    }
    
    const acc = new BankAccount('Иван', 1000)
    acc.deposit(500)
    acc.withdraw(200)
    console.log(acc.balance)              // 1300
    console.log(acc.transactions.length)  // 2
    console.log(acc._private)             // undefined — данные скрыты!
    
    try {
      acc.withdraw(5000)  // больше баланса
    } catch (e) {
      console.log(e.message)  // 'Недостаточно средств'
    }
    
    // 2. WeakMap как кеш — не блокирует сборку мусора
    const computeCache = new WeakMap()
    
    function getCachedStats(obj) {
      if (computeCache.has(obj)) {
        console.log('Из кеша')
        return computeCache.get(obj)
      }
      // Имитация сложного вычисления
      const stats = {
        keyCount: Object.keys(obj).length,
        hasNested: Object.values(obj).some(v => typeof v === 'object' && v !== null)
      }
      computeCache.set(obj, stats)
      return stats
    }
    
    const config = { host: 'localhost', port: 3000, db: { name: 'mydb' } }
    console.log(getCachedStats(config))  // { keyCount: 3, hasNested: true }
    console.log(getCachedStats(config))  // Из кеша: { keyCount: 3, hasNested: true }

    WeakMap и WeakSet

    В React каждый DOM-элемент может иметь связанные данные (обработчики, состояние). Хранить их в обычном Map опасно — элемент удалится из DOM, но данные останутся в памяти навсегда. WeakMap решает это: когда элемент удаляется, связанные данные автоматически очищаются сборщиком мусора.

    Какую проблему решает

    Если объект хранится в обычном Map или Set, он никогда не будет удалён сборщиком мусора, даже если нигде больше не используется. WeakMap и WeakSet хранят «слабые» ссылки — они не препятствуют сборке мусора.

    На основе предыдущих уроков

  • Map/Set — обычные коллекции, их API
  • Классы — классы и экземпляры
  • Замыкания — замыкания для скрытия данных
  • Проблема обычных коллекций

    let user = { name: 'Иван' }
    const cache = new Map()
    cache.set(user, { data: 'кешированные данные' })
    
    user = null  // хотим удалить объект
    // НО: Map хранит сильную ссылку — объект остаётся в памяти!
    // cache ссылается на { name: 'Иван' } — GC не удалит его

    WeakMap — ключи только объекты

    В WeakMap ключами могут быть только объекты (не примитивы). Ссылка «слабая» — если объект-ключ нигде больше не используется, GC удаляет его вместе с данными в WeakMap:

    let user = { name: 'Иван' }
    const cache = new WeakMap()
    cache.set(user, { data: 'некие данные' })
    
    user = null
    // Теперь объект может быть удалён GC
    // Запись в WeakMap исчезнет автоматически — утечки памяти нет!

    WeakMap API

    const wm = new WeakMap()
    const key = {}
    
    wm.set(key, 'value')
    wm.get(key)          // 'value'
    wm.has(key)          // true
    wm.delete(key)       // true
    
    // Чего НЕТ в WeakMap:
    // wm.size      — нет (нельзя знать сколько записей)
    // wm.keys()    — нельзя итерировать
    // wm.forEach() — нет перебора

    Отсутствие итерации — намеренное: нельзя получить список ключей, которые уже могут быть удалены GC.

    Применение 1: приватные данные класса

    const _private = new WeakMap()
    
    class BankAccount {
      constructor(owner, balance) {
        _private.set(this, { balance, transactions: [] })
        this.owner = owner
      }
    
      deposit(amount) {
        const data = _private.get(this)
        data.balance += amount
        data.transactions.push({ type: 'deposit', amount })
      }
    
      get balance() {
        return _private.get(this).balance
      }
    }
    
    const acc = new BankAccount('Иван', 1000)
    acc.deposit(500)
    console.log(acc.balance)  // 1500
    console.log(acc._private) // undefined — данные недоступны снаружи!

    Применение 2: кеш без утечек памяти

    const cache = new WeakMap()
    
    function getMetadata(element) {
      if (!cache.has(element)) {
        cache.set(element, computeExpensiveData(element))
      }
      return cache.get(element)
    }
    // Когда element удалится из DOM — данные автоматически освободятся

    WeakSet

    WeakSet хранит только объекты, без дублей, не итерируем:

    const processed = new WeakSet()
    
    function processRequest(req) {
      if (processed.has(req)) return 'уже обработан'
      processed.add(req)
      // ... обработка
      return 'обработан'
    }
    // Когда объект req удаляется — запись в WeakSet исчезает сама

    Типичные ошибки

    Ошибка 1: попытка использовать примитив как ключ WeakMap

    const wm = new WeakMap()
    wm.set('строка', 'значение')  // TypeError: Invalid value used as weak map key
    wm.set(42, 'значение')        // TypeError!
    wm.set({}, 'значение')        // OK — объект
    
    // WeakMap принимает только объекты как ключи

    Ошибка 2: попытка проитерировать WeakMap

    const wm = new WeakMap()
    wm.set({}, 1)
    
    wm.size         // undefined — не существует
    for (const k of wm) {}  // TypeError: wm is not iterable
    
    // Это намеренное ограничение — используй Map если нужна итерация

    Ошибка 3: использование WeakMap вместо Map когда нужны примитивные ключи

    // Неправильно — userId это строка, не объект
    const userCache = new WeakMap()
    userCache.set('user-123', data)  // TypeError!
    
    // Правильно — для примитивных ключей используй Map
    const userCache2 = new Map()
    userCache2.set('user-123', data)  // OK

    В реальных проектах

  • React: хранение метаданных DOM-элементов (Fiber nodes)
  • Vue.js: реактивная система отслеживает объекты через WeakMap
  • MobX: WeakMap для хранения реактивного состояния объектов
  • Memoization: кеш результатов вычислений с объектами как ключами
  • Примеры

    WeakMap для приватных данных банковского счёта и кеш вычислений

    // 1. WeakMap как хранилище приватных данных экземпляров
    const _private = new WeakMap()
    
    class BankAccount {
      constructor(owner, balance) {
        _private.set(this, { balance, transactions: [] })
        this.owner = owner
      }
    
      deposit(amount) {
        if (amount <= 0) throw new Error('Сумма должна быть положительной')
        const data = _private.get(this)
        data.balance += amount
        data.transactions.push({ type: 'deposit', amount, date: new Date().toLocaleDateString() })
      }
    
      withdraw(amount) {
        const data = _private.get(this)
        if (amount > data.balance) throw new Error('Недостаточно средств')
        data.balance -= amount
        data.transactions.push({ type: 'withdraw', amount, date: new Date().toLocaleDateString() })
      }
    
      get balance() { return _private.get(this).balance }
      get transactions() { return [..._private.get(this).transactions] }
    }
    
    const acc = new BankAccount('Иван', 1000)
    acc.deposit(500)
    acc.withdraw(200)
    console.log(acc.balance)              // 1300
    console.log(acc.transactions.length)  // 2
    console.log(acc._private)             // undefined — данные скрыты!
    
    try {
      acc.withdraw(5000)  // больше баланса
    } catch (e) {
      console.log(e.message)  // 'Недостаточно средств'
    }
    
    // 2. WeakMap как кеш — не блокирует сборку мусора
    const computeCache = new WeakMap()
    
    function getCachedStats(obj) {
      if (computeCache.has(obj)) {
        console.log('Из кеша')
        return computeCache.get(obj)
      }
      // Имитация сложного вычисления
      const stats = {
        keyCount: Object.keys(obj).length,
        hasNested: Object.values(obj).some(v => typeof v === 'object' && v !== null)
      }
      computeCache.set(obj, stats)
      return stats
    }
    
    const config = { host: 'localhost', port: 3000, db: { name: 'mydb' } }
    console.log(getCachedStats(config))  // { keyCount: 3, hasNested: true }
    console.log(getCachedStats(config))  // Из кеша: { keyCount: 3, hasNested: true }

    Задание

    Реализуй функцию createPrivateStore() которая использует WeakMap для хранения приватных данных объектов. Метод set(obj, key, value) сохраняет значение, get(obj, key) читает, has(obj, key) проверяет наличие, delete(obj, key) удаляет конкретный ключ.

    Подсказка

    set: store.get(obj)[key] = value. get: if (!store.has(obj)) return undefined; return store.get(obj)[key]. has: store.has(obj) && key in store.get(obj).

    Загружаем среду выполнения...
    Загружаем AI-помощника...