using ломает GCСвязка WeakMap и
using с Symbol.dispose выглядит элегантно, но в production может обернуться утечками и гонками. Главная проблема — сборщик мусора не синхронизирован с вызовами деструкторов, что ломает интуитивное поведение.Грабли 1: Живой ключ в WeakMap
Код вида
cleanupMap.delete(this) внутри [Symbol.dispose] не гарантирует очистку. Объект жив на момент вызова деструктора, и запись в WeakMap может висеть, если ключ захвачен в замыкании или передан наружу. Лучше проверять через has() перед удалением — это не спасет от всех кейсов, но снизит риск.Грабли 2: Порядок финализации в WeakSet
Если несколько ресурсов с общими зависимостями финализируются через
using, порядок вызова Symbol.dispose не определен. Проверка pool.has() внутри деструктора может дать race condition — один ресурс уже удален, другой еще жив. В высоконагруженных системах это ведет к нестабильности.Грабли 3: Циклы через Symbol-ключи
Использование символа с собственным
[Symbol.dispose] как ключа в WeakMap, который чистится в деструкторе ресурса, вызывает циклический вызов и stack overflow. Пример: const cleanupSymbol = Symbol('cleanup'); cleanupSymbol[Symbol.dispose] = () => map.delete(cleanupSymbol);. Такой паттерн лучше вообще избегать.Производственные советы:
* Вместо WeakMap в деструкторах используй обычные
Map с ручной очисткой — это предсказуемее.* Для WeakSet не полагайся на консистентность между деструкторами — используй отдельные флаги состояния.
* Тестируй на разных движках JS: V8 и SpiderMonkey финализируют по-разному, баги всплывают не сразу.
Вывод: WeakMap и WeakSet в паре с
using — мощный, но опасный инструмент, требующий явного контроля жизненного цикла ссылок, иначе GC и деструкторы превращают чистый код в источник трудноуловимых багов.