Ранее на этой неделе (статья от 19 июня — прим.) спецификация ES6, официально названная ECMA-262, 6th Edition, ECMAScript 2015 Language Specification, преодолела последний барьер и была утверждена как стандарт Ecma. Мои поздравления TC39 и всем остальным, кто помогал. ES6 закончен!

Даже лучше: больше не надо будет ждать следующего обновления 6 лет. Комитет собирается выпускать новую версию в срок около года. Предложения по ES7 уже примаются!

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



Сложности коэволюции



JS не очень похож на другие языки програмирования, и это временами влияет на эволюцию языка самыми неожиданными способами. Хороший пример — модули в ES6. Модули есть в других языках — Racket (отличная реализация), Python. Когда комитет решил добавить модули в ES6, почему не скопировать существующую имплементацию?

JS отличается, так как он исполняется в браузерах. I/O операции могут занять приличное время. Поэтому система модулей ES6 должна поддерживать асинхронную загрузку. Она не может периодически искать модули по разным директориям. Поэтому копирование текущих имплементаций не лучший вариант.

Как это повлияло на конечный дизайн — в другой раз. Мы не станем сейчас говорить о модулях. Мы поговорим о том, что ES6 называет “keyed collections”: Set, Map, WeakSet, WeakMap. Эти структуры похожи на хэш-таблицы (hash tables) в других языках. Но в процессе дискуссий комитет пошел на некоторые компромиссы, из-за особенностей JS.

Зачем коллекции?


Каждый, кто знаком с JS, знает что в нем уже есть что-то наподобии хэш-таблиц — объекты. Обычный Object, в конце концов немногим более чем коллекция key-value пар. Вы можете добавлять, итерировать, считывать и удалять свойства (properties) — все как в хэш-таблицах. Зачем тогда эти новые фичи в языке?

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

  • Объекты, используемые таким образом, не могут содержать методы не рискуя коллизией.
  • Из-за первого пункта, программы должны использовать Object.create(null) вместо простого {}, или внимательно следить чтобы встроенные методы (типа Object.prototype.toString) не интерпретировались как данные.
  • Ключами могут быть только строки (strings) или, в случае ES6, символы. Объекты ключами быть не могут.
  • Нет эффективного способа получить количество свойств (properties) у объекта.

ES6 добавляет хлопот такому подходу — обычные объекты теперь не итерируемы, то есть не будут работать с циклом for-of, оператором ... и тд.

Опять-таки, во многих программах это не важно и можно продолжать использовать обычные объекты. Map и Set — для остальных случаев. Для защиты от коллизий между данными и встроенными свойствами (properties), коллекции в ES6 не выставляют данные как свойства. Это значит что нельзя добраться до данных с помощью выражений типа obj.key или obj[key]. Придется писать map.get(key). Также, записи в хэш-таблице (в отличие от свойств) не наследуются с помощью прототипной цепочки (prototype chain).
Преимущество в том, что Map и Set, в отличие от обычных объектов, могут иметь методы, как стандартные, так и кастомные без конфликтов.

Set


Set — это множество значений. Оно изменяемо, поэтому элементы можно добавлять и удалять. Выглядит как простой массив, не так ли? Но есть различия.

Во-первых, Set, в отличие от массива, никогда не содержит один элемент дважды. Если попытаться добавить существующее значение, ничего не произойдет.
> var desserts = new Set("abcd");
> desserts.size
    4
> desserts.add("a");
    Set [ "a", "b", "c", "d" ]
> desserts.size
    4

Примечание: в оригинале использовались emoji, которые проблемно скопировать. Смысл в любом случае тот же.

В примере выше используются строки, но Set может содержать любые объекты. И, как и со строками, при попытке добавления дубликата, ничего не добавится.

Во-вторых, Set хранит данные таким образом, что проверка наличия элемента во множестве выполняется очень быстро.
> // Проверяем, является ли "zythum" словом.
> arrayOfWords.indexOf("zythum") !== -1  // медленно
    true
> setOfWords.has("zythum")               // быстро
    true

Индексирование в Set не доступно.
> arrayOfWords[15000]
    "anapanapa"
> setOfWords[15000]   // set индексы не поддерживает
    undefined

Вот все доступные операции:
  • new Set создает новое пустое множество.
  • new Set(iterable) создает множество и заполняет данными из любого итерируемого источника.
  • set.size возвращает кол-во элементов во множестве.
  • set.has(value) возвращает true если множество содержит данное значение.
  • set.add(value) добавляет элемент во множество. Как помните, если пытаться добавить существующий, ничего не произойдет.
  • set.delete(value) удаляет элемент из множества. Как и add(), возвращает ссылку на множество, что позволяет вызывать методы друг за другом (а-ля Fluid Interface — прим.)
  • set[Symbol.iterator]() возвращает новый итератор по значениям во множестве. Обычно не вызывается напрямую, но именно это делает множества итерируемыми. Значит, можно писать for (v of set) {...} и тд.
  • set.forEach(f) легче всего объяснить в коде. Это краткая запись следующего выражения:
    for (let value of set)
        f(value, value, set);
    

    Это аналог .forEach() в массивах.
  • set.clear() удаляет все элементы.
  • set.keys(), set.values(), set.entries() возвращают различные итераторы. Они предназначены для совместимости с Map, поэтому о них позже.

Из этих фич самой мощной является new Set(iterable) потому что она работает на уровне структур данных. Можете ее использовать для конвертации массива в Set, удаления дубликатов в одну строчку кода. Или передайте туда генератор, он выполнится и соберет элементы во множество. Это также метод, как копировать существующий Set.

Я вам обещал на прошлой неделе пожаловаться по поводу новых коллекций в ES6. Пожалуй, начну. Как бы Set ни был хорош, есть методы которые неплохо было бы включить в следующие версии стандарта:

  • Функциональные хэлперы, которые есть в массивах типа ap(), .filter(), .some() и .every().
  • Немутирующие операции set1.union(set2) и set1.intersection(set2).
  • Методы, которые оперируют многими значениями сразу, типа set.addAll(iterable), set.removeAll(iterable) и set.hasAll(iterable)

Хорошая новость в том, что все это можно имплементировать самим, используя методы, предаставленные ES6.

Из-за объема материала решил разделить на 2 части. Во второй части про Map и слабые коллекции.

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


  1. RomanYakimchuk
    26.06.2015 08:55
    +1

    В примере используется `Set` для хранения строк. Если рассматривать `Set` именно с этой стороны, то это ничто иное как синтаксический сахар, не более (все описанное можно без проблем реализовать сейчас).

    В чем реальный профит Set'а? В каких кейсах можно будет сказать «Здесь железно эффективнее Set, чем обычный массив.»?

    Ключами могут быть только строки (strings) или, в случае ES6, символы. Объекты ключами быть не могут.

    Может быть есть пример как объект может выступать в роли ключа?


    1. Allfar
      26.06.2015 09:06
      +4

      Можно реализовать все, что угодно. Но такие фундаментальный вещи, как set, все же, должны быть в stdlib любого серьезного ЯП.


      1. RomanYakimchuk
        26.06.2015 09:19
        -2

        Мысль верная, но это спорный вопрос, где обе стороны правы. Вопрос именно в том, для решения какой нерешенной задачи нужен этот инструмент. Если ни для какой, и речь идет лишь об оптимизации производительности при работе с коллекциями, тогда вопрос снят.


        1. IncorrecTSW
          26.06.2015 09:35
          +3

          Это с чего бы по вашей прихоти снимается удобный и производительный инструмент? Как следствие для уймы задач он подходит.
          Где для той же задачи (условно теперь получается костыльно) используется массив то вот вам и места применения в каждом втором коде.


          1. RomanYakimchuk
            26.06.2015 09:58
            -2

            Это с чего бы по вашей прихоти снимается удобный и производительный инструмент?

            Я нигде этого не написал, не преувеличивайте.

            Где для той же задачи используется массив то вот вам и места применения в каждом втором коде.

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

            условно теперь получается костыльно

            Здесь тонкий момент.

            У нас уже есть конструктор Array, который содержит методы для работы с массивами. Теперь появляется новая сущность, которая содержит часть методов от массива, и часть новых методов которых ему не хватало ранее. Получается, теперь у нас есть два неполноценных инструмента. Каждый из них имеет часть функциональности друг от друга, но полноценным не является ни один из них.

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

            Вы правы в том что это не самое красивое решение, и лучше это сделать нативно, но теперь это будет не решение без которого не обойтись, а фактически это будет велосипедом, и еще останется вопрос что будет работать быстрее, учитывая что горячие участки кода браузер умеет оптимизировать сам.

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


            1. IncorrecTSW
              26.06.2015 10:08

              Как нету тестов? jsperf или например benchmark вам в помощь. Чего нет напишите сами.

              К слову почему только браузер? Есть есть еще серверная сторона.
              Если появился новый инструмент под требуемую задачу и он работает быстрей чем старый костыль то он именно без которого нельзя обойтись а не велосипед.


        1. Ununtrium Автор
          26.06.2015 10:32
          +4

          Вам просто указывают, что структура Set есть почти во всех языках, в стандартной библиотеке. Что такое множество (set) и где его использовать — основы программирования (изучают в школе или на первом курсе) и лежит вне плоскости этой статьи.

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

          Оптимизация производительности как-бы одна из важнейших задач.

          Вообщем, есть мнение что вы банально зашли потроллить. Толсто.


          1. RomanYakimchuk
            26.06.2015 10:38
            -1

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


            1. AlexeyFrolov
              27.06.2015 18:57
              +1

              пожалуйте в школу en.wikipedia.org/wiki/Set_(mathematics). Мне в повседневной практике очень часто требуется определять множество значений.


          1. kibitzer
            26.06.2015 10:53
            +4

            Что вы на человека набросились. Он лишь сказал, что примеры в статье не показывают особых преимуществ set перед существующим подходом в JS.

            И уж совсем моветоном ссылаться на основы программирования. То, что описывается, как множество, реализовывалось в JS и ранее, просто другими средствами.

            Основной плюс — стало удобнее работать с коллекциями и они стали быстрее. И я думаю RomanYakimchuk просто хотел увидеть именно в чём стало удобнее и на сколько быстрее. Статья о другом, вот и всё.


    1. Antelle
      26.06.2015 09:39

      С обычными объектами (т.е. использовать {} со свойствами в качестве Set) так нельзя:

      > var s = new Set(), obj = { my: 'object' }
      > s.add(obj);
      Set {Object {my: "object"}}
      > s.add(obj);
      Set {Object {my: "object"}}  <--- один и тот же объект второй раз не добавился
      


      1. RomanYakimchuk
        26.06.2015 09:59

        Это коллекция из уникальных объектов. Это решенная задача.


        1. Antelle
          26.06.2015 10:02
          +1

          Как она решается сейчас без библиотек и добавления свойств в объекты коллекции?


          1. RomanYakimchuk
            26.06.2015 10:07
            -1

            Достаточно сделать метод который проверяет наличие элемента в массиве перед вставкой элемента.


            1. Antelle
              26.06.2015 10:07
              +1

              И получить производительность вставки O(n)


              1. RomanYakimchuk
                26.06.2015 10:13

                Правильно, поэтому я и написал, что производительность это плюс.


                1. Antelle
                  26.06.2015 10:24

                  Как бы это сказать… Это совершенно другая задача, для чего и нужны новые коллекции, которых раньше в стандартной библиотеке не было.


        1. IncorrecTSW
          26.06.2015 10:03

          Я вас удивлю но решенных задач море. Но это никак не аргумент в данном случае.


          1. RomanYakimchuk
            26.06.2015 10:04

            Аргумент в пользу чего? Я не говорил что этот инструмент не нужен.


            1. IncorrecTSW
              26.06.2015 10:11

              синтаксический сахар, не более

              Получается, теперь у нас есть два неполноценных инструмента.

              а фактически это будет велосипедом

              Я потерялся в ходе ваших мыслей. Сдается речь о аргументе в пользу полезности/бесполезности.


              1. RomanYakimchuk
                26.06.2015 10:16
                -1

                Верно, мы ищем пользу от инструмента, но не решаем вопрос «нужен ли такой инструмент языку» :)


                1. fogone
                  26.06.2015 16:02
                  +4

                  Устраивается новичок на работу на лесопилку:
                  — это хорошо, что у вас есть пилорама, но это инструмент ненужный — есть же болгарка, ей можно всё сделать.
                  — но бревно на доски ей пилить долго и неудобно, не предназначена она для этого, да и в руках её надо держать..?
                  — тогда можно её в тиски зажать, а пилорама — бесполезная.


    1. Ununtrium Автор
      26.06.2015 10:14

      Может быть есть пример как объект может выступать в роли ключа?

      Если вы не заметили:
      Во второй части про Map и слабые коллекции.

      В структуре Set ключей нет.

      Почему не надо писать свои велосипеды, вам уже сказали.


      1. RomanYakimchuk
        26.06.2015 10:35
        -4

        Set и Map, вероятно, удобнее и чище в использовании чем Array. Вероятно, он работает быстрее, и сделает Array устаревшим.
        Здесь речь о другом — все это не привносит ничего нового. Все это давно реализовано и используется. Set и Map лишь упростит использование и даст возможность работать с коллекциями быстрее, не более.


        1. Ryotsuke
          26.06.2015 11:24
          +1

          «не более»?

          Да каждого из этих двух улучшений достаточно чтобы добавить это в язык.

          «упростит использование»
          Set исправляет семантику языка. Если я реализую множество, я хочу везде видеть и знать, что это именно множество, а не что-то, что его может реализует, а может и нет. Что я банально буду в дебаггере видеть что это множество а не что-то другое.

          Про скорость вообще мне кажется объяснять не надо. Скорость современного js критично важна, на нем сейчас слишком много всего пишется.


          1. RomanYakimchuk
            26.06.2015 11:26
            -1

            Да каждого из этих двух улучшений достаточно чтобы добавить это в язык.

            Я этого не говорил.

            Про скорость вообще мне кажется объяснять не надо. Скорость современного js критично важна, на нем сейчас слишком много всего пишется.

            Протестировал на хроме: у меня добавление элементов в Set работает медленнее чем в массив.


            1. Ryotsuke
              26.06.2015 11:30

              Добавление или добавление с проверкой есть ли уже такой элемент? Размеры массивов? Удаление элементов из середины?
              Все ли учитывалось в тесте?
              Ну и надо помнить что в Хроме хорошая эвристика по оптимизациям и искусственный тест может давать неверную картину.


            1. Ryotsuke
              26.06.2015 11:38

              jsperf лежит :( Нашел jsperf.com/es6-shim-vs-es6-collections но посмотреть не могу


              1. RomanYakimchuk
                26.06.2015 11:58
                +2

                Я ошибся в тесте.

                Set работает на добавление одном уровне с массивом, но если сделать полный тест (проверка ключей, поиск индекса, все как обычно), тогда Set выигрывает в десятки раз. 100.000 итераций делал.

                var foo = [], index, bar;
                
                console.time('test');
                
                for (index = 100000; index--;) { bar = Math.random(); if (foo.indexOf(bar) === -1) {foo.push(bar);} }
                
                console.timeEnd('test');
                


                var foo = new Set(), index, bar;
                
                console.time('test');
                
                for (index = 100000; index--;) { bar = Math.random(); if (!foo.has(bar)) {foo.add(bar);} }
                
                console.timeEnd('test');
                


                1. hoack
                  26.06.2015 22:35

                  Во втором тесте, мне кажется, не нужно делать проверку на наличие элемента в сете. В этом и сила структуры Set, что она сама проигнорирует попытку вставить дублирующийся элемент.


              1. Ununtrium Автор
                26.06.2015 15:36

                У них сервак лег из-за спамеров. Надеюсь, поднимут, им там разные хостеры предлагают помочь.


              1. rock
                27.06.2015 12:13

                Собственно, что вы хотите увидеть в этом тесте? es6-shim и es6-collections? Коллекции из es6-collections реализованы поверх массивов и имеют время поиска элемента O(n). Коллекции es6-shim — цепочка вхождений и, для примитивов, индекс, время поиска элемента — O(n) для объектов и O(1) для примитивов, притом до недавнего времени es6-shim заменял быстрые нативные коллекции на свои медленные во всех движках, да и сейчас во многих. Так что и те, и те коллекции назвать настоящими язык не поворачивается, так как требование к сублинейному времени доступа содержится в стандарте ECMAScript. Используйте коллекции из core-js — O(1) для всех ключей, кроме иммутабельных объектов :)