ES6: Методы объекта Map изнутри
Перевод статьи ES6 Maps in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.
Привет, это «ES6 — ну сколько можно уже — изнутри». Вы здесь впервые? Тогда начните с краткой истории инструментария ES6. Затем изучите деструктирование, литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры, улучшения в литералах объекта, новый «сахарок» — классы поверх прототипов, let
, const
и «Временную мёртвую зону», а также итераторы и генераторы и символы. Ну а сегодня поговорим про новую структуру данных для коллекций в ES6, новой фичи в ES6 — я говорю про объект Map
.
Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощьюинтерактивной оболочки REPL, либо командной строки babel-node и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6. Ещё одна довольно полезная альтернатива, это использовать онлайновый REPL для Babel — он показывает скомпилированный ES5-код справа от ES6-кода, чтобы быстро сравнить.
Пока мы не начали, позвольте беззастенчиво попросить вашей поддержки, если вы наслаждаетесь моей серией «ES6 изнутри». Ваши взносы пойдут на хостинг сайту, мне на еду, на то, чтоб я не отставал от графика, и на то, чтобы Pony Foo оставался поистине источником джаваскриптовых вкусняшек.
Спасибо, что выслушали, а теперь перейдём к коллекциям! Чтобы напомнить себе, о чем вообще речь, можете заглянуть в статью про итераторы — которые тесно связаны с коллекциями в ES6 — и в статью про оператор расширения и оставшиеся параметры.
А теперь приступим к Map
. Здравый смысл подсказал мне оставить остальные коллекции ES6 для завтрашней статьи, иначе одна статья была бы огромной.
До ES6 были ассоциативные массивы
Весьма распространённое злоупотребление объектами JavaScript — ассоциативные массивы, где мы привязываем строковые ключи к произвольным значениям. Например, можно взять объект и привязать имена пакетов npm
к их метаданным, вот так:
var registry = {} function add (name, meta) { registry[name] = meta } function get (name) { return registry[name] } add('contra', { описание: 'Управление асинхронным потоком' }) add('dragula', { описание: 'Перетягивание элементов' }) add('woofmark', { описание: 'Маркдаун и визивиг-редактор' })
В этом подходе есть несколько проблем, а именно:
- Проблемы с безопасностью, когда из-за пользовательских ключей типа
__proto__
,toString
или чего-нибудь вObject.prototype
объект становится непредсказуемым, превращая взаимодействие с такой разновидностью ассоциативных массивов в сущую каторгу. - Для перебора элементов списка с
Object.keys(registry).forEach
или реализацией протокола «Итерируемый» в регистре (registry
) требуется много кода. - Ключи ограничены строками, что затрудняет создание ассоциативных массивов с ключами в виде DOM-элементов или других нестроковых данных.
Первую проблему решили бы префикс и предосторожность в виде обращения к значениям ассоциативного массива исключительно через методы. А ещё лучше использовать прокси в ES6, но оставим эту тему до завтра!
var registry = {} function add (name, meta) { registry['map:' + name] = meta } function get (name) { return registry['map:' + name] } add('contra', { описание: 'Управление асинхронным потоком' }) add('dragula', { описание: 'Перетягивание элементов' }) add('woofmark', { описание: 'Маркдаун и визивиг-редактор'})
К счастью для нас, объекты Map в ES6 гораздо лучше справляются с проблемами безопасности именования ключей. И в то же время они автоматически обеспечивают функциональность коллекций, что также может пригодиться. Давайте посмотрим, как они устроены и какая от них практическая польза.
Объекты Map в ES6
Объект Map — структура данных для пар ключ/значение в ES6. Благодаря ему мы получаем более удачную структуру данных для ассоциативных массивов. Раньше что-то похожее на Map в ES6 выглядело так.
var map = new Map() map.set('contra', { описание: 'Управление асинхронным потоком' }) map.set('dragula', { описание: 'Перетягивание элементов' }) map.set('woofmark', { описание: 'Маркдаун и визивиг-редактор' })
Одно из важных отличий ещё и в том, что для ключей можно использовать всё что угодно. Вы не ограничены примитивными значениями типа символов, чисел или строк, а можете даже использовать функции, объекты и даты. К тому же ключи не будут приводиться к строкам, как с обычными объектами.
var map = new Map() map.set(new Date(), function today () {}) map.set(() => 'key', { pony: 'foo' }) map.set(Symbol('items'), [1, 2])
Также можете использовать объекты Map
с любыми объектами, которые следуют протоколу «Итерируемый» и создают такие коллекции, как [['key', 'value']
, ['key', 'value']]
.
var map = new Map([ [new Date(), function today () {}], [() => 'key', { pony: 'foo' }], [Symbol('items'), [1, 2]] ])
В этом примере фактически получается то же самое, что и в следующем. Заметьте, как мы используем деструктирование в параметрах items.forEach
, чтобы без особых усилий получить key
и value
из двумерного элемента.
var items = [ [new Date(), function today () {}], [() => 'key', { pony: 'foo' }], [Symbol('items'), [1, 2]] ] var map = new Map() items.forEach(([key, value]) => map.set(key, value))
Конечно, довольно глупо мучиться, добавляя элементы по одному, когда можно просто «скормить» нашему объекту Map
что-нибудь итерируемое. К слову об итерируемых объектах — Map
сам придерживается протокола «Итерируемый». Очень легко вытащить коллекцию пар ключ-значение, очень похожую на те, что можно «скормить» конструктору Map
.
Конечно, для этого эффекта можно воспользоваться оператором расширения.
var map = new Map() map.set('p', 'o') map.set('n', 'y') map.set('f', 'o') map.set('o', '!') console.log([...map]) // <- [['p', 'o'], ['n', 'y'], ['f', 'o'], ['o', '!']]
Кроме того, можно использовать цикл for..of
и сочетать это с деструктированием, чтобы по максимуму избаваться от лишних символов. Кстати, а про литералы шаблонов не забыли?
var map = new Map() map.set('p', 'o') map.set('n', 'y') map.set('f', 'o') map.set('o', '!') for (let [key, value] of map) { console.log(`${key}: ${value}`) // <- 'p: o' // <- 'n: y' // <- 'f: o' // <- 'o: !' }
Даже несмотря на то, что у объектов Map
есть программный API для добавления элементов, ключи уникальны, как и в случае с ассоциативными массивами. Многократная установка ключа только лишь перезапишет его значение.
var map = new Map() map.set('a', 'a') map.set('a', 'b') map.set('a', 'c') console.log([...map]) // <- [['a', 'c']]
В объектах Map
в ES6 NaN
становится «исключительным случаем», который обрабатывается как значение, равное самому себе, даже при том, что значением следующего выражения на самом деле будет true — NaN !== NaN
.
console.log(NaN === NaN) // <- false var map = new Map() map.set(NaN, 'foo') map.set(NaN, 'bar') console.log([...map]) // <- [[NaN, 'bar']]
Ассоциативные массивы и DOM
В ES5, всякий раз, когда у нас был DOM-элемент, который хотелось связать с API объекта для какой-нибудь библиотеки, нам приходилось приходилось писать много медленного кода, вроде того, что показано ниже. Следующая функция в коде просто возвращает API объекта с кучей методов для данного DOM-элемента, позволяя помещать и удалять DOM-элементы из кэша и также возвращать API объекта для DOM-элемента — если элемент уже существует.
var cache = [] function put (el, api) { cache.push({ el: el, api: api }) } function find (el) { for (i = 0; i < cache.length; i++) { if (cache[i].el === el) { return cache[i].api } } } function destroy (el) { for (i = 0; i < cache.length; i++) { if (cache[i].el === el) { cache.splice(i, 1) return } } } function thing (el) { var api = find(el) if (api) { return api } api = { method: method, method2: method2, method3: method3, destroy: destroy.bind(null, el) } put(el, api) return api }
Один из крутых аспектов Map
, как я уже упоминал, это возможность указывать DOM-элементы в качестве ключей. А тот факт, что у Map
есть возможности манипуляции коллекцией, здорово всё упрощает.
var cache = new Map() function put (el, api) { cache.set(el, api) } function find (el) { return cache.get(el) } function destroy (el) { cache.delete(el) } function thing (el) { var api = find(el) if (api) { return api } api = { method: method, method2: method2, method3: method3, destroy: destroy.bind(null, el) } put(el, api) return api }
То, что эти методы теперь стали однострочными, означает, что мы можем не выносить их отдельно, поскольку код и так легко читается. Мы просто взяли и сократили код в ~30 строк наполовину. Не говоря уже о том, что когда-нибудь в будущем это также будет работать гораздо быстрее варианта с поиском нужного элемента в массиве, как иголки в стоге сена.
var cache = new Map() function thing (el) { var api = cache.get(el) if (api) { return api } api = { method: method, method2: method2, method3: method3, destroy: () => cache.delete(el) } cache.set(el, api) return api }
Простота Map
поразительна. Если интересно моё мнение, то нам крайне не хватало такой фичи в JavaScript. Произвольные объекты в качестве ключей коллекции — это очень важно.
Для чего ещё нужен
Map
?
Методы коллекции в Map
Объекты Map
позволяют довольно легко исследовать коллекцию и выяснить, определён ли ключ в Map
. Как отмечалось ранее, NaN
равен NaN
, когда дело касается Map
. Однако, значения Symbol
всегда разные, так что вам придётся использовать их по значению.
var map = new Map([[NaN, 1], [Symbol(), 2], ['foo', 'bar']]) console.log(map.has(NaN)) // <- true console.log(map.has(Symbol())) // <- false console.log(map.has('foo')) // <- true console.log(map.has('bar')) // <- false
Пока вы сохраняете ссылку на Symbol
, всё в порядке. Держите ссылки поблизости, а Symbol
-ы ещё ближе?
var sym = Symbol() var map = new Map([[NaN, 1], [sym, 2], ['foo', 'bar']]) console.log(map.has(sym)) // <- true
И не забыли, что у ключей нет приведения типов? Осторожно! Мы так привыкли к приведению ключей объекта к строкам, что можно попасться, если не быть начеку!
var map = new Map([[1, 'a']]) console.log(map.has(1)) // <- true console.log(map.has('1')) // <- false
Также можно полностью очистить объект Map
от записей, сохранив на него ссылку. Порой это очень кстати.
var map = new Map([[1, 2], [3, 4], [5, 6]]) map.clear() console.log(map.has(1)) // <- false console.log([...map]) // <- []
При использовании объекта Map
в качестве итерируемого объекта, вы фактически перебираете его .entries()
. Поэтому необязательно явно перебирать .entries()
. Это будет сделано за вас в любом случае. Вы же помните Symbol.iterator
, так ведь?
console.log(map[Symbol.iterator] === map.entries) // <- true
Наряду с .entries()
у Map
есть и два других итератора. Это .keys()
и .values()
. Скорее всего вы уже догадались, какую последовательность значений они выдают, но вот код на всякий случай.
var map = new Map([[1, 2], [3, 4], [5, 6]]) console.log([...map.keys()]) // <- [1, 3, 5] console.log([...map.values()]) // <- [2, 4, 6]
Также у объектов Map
есть свойство .size
(только для чтения), поведение которого похоже на likeArray.prototype.length
— благодаря ему в любое время можно получить текущее количество записей в map.
var map = new Map([[1, 2], [3, 4], [5, 6]]) console.log(map.size) // <- 3 map.delete(3) console.log(map.size) // <- 2 map.clear() console.log(map.size) // <- 0
Стоит упомянуть ещё об одном аспекте Map
: их записи всегда перебираются в порядке добавления. В отличие от циклов по Object.keys
, которые обходит их в произвольном порядке.
Оператор
for..in
перебирает перечисляемые свойства объекта в произвольном порядке.
Также у объектов Map есть метод .forEach
, который ведёт себя так же, как и объекты Array
в ES5. Опять же, ключи здесь не приводятся к строкам.
var map = new Map([[NaN, 1], [Symbol(), 2], ['foo', 'bar']]) map.forEach((value, key) => console.log(key, value)) // <- NaN 1 // <- Symbol() 2 // <- 'foo' 'bar'
Просыпайтесь завтра пораньше, чтобы полакомиться на завтрак вкусными объектами WeakMap
, Set
и WeakSet
! :)
P.S. Это тоже может быть интересно: