Пошук уроків, статей та іншого контенту
Розберете WeakMap і WeakSet для прив’язки даних до об’єктів без перешкод для автоматичного збирання сміття.
WeakMap і WeakSetWeakMap і WeakSet — спеціальні колекції JavaScript для роботи з об’єктами без утримання їх у пам’яті лише через наявність у колекції.
Основна ідея:
Map зберігає сильні посилання на ключі;
WeakMap не перешкоджає автоматичному збиранню сміття;
Set зберігає сильні посилання на значення;
WeakSet не перешкоджає збиранню об’єктів, доданих до нього.
Це корисно, коли потрібно прив’язати дані або стан до об’єкта, але не потрібно володіти цим об’єктом протягом усього часу життя програми.
const metadata = new WeakMap();
function attachMetadata(object, data) {
metadata.set(object, data);
}
let user = { name: "Olena" };
attachMetadata(user, {
lastAccess: Date.now(),
permissions: ["read", "write"]
});
console.log(metadata.get(user));
// { lastAccess: ..., permissions: ["read", "write"] }
user = null;
// Якщо інших сильних посилань на початковий об’єкт немає,
// об’єкт і пов’язані з ним дані можуть бути видалені збирачем сміття.Важливо: JavaScript не гарантує, коли саме відбудеться збирання сміття. Тому не можна використовувати WeakMap або WeakSet для логіки, яка залежить від моменту видалення об’єкта.
WeakMapWeakMap зберігає пари ключ — значення, але ключем може бути лише об’єкт або допустимий символ без глобальної реєстрації.
Найчастіший варіант використання — об’єкт як ключ:
const cache = new WeakMap();
const request = {
url: "/api/users",
method: "GET"
};
cache.set(request, {
status: "pending",
createdAt: Date.now()
});
console.log(cache.get(request));
console.log(cache.has(request));
cache.delete(request);
console.log(cache.has(request)); // falseWeakMapset(key, value) — додає або оновлює значення;
get(key) — повертає значення або undefined;
has(key) — перевіряє наявність ключа;
delete(key) — видаляє пару та повертає true або false.
Метод set повертає сам WeakMap, тому виклики можна ланцюжити:
const state = new WeakMap();
const button = {};
state
.set(button, { disabled: false })
.set({ id: 1 }, { active: true });У другому виклику створюється анонімний об’єкт, на який після завершення виразу більше немає сильних посилань. Такий запис рідко має практичну цінність, оскільки отримати значення за цим ключем уже неможливо.
WeakMap мають бути об’єктамиОб’єкти мають ідентичність. Два об’єкти з однаковими властивостями все одно є різними ключами:
const metadata = new WeakMap();
const first = { id: 1 };
const second = { id: 1 };
metadata.set(first, "first object");
console.log(metadata.get(first)); // "first object"
console.log(metadata.get(second)); // undefinedПримітивні значення не мають такої об’єктної ідентичності:
const metadata = new WeakMap();
metadata.set("user-id", "data");
// TypeError: Invalid value used as weak map keyДля примітивних ключів використовуйте звичайний Map:
const values = new Map();
values.set("user-id", "data");
console.log(values.get("user-id")); // "data"У сучасних реалізаціях JavaScript ключами WeakMap також можуть бути символи, створені не через глобальний реєстр Symbol.for. Проте для переносимості та зрозумілості коду зазвичай використовують об’єкти як ключі.
WeakMap і автоматичне збирання сміттяРозглянемо різницю між Map і WeakMap:
const regularMap = new Map();
const weakMap = new WeakMap();
let regularObject = { type: "regular" };
let weakObject = { type: "weak" };
regularMap.set(regularObject, "data");
weakMap.set(weakObject, "data");
regularObject = null;
weakObject = null;Після присвоєння null:
початковий об’єкт у regularMap усе ще доступний як ключ;
початковий об’єкт у weakMap може бути видалений збирачем сміття.
У regularMap ключ залишається сильним посиланням:
console.log(regularMap.size); // 1Для weakMap немає аналогічного способу перевірити кількість елементів.
Це зроблено навмисно. Якби можна було отримати розмір або перебрати всі ключі WeakMap, програма могла б визначати, чи вже видалився певний об’єкт. Така можливість зробила б поведінку залежною від внутрішньої роботи збирача сміття.
WeakMapДо появи приватних полів класу WeakMap часто використовували для приховування внутрішнього стану екземплярів.
const privateState = new WeakMap();
class Counter {
constructor(initialValue = 0) {
privateState.set(this, {
value: initialValue
});
}
increment() {
const state = privateState.get(this);
state.value += 1;
}
getValue() {
return privateState.get(this).value;
}
}
const counter = new Counter(10);
counter.increment();
counter.increment();
console.log(counter.getValue()); // 12
console.log(counter.value); // undefinedСтан зберігається окремо від екземпляра:
користувач не може напряму звернутися до counter.value;
властивість не потрапляє до Object.keys(counter);
стан автоматично стає недоступним, коли екземпляр більше не використовується.
Для нового коду часто краще застосовувати приватні поля класу:
class ModernCounter {
#value;
constructor(initialValue = 0) {
this.#value = initialValue;
}
increment() {
this.#value += 1;
}
getValue() {
return this.#value;
}
}Проте WeakMap усе ще корисний, коли:
стан потрібно зберігати поза класом;
клас або об’єкт не можна змінити;
потрібно додати метадані об’єктам сторонньої бібліотеки;
одна структура даних обслуговує об’єкти різних типів.
Іноді потрібно пов’язати додаткову інформацію з об’єктом, але не можна або не варто додавати власні властивості:
const validationState = new WeakMap();
function validateForm(form) {
const errors = [];
if (!form.email.includes("@")) {
errors.push("Некоректна електронна пошта");
}
validationState.set(form, {
valid: errors.length === 0,
errors
});
return errors.length === 0;
}
const form = {
email: "user@example.com"
};
console.log(validateForm(form)); // true
console.log(validationState.get(form)); // { valid: true, errors: [] }Якщо form більше ніде не використовується, його метадані також не повинні залишатися в пам’яті через validationState.
WeakMap зручно застосовувати для кешування результатів функцій, які приймають об’єкти:
const normalizedCache = new WeakMap();
function normalizeUser(user) {
if (normalizedCache.has(user)) {
return normalizedCache.get(user);
}
const normalized = {
id: Number(user.id),
name: user.name.trim().toLowerCase()
};
normalizedCache.set(user, normalized);
return normalized;
}
const user = {
id: "42",
name: " Alice "
};
const firstResult = normalizeUser(user);
const secondResult = normalizeUser(user);
console.log(firstResult); // { id: 42, name: "alice" }
console.log(firstResult === secondResult); // trueТакий кеш прив’язаний до самого об’єкта. Якщо об’єкт більше не потрібен, кеш не має бути причиною його збереження.
Кеш ключується за ідентичністю об’єкта, а не за його вмістом:
const firstUser = { id: 1, name: "Alice" };
const secondUser = { id: 1, name: "Alice" };
console.log(firstUser === secondUser); // falseДля firstUser і secondUser будуть створені різні записи, хоча властивості однакові.
Якщо кеш потрібно будувати за значенням, використовуйте Map і продумайте серіалізацію або стабільний ключ.
WeakSetWeakSet зберігає лише об’єкти без пов’язаних значень.
const processedObjects = new WeakSet();
const message = {
id: 1,
text: "Hello"
};
processedObjects.add(message);
console.log(processedObjects.has(message)); // true
processedObjects.delete(message);
console.log(processedObjects.has(message)); // falseОсновні методи:
add(value) — додає об’єкт;
has(value) — перевіряє наявність;
delete(value) — видаляє об’єкт.
На відміну від Set, WeakSet не має:
size;
keys();
values();
entries();
forEach();
можливості перебрати всі елементи.
WeakSet підходить для захисту від повторної обробки об’єктів:
const visited = new WeakSet();
function processNode(node) {
if (visited.has(node)) {
return;
}
visited.add(node);
console.log(`Обробка вузла: ${node.name}`);
for (const child of node.children) {
processNode(child);
}
}
const tree = {
name: "root",
children: []
};
const child = {
name: "child",
children: []
};
tree.children.push(child, child);
processNode(tree);
// Обробка вузла: root
// Обробка вузла: childДругий запис child не обробляється повторно, оскільки об’єкт уже є у visited.
Це особливо корисно для:
обходу графів із циклами;
виявлення вже оброблених вузлів;
запобігання повторній ініціалізації;
тимчасового маркування об’єктів.
WeakSet для перевірки належностіІноді потрібно знати, чи об’єкт створений або зареєстрований конкретним компонентом:
const registeredComponents = new WeakSet();
function registerComponent(component) {
registeredComponents.add(component);
}
function isRegistered(component) {
return registeredComponents.has(component);
}
const component = {
render() {
return "<div>Компонент</div>";
}
};
registerComponent(component);
console.log(isRegistered(component)); // true
console.log(isRegistered({})); // falseWeakSet зберігає лише факт належності. Якщо разом із об’єктом потрібно зберігати дані, варто використати WeakMap:
const components = new WeakMap();
function registerComponent(component) {
components.set(component, {
registeredAt: Date.now(),
version: 1
});
}
function getComponentInfo(component) {
return components.get(component);
}Map і WeakMapMap, якщо:потрібно перебирати всі записи;
потрібен size;
ключами можуть бути примітиви;
записи мають жити незалежно від зовнішніх посилань на ключі;
потрібно явно керувати часом життя кешу.
WeakMap, якщо:ключем є об’єкт;
дані логічно належать конкретному об’єкту;
не потрібно перебирати ключі;
потрібно уникнути утримання об’єкта в пам’яті;
метадані мають зникати разом із ключем.
Наприклад:
const usersById = new Map();
const userMetadata = new WeakMap();
const user = {
id: 10,
name: "Olena"
};
usersById.set(user.id, user);
userMetadata.set(user, {
lastLogin: Date.now()
});У цьому прикладі:
Map дає змогу знайти користувача за ідентифікатором;
WeakMap зберігає службові дані, пов’язані саме з об’єктом користувача.
Set і WeakSetSet, якщо:потрібно перебирати всі значення;
потрібен size;
значеннями можуть бути примітиви;
множина є частиною явного стану програми.
WeakSet, якщо:потрібна лише перевірка has;
значеннями є об’єкти;
множина не повинна утримувати об’єкти в пам’яті;
порядок і кількість елементів не мають значення.
«Слабке» не означає, що об’єкт видаляється одразу після додавання до WeakMap або WeakSet.
Об’єкт може бути видалений лише тоді, коли на нього більше немає доступних сильних посилань:
const records = new WeakMap();
let record = {
status: "active"
};
records.set(record, {
log: "created"
});
// Сильне посилання все ще існує через змінну record.
console.log(records.has(record)); // true
record = null;
// Тепер об'єкт може бути зібраний,
// якщо інших сильних посилань немає.Якщо десь залишилося інше посилання, об’єкт не буде видалений:
const records = new WeakMap();
let record = {
status: "active"
};
const anotherReference = record;
records.set(record, "metadata");
record = null;
// anotherReference усе ще посилається на об’єкт.
console.log(records.has(anotherReference)); // trueWeakMap і WeakSetНе можна покладатися на кількість елементів:
const cache = new WeakMap();
// У WeakMap немає властивості size.
console.log(cache.size); // undefinedНе можна перебирати елементи:
const objects = new WeakSet();
objects.add({ id: 1 });
// TypeError: objects is not iterable
for (const object of objects) {
console.log(object);
}Не можна отримати всі ключі WeakMap:
const metadata = new WeakMap();
// У WeakMap немає keys(), values(), entries() і forEach().Якщо перебір потрібен за вимогами задачі, використовуйте Map або Set.
FinalizationRegistry: не заміна WeakMapFinalizationRegistry дає змогу зареєструвати callback, який потенційно може виконатися після збирання об’єкта:
const registry = new FinalizationRegistry((resourceName) => {
console.log(`Об'єкт більше не використовується: ${resourceName}`);
});
let resource = {
name: "temporary-resource"
};
registry.register(resource, resource.name);
resource = null;Але:
callback може виконатися набагато пізніше;
callback може не виконатися до завершення процесу;
порядок виконання не гарантується;
його не можна використовувати для критично важливого очищення ресурсів.
Для файлів, мережевих з’єднань, транзакцій та інших ресурсів потрібне явне очищення:
class Connection {
#closed = false;
close() {
if (this.#closed) {
return;
}
this.#closed = true;
// Явне закриття з'єднання.
}
}WeakMap розв’язує проблему прив’язки даних до об’єктів, а не проблему детермінованого керування зовнішніми ресурсами.
WeakMapconst map = new WeakMap();
map.set(123, "value");
// TypeErrorДля чисел, рядків і булевих значень використовуйте Map.
sizeconst set = new WeakSet();
set.add({});
console.log(set.size); // undefinedWeakSet навмисно не повідомляє кількість елементів.
WeakMap і WeakSet не підтримують ітерацію. Якщо логіці потрібен список усіх записів, це ознака, що потрібна звичайна колекція.
WeakMap як звичайного кешуЯкщо ключем є тимчасово створений об’єкт, знайти кешований результат повторно буде неможливо:
const cache = new WeakMap();
function getData() {
const key = { id: 1 };
if (!cache.has(key)) {
cache.set(key, "result");
}
return cache.get(key);
}На кожен виклик створюється новий ключ. Щоб кеш працював, потрібно зберігати той самий об’єкт або використовувати стабільний примітивний ключ із Map.
Після того як змінна отримала значення null, об’єкт не обов’язково негайно зникає. Збирач сміття працює у невизначений момент, коли середовище виконання вважає це доцільним.
WeakMap послаблює лише посилання на ключ. Значення може містити сильне посилання на цей самий ключ або на інші об’єкти:
const data = new WeakMap();
const object = {};
const value = {
owner: object
};
data.set(object, value);Така циклічна структура сама по собі не створює витік, якщо весь цикл недоступний із коренів програми. Але значення може утримувати інші великі об’єкти, які залишаться живими, доки живий ключ.
Перед вибором колекції поставте такі запитання:
Ключ або значення є об’єктом?
Потрібно зберігати дані саме за ідентичністю об’єкта?
Потрібно перебирати всі записи?
Потрібен розмір колекції?
Чи повинна колекція не перешкоджати збиранню об’єктів?
Якщо відповідь на останнє запитання позитивна, а ітерація не потрібна:
пари «об’єкт — дані» зберігайте у WeakMap;
лише належність об’єкта зберігайте у WeakSet.
WeakMap зберігає дані за об’єктними ключами.
WeakSet зберігає лише об’єкти та факт їхньої наявності.
Слабкі колекції не перешкоджають збиранню недоступних об’єктів.
WeakMap підходить для метаданих, приватного стану та кешів, пов’язаних з об’єктами.
WeakSet підходить для маркування оброблених або зареєстрованих об’єктів.
У слабких колекцій немає size та ітерації.
Момент збирання сміття не визначений, тому не можна будувати на ньому синхронну логіку.
Для примітивних ключів та ітерації використовуйте Map або Set.
WeakMap і WeakSet не замінюють явне звільнення зовнішніх ресурсів.