Структури даних: WeakMap & WeakSet

Структури даних: WeakMap & WeakSet

  • 11 квітня
  • читати 20 хв
Володимир Шайтан
Володимир Шайтан Senior Full Stack Developer у UKEESS Software House, Викладач Комп'ютерної школи Hillel.
Аліна Мица
Аліна Мица Front End Developer

Зовсім недавно ми з вами детально розібрали Map і Set. Там ми зʼясували, що Map — це структура даних, яка чудово підходить для зберігання пар ключ-значення, де ключ може бути будь-якого типу, а Set — це колекція унікальних значень. І Map, і Set часто виступають більш ефективними, ніж звичні нам Object і Array.

Але, звісно ж, на цьому JavaScript не обмежується, адже крім Map і Set існують їх дуже корисні «слабкі» аналоги WeakMap і WeakSet.

Тож якщо ви досі не читали попередню статтю, то я щиро рекомендую зробити це до ознайомлення з цією статею, бо конкретно в цій я буду дуже часто робити відсилання на попередню статтю:

Map & Set

ВСТУП

У роботі над великими проєктами може бути (частіше за все є) купа різних за специфічністю завдань, які стосуються керуванням памʼяттю, оптимізації роботи з обʼєктами й загалом з даними, уникнення витоків памʼяті та зміни життєвого циклу тощо.

Маю пояснити, що таке ті витоки даних, щоб далі було простіше розуміти.

Виток даних — це проблема в програмуванні, коли дані або об’єкти зберігаються в пам’яті, навіть після того, як вони більше не використовуються або не потрібні. 

у результаті цього програма не може звільнити пам’ять, яка була зайнята старими даними, що призводить до збільшення споживання пам’яті й може викликати її переповнення. Витоки даних можуть спричинити серйозні проблеми з продуктивністю програми, тому важливо вчасно видаляти непотрібні об’єкти й використовувати механізми управління пам’яттю.

Буває таке, що деякі дані вам потрібно залишати, так скажемо «під капотом», не показуючи їх на загал; також деяким з ваших обʼєктів потрібно просто «зникнути» рівно в той момент, коли вони стануть непотрібними для подальшої логіки, тобто «живуть» ці обʼєкти доволі короткий час.

От саме в ці моменти для роботи найкраще підходять WeakMap і WeakSet. Тож почнімо розбиратися що тут і до чого.

WEAKMAP

Доволі часто WeakMap називають сховищем для слабких ключів. І слово «слабкий» у цій назві зовсім не означає нічого поганого, бо це навпаки полегшує нам деяку роботу. Трошки про слабкі звʼязки.

Слабкі звʼязки (weak references) — це спеціальний тип посилань у програмуванні, який дозволяє обʼєктам бути доступними для використання, але не утримує їх у памʼяті, коли на них більше не існує сильних посилань. Якщо обʼєкт є доступним лише через слабке посилання, збирач сміття автоматично видаляє його з пам’яті, коли він більше не потрібен.

Це дозволяє уникнути витоків памʼяті, оскільки обʼєкти, що більше не використовуються, будуть видалені без втручання розробника. Слабкі звʼязки часто застосовуються в структурах даних типу WeakMap і WeakSet, про які ми сьогодні говоримо.

Як можна зрозуміти з назви, WeakMap схожий на Map тим, що також зберігає «ключ: значення», але, само собою, тут є ряд відмінностей (інакше навіщо придумувати дві однакові штуки, правда?):

  • Ключі у WeakMap можуть бути лише типу Object:

Тобто якщо ви спробуєте використати будь-який примітив як ключ, то це спровокує виникнення помилки. А повʼязано це з тим, що WeakMap дуже пасує до сценаріїв, де потрібно швиденько видалити обʼєкти, які більше не будуть використовуватися (далі стане зрозуміліше).

  • Слабка привʼязка до обʼєктів:

Наприклад, у Map обʼєкти, які є ключами, залишаються в памʼяті доти, доки вони є ключами. А у WeakMap такого немає: як тільки ключ більше не потрібен — це значить, що й обʼєкт також не потрібен. Цей обʼєкт перестає бути доступним у коді, бо автоматично видаляється з WeakMap.

Звісно, у WeakMap є свої особливості, такі як недоступність size й ітераторів. Це означає, що, на відміну від Map, у WeakMap немає таких методів, за допомогою яких можна отримати всі ключі, всі значення чи всі пари. Зроблено це для того, щоб захиститися від витоку даних.

WeakMap має всього чотири методи:

  • set (key, value) — дозволяє додати пару «ключ: значення». Якщо ключ уже існує, його значення буде оновлено;
  • get (key) — повертає значення за заданим ключем, або undefined, якщо ключ не знайдено;
  • delete (key) — видаляє пару за ключем;
  • has (key) — перевіряє, чи є такий ключ у колекції.

ЯК МОЖНА ВИКОРИСТОВУВАТИ WEAKMAP?

Розглянемо ситуацію, де ми хочемо зберегти приватні дані обʼєкта. У цьому прикладі приватні дані користувача зберігаються в WeakMap і ми не можемо отримати доступ до них безпосередньо.

const privateData = new WeakMap();

class User {
  constructor(name) {
    privateData.set(this, { name });
  }

  getName() {
    return privateData.get(this).name;
  }
}

const user = new User("Alice");
console.log(user.getName()); // Alice

// Якщо об'єкт user більше не використовується, його дані в WeakMap теж зникнуть.

Кешування результатів функції. Маємо функцію для обчислення деяких складних даних, наприклад, результатів певних запитів до бази даних чи дорогих обчислень.

const cache = new WeakMap();

function expensiveComputation(obj) {
  // Якщо результат обчислення вже зберігається в кеші, просто повертаємо його.
  if (cache.has(obj)) {
    console.log('Отримано результат з кешу');
    return cache.get(obj);
  }

  // Інакше, виконуємо складне обчислення.
  console.log('Обчислюємо нові дані...');
  const result = obj.value * 10;  // Уявімо, що це складне обчислення

  // Зберігаємо результат обчислення в кеші.
  cache.set(obj, result);
  return result;
}

const obj1 = { value: 5 };
const obj2 = { value: 7 };

console.log(expensiveComputation(obj1)); // Обчислюємо нові дані...
console.log(expensiveComputation(obj1)); // Отримано результат з кешу
console.log(expensiveComputation(obj2)); // Обчислюємо нові дані...

WEAKSET

WeakSet — це колекція обʼєктів, у якій кожен обʼєкт може зʼявитися лише один раз. Це про унікальні ключі з обмеженим доступом. Також, як і у випадку з WeakMap, ця структура використовує слабкі посилання, тому об’єкти, які більше не використовуються, будуть автоматично видалені.

Відмінності від Set:

  • WeakSet може зберігати тільки обʼєкти, а примітиви не підтримуються ним зовсім;
  • так само слабка привʼязка — якщо обʼєкт не використовується, то він видаляється;
  • немає способу отримати список усіх елементів (values(), keys(), entries() відсутні);
  • так само як і у WeakMap, немає ітераторів, і це робить WeakSet більш захищеним.

Методи WeakSet:

  • add (value) — додати обʼєкт до колекції;
  • has (value) — перевіряє, чи є обʼєкт у колекції;
  • delete (value) — видаляє обʼєкт із колекції.

ЯК ВИКОРИСТОВУВАТИ WEAKSET?

Розглянемо ситуацію, де WeakSet використовується для відстеження оброблених обʼєктів. У цьому прикладі WeakSet автоматично видалить обʼєкти, коли вони перестануть використовуватися:

const processed = new WeakSet();

function process(obj) {
  if (processed.has(obj)) {
    console.log("Об'єкт вже оброблений");
    return;
  }

  console.log("Обробка об'єкта...");
  processed.add(obj);
}

const task1 = {};
const task2 = {};

process(task1); // Обробка об'єкта...
process(task1); // Об'єкт вже оброблений
process(task2); // Обробка об'єкта...

Відстеження активних користувачів. Нам потрібно відстежувати активних користувачів на сайті. При цьому важливо, щоб коли користувач вийде з сайту і його об’єкт більше не буде використовуватися, він автоматично видалявся з колекції відстежених користувачів, щоб не виникало витоків пам’яті.

const activeUsers = new WeakSet();

class User {
  constructor(name) {
    this.name = name;
    this.loggedIn = false;
  }

  login() {
    this.loggedIn = true;
    activeUsers.add(this);
  }

  logout() {
    this.loggedIn = false;
    activeUsers.delete(this);
  }
}

const user1 = new User('Alice');
const user2 = new User('Bob');

user1.login();
user2.login();

console.log(activeUsers.has(user1)); // true
console.log(activeUsers.has(user2)); // true

// Користувач 1 виходить з сайту
user1.logout();

console.log(activeUsers.has(user1)); // false
console.log(activeUsers.has(user2)); // true

// user2 більше не потрібен - він буде автоматично видалений з activeUsers, коли перестане використовуватися

ПОРІВНЯННЯ

Тепер нарешті, завершуючи дві статті про ці конкретні структурам даних, хочу запропонувати вашій увазі порівняльну таблицю:

ХарактеристикаMapSetWeakMapWeakSet
Тип даних для ключів/значеньКлючі: будь-які типи. Значення: будь-які типи.Значення: будь-які типи.Ключі: тільки об’єкти. Значення: будь-які типи.Значення: тільки об’єкти.
Унікальність ключів/значеньУнікальні ключі. Значення можуть повторюватися.Унікальні значення.Унікальні ключі (об’єкти). Значення можуть повторюватись.Унікальні об’єкти.
Доступ до данихМожна отримати ключі, значення або і те, і те через ітерацію або методи (keys(), values(), entries()).Можна отримати всі значення через ітерацію або методи (values(), entries()).Доступ до елементів можливий тільки через ключ (методи: get(), set(), has(), delete()).Доступ до елементів тільки через об’єкти (методи: add(), has(), delete()).
ІтераціяТак (forEach(), for...of, keys(), values(), entries()).Так (forEach(), for...of, values(), entries()).Ні. Немає можливості ітерувати ключі чи значення.Ні. Ітерація недоступна.
Збирання сміттяКлючі та значення не видаляються автоматично.Значення не видаляються автоматично.Ключі видаляються автоматично, якщо на об’єкт немає інших посилань.Об’єкти видаляються автоматично, якщо на них немає інших посилань.
Методи додаванняset(key, value)add(value)set(key, value)add(value)
Методи перевіркиhas(key)has(value)has(key)has(value)
Методи видаленняdelete(key)delete(value)delete(key)delete(value)
Отримання значенняget(key)Немає.get(key)Немає.
Розмір колекціїsizesizeНемає. Розмір WeakMap не відображається.Немає. Розмір WeakSet не відображається.
Придатність для кешуванняМожна, але потрібно вручну видаляти невикористовувані дані.Непридатний через слабке управління ключами.Ідеально підходить, оскільки ключі автоматично видаляються, якщо об’єкт більше не використовується.Підходить для зберігання унікальних об’єктів без ризику витоків пам’яті.
Основні сценарії використанняЗберігання пар ключ-значення з будь-якими типами даних, ітерація даних.Зберігання унікальних значень будь-яких типів.Зберігання метаданих об’єктів, кешування, прив’язка приватних даних до об’єктів.Відстеження унікальних об’єктів, контроль доступу до об’єктів.

ВИСНОВОК

Тепер, коли ми з вами познайомилися ближче з цими чотирма структурами даних, то ми можемо підсумувати.

WeakMap і WeakSet — це чудові інструменти, створені не для заміни Map і Set, а для розв'язання специфічних задач, пов’язаних із керуванням пам’яттю, збереженням приватності даних й оптимізацією роботи з об’єктами, адже їх особливості роблять їх незамінними у великих проєктах.

Також дуже важливо завжди тримати в голові їх обмеження, щоб використовувати все правильно й уникнути всяких «ой», коли доведеться виправляти помилки:)

Це точно було не відмовляння використовувати ці інструменти, бо без них іноді дуже складно написати класний і якісний код, який ми всі так сильно любимо.

На цьому моменті прощаємося і до зустрічі в цей самий час і в цьому самому місці!

Рекомендуємо публікації по темі