[Перевод] Тонкости ES6: Коллекции (часть 1)
Ранее на этой неделе (статья от 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 и слабые коллекции.