Введение

Я работаю реактивным разработчиком около трех лет и за все это время не видел, чтобы кто-то использовал Map или Set. Тем не менее, мне часто приходится иметь дело со Swift/Kotlin, и я время от времени сталкиваюсь с использованием этих коллекций. Вот я и решил задать вопрос: почему никто не использует Map/Set?

Начнем с карты.

карта

Map — это набор ключей/значений, как и Object. Но главное отличие в том, что Map позволяет использовать любой тип ключа.

Подробнее об этой коллекции можно прочитать на MDN.

Здесь я дам вам краткий обзор:

Конструкторы:

  • new Map() — создает пустую коллекцию.
  • new Map(iterable) — создает коллекцию. Пример: новая Карта([[ 1, ‘один’],[ 2, ‘два’ ]]).

Базовые методы:

  • map.set(key, value) — записывает значение ключа.
  • map.get(key) — возвращает значение ключа или undefined, если ключ отсутствует.
  • map.has(key) — возвращает true, если ключ есть в коллекции, иначе false.
  • map.delete(key) — удаляет элемент (пару ключ/значение) по ключу.
  • map.clear() — очищает коллекцию от всех элементов.
  • map.size — возвращает текущее количество элементов.

Также стоит отметить два нюанса эксплуатации:

  1. Карта использует алгоритм SomeZeroValue для сравнения ключей. Это похоже на строгое сравнение (===), за исключением того, что NaN будет равно NaN. Так что можете смело использовать NaN в качестве ключа к своей коллекции (только зачем? 🙂 )
  2. Карта — это защищенная структура, у вас нет доступа к прототипу. Так что изменить его никак нельзя.

Итак, как мы можем использовать Map в наших проектах?

Я не знаю.😅

Как я сказал во вступлении: за всю свою работу я ни разу не видел Карту, или даже больше, я видел разработчиков, которые не знали о Карте. И перед написанием этой статьи я потратил много времени на поиск реальных примеров, где использование Map было оправдано. Но большинство статей просто пересказывают документацию.

Но почти всегда проще и проще использовать обычный Объект.

В итоге я вижу только две причины использовать Map:

  1. Вам нужно использовать другие объекты в качестве ключа.
  2. Вы должны защитить себя от изменений прототипа.

Но, в целом, можно спокойно использовать обычный объект.

Набор

На мой взгляд, Set — гораздо более полезная структура во внешнем интерфейсе.

Набор — это особый вид коллекции: «набор» значений (без ключей), в котором каждое значение может встречаться только один раз.

Подробнее о коллекциях можно прочитать на MDN.

Я также дам вам краткий обзор конструкторов и методов:

Конструкторы:

  • new Set() — создает пустую коллекцию.
  • new Set(iterable) — создает коллекцию. Пример: новый набор([1, 2, 3]).

Методы:

  • set.add(value) — добавляет значение (если оно уже существует, ничего не делает), возвращает тот же объект set.
  • set.delete(value) — удаляет значение, возвращает true, если значение было в наборе на момент вызова, иначе false.
  • set.has(value) — возвращает true, если значение присутствует в наборе, иначе false.
  • set.clear() — удаляет все существующие значения.
  • set.size — возвращает количество элементов в наборе.

Как и Map, он также имеет методы values(), keys(), entry().

Это сделано для совместимости с Map. Судя по всему, Map используется «под капотом» для работы Set.

Хотя я никогда не сталкивался с Картой, я видел Сета пару раз. Часто используется для фильтрации:

const array = [1, 2, 3, 3, 4, 1];

const unique = [...new Set(array)];

console.log(unique); // unique is [1, 2, 3, 4]

Но мы также можем использовать Set по его прямому назначению — для хранения уникальных значений. Но в JS есть проблема с объектами.

const obj1 = {
  title: "Test String"
};

const obj2 = {
  title: "Test String"
};

const set = new Set([obj1, obj2]);

console.log(set.size); // 2

set.forEach((item) => {
  console.log(item); // { title: "Test String" } x2
});

console.log(obj1 === obj2) // false

Проблема в том, что два одинаковых объекта имеют разные ссылки, поэтому сравнение «===» вернет false, поэтому элемент будет добавлен, и мы получим два одинаковых элемента в нашей коллекции уникальных элементов.

Из этого делаем вывод, что Set «из коробки» корректно работает только с примитивными типами.

Для корректной работы с объектами нужно написать свою реализацию.

Во-первых, нам нужно определить тип, по которому мы можем сравнивать

/**
 * The basic type for marking an object as able to be comparing
 */
export type Identifiable = {
  id: string;
};

вы можете использовать интерфейсы, если хотите. Id нужен здесь, чтобы у нас было поле, которое мы могли бы сравнить.

Следующий шаг: определить класс:

import { Identifiable } from "./types";

/**
 * a class for working with an array of unique values
 */
export class CustomSet<T extends Identifiable> { // use generic for versatility
  /**
   * base array for work
   */
  #value: T[]; // use new private class features: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields

  constructor(array: T[] = []) {
    const uniqueIds: string[] = [];

    this.#value = array.filter((element) => {
      const isDuplicate = uniqueIds.includes(element.id);

      if (!isDuplicate) {
        uniqueIds.push(element.id);

        return true;
      }

      return false;
    });
  }

  /**
   * getter for private value
   */
  get array() {
    return this.#value;
  }

  /**
   * get item by passed id.
   */
  getItemBy(id: string) {
    return this.#value.find((item) => item.id === id);
  }

  /**
   * remove item by id from array
   *  - return `true` if item was removed
   *  - return `false` if item not found
   *
   * @returns result of deleting
   */
  removeItemBy(id: string) {
    const targetIndex = this.#value.findIndex((item) => item.id === id);

    if (targetIndex === -1) {
      return false;
    }

    this.#value.splice(targetIndex, 1);
    return true;
  }

  /**
   * function for adding new item
   *  - return `true` if item was added
   *  - return `false` if item with the same id already exist
   *
   * @param item object with `id` field
   * @returns result of adding
   */
  add(item: T): boolean {
    if (this.#isUnique(item)) {
      this.#value.push(item);
      return true;
    }
    return false;
  }

  /**
   * the function checks if a similar object exists in the array
   * @param value object with `id` field
   * @returns result of checking
   */
  #isUnique(value: T): boolean {
    const result = this.#value.findIndex((item) => item.id === value.id);

    return result === -1;
  }
}

Это не лучший вариант. Там мы начинаем с поля id, которое должен иметь каждый объект в массиве, и сравниваем по нему. Но этот код может быть полезен, если вам нужна утилита для работы со многими объектами.

Вы также можете написать другую реализацию: использовать объект вместо массива в качестве хранилища значений. Я предоставляю это в репозитории, который я создал, когда писал эту статью. Там же можно посмотреть тесты.

Таким образом, Set определенно является более распространенной структурой данных во фронтенд-разработке.

Стандартный набор хорош для фильтрации или работы с большим количеством примитивных элементов. А для объектов можно написать свою обертку над массивом, в примере который я привел.

Краткое содержание

Во фронтенд-разработке мы редко видим Map и Set. В этой статье я попытался ответить на вопрос почему.

Карта легко заменяется объектом, более гибким и простым в использовании.

А Set хоть и хорош для примитивов, но не подходит для работы с объектами. Я попытался исправить эту проблему с помощью моей реализации.

Список источников

  1. Документация карты от MDN
  2. Алгоритм сравнения SameValueZero
  3. Комплект документации от MDN
  4. Мой репозиторий с примерами пользовательских реализаций Set