
Введение
Я работаю реактивным разработчиком около трех лет и за все это время не видел, чтобы кто-то использовал 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 хоть и хорош для примитивов, но не подходит для работы с объектами. Я попытался исправить эту проблему с помощью моей реализации.