От переводчика: это вторая часть перевода статьи про коллекции в EcmaScript 6. Первую часть можно прочитать тут. По разным причинам перевод второй части затянулся на достаточно долгий срок.

Map


Map (ассоциативный массив) — это коллекция пар ключ-значение (key-value). Вот что умеет Map:
  • new Map возвращает новый пустой ассоциативный массив.
  • new Map(pairs) создает новый ассоциативный массив и наполняет его данными из существующей коллекции пар [key, value]. Эта коллекция может быть другим Map объектом, массивом из двухэлементных массивов, генератором который возвращает двухэлементые массивы, и т.д.
  • map.size возвращает количество элементов в ассоциативном массиве.
  • map.has(key) проверяет, существует ли ключ key в ассоциативном массиве.
  • map.get(key) возвращает значение ассоциируемое с ключом key, либо undefined, если такого ключа нет. (аналогично obj[key])
  • map.set(key, value) добавляет запись в массив, ассоциирующую key с value. Перезаписывает существующую ассоциацию, если таковая имеется (аналогично obj[key])
  • map.delete(key) удаляет запись (аналогично delete obj[key])
  • map.clear() удаляет все записи в ассоциативном массиве.
  • map[Symbol.iterator]() возаращает итератор по записям в ассоциативном массиве. Итератор представляет каждую запись как массив[key, value].
  • map.forEach(f) работает так:
    for (let [key, value] of map)
      f(value, key, map);
    

    Странный порядок аргументов сделан по аналогии с Array.prototype.forEach()
  • map.keys() возвращает итератор по ключам в ассоциативном массиве.
  • map.values() возвращает итератор по значениям в ассоциативном массиве.
  • map.entries() возвращает итератор по записям в ассоциативном массиве, аналогично map[Symbol.iterator](). На самом деле, в обоих случаях вызывается один и тот же метод.


На что тут жаловаться? Вот некоторые фичи, которых нет в ES6 коллекциях, и которые, я думаю, были бы полезны:

  • Приспособление для значений по умолчанию, как collections.defaultdict в Python.
  • Хэлпер-функция, Map.fromObject(obj), чтобы облегчить написание ассоциативных массивов с использованием синтаксиса объект-литерал (object-literal syntax) (Прим. перев: пример синтаксиса объект-литерал в ES — { "test" : 1 })


Опять же, эти фичи легко добавить.

Ок. Помните, как я начал эту статью с размышлений о том, как забота об исполнении JS в браузере, влияет на дизайн языка и его фич? Сейчас мы начнем говорить об этом. У меня есть три примера. Вот вам первые два.

Отличия JS, часть 1: хэш-таблицы без хэш-кодов?


Есть одна полезная фича, которую классы коллекций в ES6, насколько мне известно, не поддерживают вообще.

Допустим, у нас есть Set из URL-объектов.
var urls = new Set;
urls.add(new URL(location.href));  // два URL-объекта
urls.add(new URL(location.href));  // одинаковы ли они?
alert(urls.size);  // 2

Эти два URL должны быть интерпретированы как идентичные. Все поля у них одинаковые. Но в Javascript это два разных объекта, и перегрузить понятие языка о равенстве объектов никак нельзя.

Другие языки это поддерживают. В Java, Python, Ruby отдельные классы могут перегружать равенство. Во многих имплементациях Scheme, индивидуальные хэш-таблицы могут быть созданы с использованием разных понятий равенства. С++ поддерживает оба варианта.

Однако все эти механизмы требуют от пользователей имплементации кастомных хэш-функций, и все они экспонируют системную хэш-функцию по умолчанию. Комитет решил не выставлять на показ хэш-коды в Javascript — во всяком случае пока — из-за открытых вопросов об интероперабельности и безопасности, что не является поводом для такого беспокойства в других языках.

Отличия JS, часть 2: Сюрприз! Предсказуемость!


Вы наверное думаете, что детерминированное поведение со стороны компьютера не повод для удивления. Но люди часто удивляются, когда я им говорю, что итерация по элементам в Map и Set происходит в порядке их добавления в коллекцию. Она детерминирована.

Мы привыкли к произвольности некоторых аспектов хэш-таблиц. Мы научились принимать это. Но есть чертовски хорошие причины попытаться избегать случайностей. Как я писал в 2012 году:
  • Есть свидетельства, что некоторые программисты находят произвольный порядок итерации удивительным и поначалу непонятным. [1][2][3][4][5][6]
  • Порядок энумерации свойств не специфицирован в ECMAScript, однако все крупные имплементации вынуждены сойтись на конкретном порядке вставки, для совместимости. Поэтому есть мнение, что если TC39 не станет специфицировать детерменированный порядок итерации, «веб сделает это за нас». [7]
  • Порядок итерации хэш-таблицы может раскрыть части хэш-кодов объектов. Это возлагает огромную ответственность за безопасность на имплементатора хэш-функции. Например, адрес объекта не должен быть восстанавливаемым из раскрытых частей его хэш-кода (раскрытие адреса объекта недоверенному ECMAScript-коду хоть и не может эксплуатироваться злоумышленниками само по себе, но было бы большой дырой в безопасности).

Когда это обсуждалось в феврале 2012, я был за произвольный порядок итерации. Я провел эксперимент, чтобы доказать, что слежение за порядком вставки сделает хэш-таблицы слишком медленными. Я написал парочку бенчмарков на С++. Результат меня поразил.

Вот так мы оказались с хэш-таблицами, которые следят за порядком вставки в JS!

Весомые причины использовать слабые коллекции


В начале июня 2015 мы обсуждали пример, включающий в себя библиотеку анимации на JS. Мы хотели хранить булевый флаг для каждого DOM-объекта, вроде такого:
if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;

К сожалению, задание expando-свойства в DOM-объекте таким образом — плохая идея, что и обсуждалась в статье. В той статье мы решили проблему, используя символы. Но не можем ли мы сделать то же самое используя Set? Это может выглядеть так:
if (movingSet.has(element)) {
  smoothAnimations(element);
}
movingSet.add(element);

Есть только один недостаток: Map и Set имеют сильную ссылку (strong reference) на каждый ключ и каждое значение. Это значит, если DOM-элемент был удален из документа, сборщик мусора не может освободить память пока элемент не удален из movingSet. Библиотеки обычно с грехом пополам справляются с очисткой памяти после себя. Это может привести к утечкам памяти.

ES6 предлагает удивительное решение для этого. Сделайте movingSet WeakSet вместо Set. Проблема утечки решена!

Итак, можно решить эту проблему с помощью слабых коллекций или символов. Что лучше? Дискуссия о преимуществах и недостатках каждого способа сделает этот пост слишком длинным. Если вы можете использовать один символ на протяжении всей жизни веб-страницы, то это, наверное, нормально. Если вы хотите использовать много короткоживущих символов, это тревожный знак: рассмотрите возможность использовать WeakSet вместо этого, чтобы предотвратить утечки памяти.

WeakMap и WeakSet


WeakMap и WeakSet специфицированы вести себя так же как Map и Set, но со следующими ограничениями:
  • WeakMap поддерживает только new, .has(), .get(), .set(), .delete().
  • WeakSet поддерживает только new, .has(), .add(), .delete().
  • Значения в WeakSet и ключи в WeakMap должны быть объектами.

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

Эти ограничения позволяют сборщику мусора собирать мертвые объекты из живых слабых коллекций. Похожего эффекта можно добиться с помощью слабых ссылок или словарей со слабыми ключами (weak-keyed dictionaries), но слабые коллекции в ES6 имеют все преимущества управления памятью без раскрытия факта, что в скриптах произошла сборка мусора.

Отличия JS, часть 3: скрывая недетерменированность сборщика мусора


Под капотом, слабые коллекции имплементированы как таблицы эфемеронов (ephemeron tables).

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

Зачем все эти ограничение? Почему просто не добавить слабые ссылки в JS?

Опять же, комитет не хочет раскрывать недетерминированное поведение скриптам. Слабая кроссбраузерная совместимость — проклятие веб-разработки. Слабые ссылки раскрывают детали имплементации сборщика мусора — само определение платформо-специфичного произвольного поведения. Конечно, приложения не должны зависеть от деталей конкретной платформы, но слабые ссылки делают сложным для понимания, насколько вы зависите от поведения сборщика мусора в текущем браузере, в котором вы запускаете свое приложение. C ними сложно иметь дело.

Напротив, слабые коллекции в ES6 имеют более ограниченное, но крепкое множество фич. Факт сборки ключа или значения сборщиком мусора ненаблюдаем, так что приложения никак не могут зависеть от него, даже по случайности.

Это конкретный случай того, как специфичные для веба проблемы привели к удивительному решению в дизайне, которое улучшило JS как язык.

Где я могу использовать коллекции?


Все четыре класса коллекций поддерживаются в Firefox, Chrome, MS Edge и Safari. Для поддержки старых браузеров, используйте полифил (polyfill) типа es6-collections.

WeakMap был впервые имплементирован в Firefox Андреасом Галом, бывшим CTO Mozilla. Том Шустер имплементровал WeakSet. Я имплементировал Map и Set. Спасибо Тоору Фудзисаве (Tooru Fujisawa) за патчи.

На следующей неделе «Тонкости ES6» берут двухнедельный перерыв. Эта серия статей рассказала о многом, но некоторые самые мощные фичи ES6 еще будут описаны. Присоеденяйтесь к нам, когда мы вернемся с новым контентом 9 июля.

Комментарии (26)


  1. Nikelandjelo
    05.08.2015 01:34
    +1

    Хм, т.е. Map и Set для кастомных классов будут бесполезны во многих случаях? Например у меня есть класс Size с 2 полями width и height, сегодня я могу переопределить toString() и использовать обычные объекты в качестве map или set. И похоже что новые Map и Set тут не помогут. Жалко.


  1. vintage
    05.08.2015 08:58

    По WeakSet и WeakMap нельзя итерироваться, что очень печалит :-(


    1. Ununtrium
      05.08.2015 09:20
      +1

      Я думаю, что причина в том, что итерироваться по изменяющейся коллекции весьма опасно.


      1. vintage
        05.08.2015 09:59
        -3

        WeakSet от Set в данном плане ни чем не отличаются.


        1. Ununtrium
          05.08.2015 12:48

          Может покажете пример когда это нужно?


          1. vintage
            05.08.2015 13:43
            +2

            Например, при реализации атомов чтобы хранить ссылки на зависимые атомы. По хорошему эти ссылки не должны быть «жёсткими». Но в случае изменения значения нужно иметь возможность пробежаться по всем зависимым атомам и уведомить их об изменении.


  1. lolmaus
    05.08.2015 15:49

    А где map.map()?

    Почему JS так не везет со стандартной библиотекой? :(


    1. Ununtrium
      05.08.2015 16:48

      Не очень понял что должен делать map.map(). Конечно, Map можно юзать в качестве значения


      1. lolmaus
        05.08.2015 16:52

        Очевидно, то же самое, что и array.map().


        1. Ununtrium
          05.08.2015 17:07

          Ок, не понял сразу. Ну напишите сами и предложите в ES7. Вообще, думаю есть уже какой-нибудь JS LINQ который все это реализует (в том числе и для новых коллекций).


          1. lolmaus
            05.08.2015 17:16
            +1

            map.reduce() тоже нет. Они вообще пробовали пользоваться своим API? :/ :(


            1. Ununtrium
              06.08.2015 10:41

              По-моему написать свой reduce — выполнимая задача. Даже очень. В любом случае возмущаться здесь — контрпродуктивно. Есть гитхаб с предложениями по ES7, который не за горами.


              1. lolmaus
                06.08.2015 11:14

                Да и написать свой класс Map — тоже вполне посильная задача, и, уверен, в инете можно найти множество полнофункциональных реализаций.

                Вопрос в том, кому тогда нужна эта убогая официальная реализация.


                1. Ununtrium
                  06.08.2015 12:28
                  -1

                  Что за привычка пенять на других? Ай-яй-яй какие плохие, не добавили ваш любимый сахар! Вот сюда можно слать свои предложения. Процесс открыт. Если там вас завернут — можно возмущаться.

                  Или вы просто мимо проходили и решили говном покидать в JS?

                  P.S. Согласен, что map/reduce делает код красивее. Но лично для меня это не жизненно необходимая фича.


                  1. lolmaus
                    06.08.2015 12:31
                    +1

                    Это же стандарт! Не хобби-проект. Тем, что они там выпускают, пользоваться потом всем и всегда! Это будет высечено в браузерах и серверах на годы, на десятилетия может даже!

                    Как так получается, что качество стандартной библиотеки JS, самой распространенной, млять, платформы в мире, зависит от мимо проходящего кидателя какашек?


                  1. senia
                    06.08.2015 12:40
                    +2

                    Быстро, решительно, молча и без пояснений.
                    Так что какашками кидаться уже можно.


                    1. rock
                      06.08.2015 22:07

                      1. senia
                        06.08.2015 23:03

                        Сможете найти туда ссылку из самого репозитория, из коммита или из откаченного пул-реквеста? По вашей ссылке совершенно неофициальные логи по какой-то сходке. Так что да, без пояснений. Была бы ссылка из коммита, было бы «с пояснением».

                        Ну а по ссылке: хорошо перетерли. Подозреваю, что не будет у нас ни map.map, ни map.entries().map. Лично я трагедии не вижу: как пользовались все либами, так и будут пользоваться. Map и Set добавили, а остальное можно в либах докрутить.


                        1. rock
                          06.08.2015 23:23

                          А должен? Извините, а вы вообще в курсе процесса стандартизации ECMAScript?


                          1. senia
                            06.08.2015 23:36

                            Нет, не должен.

                            Я отвечал вот на это:

                            Процесс открыт. Если там вас завернут — можно возмущаться.

                            В открытой части процесса предложение уже было и его уже завернули без пояснений в открытой части процесса.


                            1. rock
                              07.08.2015 00:02

                              Добавление предложений в данный репозиторий не является «открытой» частью — могут быть добавлены только те предложения, у которых есть чемпион — член tc39. Открытой частью обсуждения является эта рассылка, а никак не список предложений. Где найти логи собраний tc39, на которых решается судьба данных предложений — всем известно, так что, как минимум, глупо удивляться отсутствую комментариев в коммите.


                              1. senia
                                07.08.2015 06:37

                                Спасибо за информацию, был введен в заблуждение данным комментарием.


                              1. Ununtrium
                                07.08.2015 07:35
                                +1

                                Интересно, поскольку сам Jason Orendoff указывал, что предложения принимаются в данном репозитории, т.е. в issues. Но, возможно я его не так понял.


                                1. rock
                                  07.08.2015 08:45

                                  Предложения может и принимаются в виде ишью (пока, вроде, ничего хорошего из этого не вышло, да и на упомянутое предложение такое ишью отсутствует), но добавленных в список, о котором, собственно, была речь, без «чемпиона», представляющего предложение комитету, не видел.


                                  1. Ununtrium
                                    07.08.2015 14:27

                                    Речь вообще шла о том, что у комитета есть (более-менее) нормальная обратная связь с сообществом, и что если чем-то недоволен, то можно идти в мейлинг лист или на гитхаб и кидать им какашки прямо в лицо.

                                    Впрочем, как я теперь вижу, процесс аппрува фич не вполне прозрачный.


                              1. Ununtrium
                                07.08.2015 08:38
                                +2

                                Где найти логи собраний tc39, на которых решается судьба данных предложений — всем известно, так что, как минимум, глупо удивляться отсутствую комментариев в коммите.


                                Вообще, это хреновая мотивация. «Где issue-трекер и так все знают, так что в комментариях ничего не пишем».