ECMAScript 6 — будущее JavaScript
За те 13 лет, что я занимаюсь фронтендом, JavaScript успел сильно измениться в применении. Сначала мы делали меню, калькуляторы и снежинки к Новому году. Теперь на нём пишут серверные приложения, мобильные программы, игровые движки и даже системы управления домом. Сам язык долго не поспевал за всем, что мы пытались на нём построить.
В 2013 году ECMAScript 6 ещё впереди, но отдельные возможности уже можно пробовать. Мне интереснее всего те изменения, которые убирают знакомую ручную работу: собственные коллекции, преобразование аргументов, передачу значений между функциями. Многое из этого мы годами собирали с помощью библиотек.
Привычные операции становятся частью языка
Для поиска подстроки мы обычно вызываем indexOf, хотя часто нам не нужна её позиция. Нам нужен ответ на вопрос, есть ли она вообще. Приходится сравнивать результат с минус единицей. Новые методы строк позволяют выразить такие проверки непосредственно.
У объектов появляются Object.is для сравнения значений и Object.assign для переноса свойств из одного объекта в другой. Последнюю операцию реализует чуть ли не каждый фреймворк. Наконец она становится частью языка.
В массивах тоже можно будет искать элемент по условию или получать его индекс, чтобы затем продолжить работу с нужного места. Есть дополнения к Number и математическим функциям. А вот метода unique, который оставляет в массиве только уникальные значения, по-прежнему нет. К нему я ещё вернусь: другие нововведения позволяют собрать такую операцию самим.
Переменная внутри блока
С var область видимости переменной ограничена функцией. Объявление внутри if не создаёт отдельную локальную переменную, и на этом можно ошибиться: кажется, что меняешь значение в одном небольшом участке, а оно меняется и за его пределами.
Новое ключевое слово let ограничивает область видимости блоком. Если внутри if объявить переменную с тем же именем, снаружи останется прежнее значение. Речь именно о блоках кода: фигурные скобки встречаются и в записи объектов, поэтому считать любые скобки новой областью видимости было бы ошибкой.
У const такая же блочная область видимости, но объявленному имени нельзя присвоить другое значение. Это ограничение на присваивание, а не обещание неизменности всего содержимого объекта.
Вложенные объявления могут перекрывать внешние. Например, снаружи объявлена константа, а внутри блока — let с тем же именем. Внутри мы работаем с локальной переменной; внешняя константа от этого не меняется. У кода появляются границы, которые раньше приходилось создавать дополнительными функциями.
Коллекции и перебор значений
Больше всего мне хочется поскорее использовать коллекции. Map хранит пары ключей и значений, причём ключом может быть не только строка. Подойдут объект, функция, null или undefined. Если функция используется как ключ, обращение к коллекции не вызывает её: get возвращает значение, связанное именно с этой функцией.
У коллекции есть отдельные операции для получения значения, проверки наличия ключа и удаления записи: get, has и delete. Не приходится каждый раз договариваться, как имитировать такое хранилище обычным объектом.
Set решает другую задачу: хранит уникальные значения. В него можно передать последовательность с повторами, а потом добавлять новые элементы через add. Повторная попытка добавить то же значение не увеличит набор. Проверка и удаление работают через has и delete.
Остаётся перебрать содержимое. for…in при работе с массивом даёт имена свойств, в том числе индексы, хотя обычно нам нужны сами элементы. Для значений появляется for…of. Он работает с массивами и коллекциями; при переборе Set мы получаем его элементы, а при переборе Map — пары из ключа и значения.
Сначала такая пара выглядит немного неудобно: из маленького массива нужно отдельно достать обе части. Но здесь коллекции встречаются с ещё одной возможностью языка — деструктуризацией.
Данные, связанные с объектом
У WeakMap ключами служат объекты. Поначалу я даже хотел убрать его из презентации: не понимал, где мне это понадобится. Потом вспомнил про DOM. В галерее удобно связывать элемент страницы с дополнительными данными или другими элементами.
Отличие от обычного Map связано со временем жизни ключа. Слабая ссылка в WeakMap не удерживает объект от сборки мусора. Когда на объект больше нет сильных ссылок, сборщик мусора может освободить его вместе со связанной записью. Одного удаления элемента из DOM для этого недостаточно: программа может продолжать хранить ссылку на него.
Для такого сценария коллекция полезна именно тем, что не заставляет держать объект в памяти только ради сопутствующих данных.
Разобрать массив и собрать его обратно
Деструктуризация позволяет присваивать элементы массива нескольким переменным за одну операцию. Можно пропускать ненужные позиции, разбирать вложенные массивы или менять два значения местами без третьей переменной:
[first, second] = [second, first];
Та же идея работает с объектами. Если функция возвращает объект, можно сразу получить нужное свойство, не сохраняя весь результат ради последующего обращения к нему. В CoffeeScript мне как раз нравились подобные конструкции: в короткой записи остаётся видно, какие данные нам нужны.
Есть и сокращённая запись свойств. Когда имена переменных совпадают с именами полей, вместо { name: name, email: email } достаточно написать { name, email }.
Теперь пары из Map уже не выглядят так странно. Ключ и значение можно сразу разобрать в заголовке цикла:
for (const [key, value] of map) {
console.log(key, value);
}
Оператор ... работает в двух направлениях. При вызове функции он разворачивает последовательность в отдельные аргументы. Например, набор [2013, 9, 13, 15, 30] можно передать в конструктор даты:
const parts = [2013, 9, 13, 15, 30];
const date = new Date(...parts);
Так же можно вставить элементы одного массива внутрь другого. В параметрах функции ..., наоборот, собирает оставшиеся аргументы в массив. Такой параметр ставится последним. Для этой работы больше не нужно вручную превращать arguments в массив через slice.
Из Set и разворачивания получается тот самый unique, которого мне не хватало:
const values = [1, 3, 3, 5, 7, 7];
const unique = [...new Set(values)];
Сначала коллекция оставляет уникальные значения, затем мы возвращаем их в массив. Отдельного метода нет, но операция складывается из возможностей языка, у каждой из которых есть своё назначение.
Функции: значения по умолчанию и короткая запись
Параметрам функций можно задавать значения по умолчанию прямо в объявлении. Если аргумент передан, используется он. Если аргумент пропущен или равен undefined, берётся значение по умолчанию. Переданный null остаётся null — это различие важно, когда у него есть собственный смысл.
Значением по умолчанию может быть и this. Сохранённым в параметре значением this текущего вызова затем можно пользоваться во вложенных колбэках.
Стрелочная запись особенно хорошо смотрится там, где функция принимает одно значение и сразу возвращает результат выражения. Например, при преобразовании элементов массива:
const doubled = values.map(value => value * 2);
В такой записи не нужны ни слово function, ни явный return. Если тело разрастается до нескольких инструкций, понадобятся фигурные скобки и обычный возврат результата. Для меня привлекательнее всего именно короткий вариант: небольшое преобразование остаётся рядом с местом, где оно применяется.
Генератор можно продолжить
Генератор похож на функцию, но после function у него стоит звёздочка, а внутри появляется yield. Вызов такой функции возвращает объект генератора. Дальше его выполнение можно продвигать вызовами next.
Каждый next продолжает работу до следующего yield или до завершения функции. Результат содержит значение и признак done, по которому понятно, закончилась ли работа. После завершения дальнейший вызов вернёт undefined и done: true. Генератор можно перебирать и через for…of, не вызывая next вручную для каждого значения.
Обычно эту возможность показывают на числах Фибоначчи. Мне такой пример мало что объясняет. Интереснее то, что через next можно передавать значение обратно в приостановленную функцию.
Представим генератор, который получает a, возвращает a + 5, затем приостанавливается ещё раз и ждёт значение для b:
function* calculate(a) {
yield a + 5;
const b = yield null;
return a + b;
}
const calculation = calculate(5);
calculation.next();
calculation.next();
calculation.next(8);
Первый вызов next доходит до первого yield и отдаёт 10. Второй продолжает выполнение и отдаёт null. Третий передаёт в ожидающее выражение число 8: оно попадает в b, и генератор заканчивает работу с результатом 13.
На вопрос из зала, нельзя ли написать то же самое через замыкание, я отвечаю: можно. Но от генераторов я прежде всего жду другого применения — работы с асинхронными операциями, которую удастся записывать в последовательном порядке. Похожую задачу мы уже решаем с помощью библиотеки Async.
В Serenity разработчица фронтенда как раз переделывает нашу сборку проектов с использованием генераторов. Вокруг них появляются и серверные инструменты: Koa, модули для него, библиотека co. Мне интересно смотреть, как эта конструкция будет работать за пределами учебных примеров.
Классы и прежняя модель наследования
Запись классов становится привычнее для людей, пришедших из других языков. У класса есть constructor, наследование через extends и обращение к родителю через super.
При этом прототипная модель JavaScript сохраняется. Я воспринимаю классы прежде всего как синтаксический сахар: они дают более удобную запись знакомых отношений между объектами. Новое оформление не отменяет необходимости понимать прототипы.
Где пробовать новое
Поддержка возможностей пока неравномерная. Перед докладом я посмотрел на Internet Explorer 11 и, честно говоря, ожидал гораздо худшего. Оказалось, что часть работы с областями видимости и коллекциями там уже реализована. Это повод проверять каждую нужную возможность отдельно, а не судить о браузере по старой репутации.
Обратную совместимость тоже приходится выбирать под проект. Некоторые методы и коллекции можно воспроизвести полифилами. Для нового синтаксиса нужен транслятор: он преобразует программу в код, который способен разобрать старый браузер. Нельзя добавить незнакомую грамматику языка обычной функцией.
Сам я пока чаще использую новые возможности на сервере, в инструментах на Node.js. В браузерных проектах особенно хочу попробовать коллекции. В нашей работе на Node.js утечек памяти из-за новых конструкций пока не наблюдали.
Сравнивать производительность окончательно ещё рано. Реализации появляются раньше, чем их успевают хорошо оптимизировать. Даже уже знакомые методы могут долго оставаться медленными. Поэтому я не стал бы обещать, что любое переписанное на ES6 приложение сразу заработает быстрее.
Мне задают и другой вопрос: зачем ждать возможностей языка, если всё уже есть в библиотеках? Похожий разговор был с поиском элементов по CSS-селектору. Сначала каждый фреймворк писал свой селекторный движок, потом появились querySelector и querySelectorAll. Нативная возможность постепенно снимает с библиотек часть работы; со временем у неё появляется и преимущество в производительности.
Что будет с CoffeeScript
Я люблю CoffeeScript и вижу в новом JavaScript много знакомых идей. Для меня это хороший исход: язык, которым мы пользовались поверх JavaScript, помог показать востребованные конструкции. Если они становятся частью стандарта, я готов перейти на ES6 и ради совместимости использовать транслятор уже для него.
Мне важнее, чтобы выразительнее становился сам JavaScript.
В своём отделе веб-разработки я поддерживаю такие эксперименты: использование новых возможностей, написание полифилов и публикацию кода. Сам стараюсь писать и выступать о том, что появляется в языке и как это можно применить. Постоянная оглядка на старые браузеры надолго приучила нас ждать. Мне хочется, чтобы у разработчиков оставалось время и на то, что будет дальше.