Введение
Я работаю реактивным разработчиком около трех лет и за все это время не видел, чтобы кто-то использовал 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 — возвращает текущее количество элементов.
Также стоит отметить два нюанса эксплуатации:
- Карта использует алгоритм SomeZeroValue для сравнения ключей. Это похоже на строгое сравнение (===), за исключением того, что NaN будет равно NaN. Так что можете смело использовать NaN в качестве ключа к своей коллекции (только зачем? 🙂 )
- Карта — это защищенная структура, у вас нет доступа к прототипу. Так что изменить его никак нельзя.
Итак, как мы можем использовать Map в наших проектах?
Я не знаю.😅
Как я сказал во вступлении: за всю свою работу я ни разу не видел Карту, или даже больше, я видел разработчиков, которые не знали о Карте. И перед написанием этой статьи я потратил много времени на поиск реальных примеров, где использование Map было оправдано. Но большинство статей просто пересказывают документацию.
Но почти всегда проще и проще использовать обычный Объект.
В итоге я вижу только две причины использовать Map:
- Вам нужно использовать другие объекты в качестве ключа.
- Вы должны защитить себя от изменений прототипа.
Но, в целом, можно спокойно использовать обычный объект.
Набор
На мой взгляд, 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 хоть и хорош для примитивов, но не подходит для работы с объектами. Я попытался исправить эту проблему с помощью моей реализации.