Пять новых возможностей JavaScript в 2024 году
Следующие заявки имеют все шансы попасть в версию ECMAScript этого года:
Обновления ECMAScript
Новая версия JS всегда вызывает ажиотаж. Начиная с обновления ES6, каждый год выходит новая версия, и мы ожидаем, что в этом году (ES2024) она появится примерно в июне.
ES6 — это масштабный релиз, вышедший через шесть лет после своего предшественника, ES5. Производители браузеров и разработчики JavaScript были перегружены огромным количеством новых функций, которые нужно было принять и изучить. С тех пор, чтобы не допустить такого большого количества новых функций сразу, существует ежегодный цикл релизов.
Ежегодный цикл релиза включает в себя предложение новых функций, которые затем обсуждаются, оцениваются и голосуются комитетом, прежде чем будут добавлены в язык. Этот процесс также позволяет браузерам попробовать реализовать предложения до того, как они будут официально добавлены в язык, что может помочь устранить любые проблемы с реализацией.
Как уже говорилось, решение о новых возможностях JavaScript (или ECMAScript) принимает Technical Committee 39 (TC39). В состав TC39 входят представители всех основных производителей браузеров, а также эксперты по JavaScript. Они регулярно встречаются, чтобы обсудить новые возможности языка и то, как они могут быть реализованы. Новые возможности выдвигаются в виде предложений (их может внести любой желающий), после чего члены комитета голосуют, может ли каждое предложение перейти на следующий этап. Для каждого предложения существует 4 этапа; как только предложение достигает четвёртого этапа, ожидается, что оно будет включено в следующую версию ES.
Важной частью спецификации ES является то, что она должна быть обратно совместимой. Это означает, что любые новые функции не могут сломать Интернет, изменив работу предыдущих версий ES. Таким образом, они не могут изменить работу существующих методов, а могут только добавить новые, поскольку любой веб-сайт, работающий с потенциально существующим методом, будет подвержен риску поломки.
Полный список всех текущих предложений можно посмотреть здесь.
Temporal
В опросе State of JS 2022 третьим по частоте ответом на вопрос Чего, по вашему мнению, сейчас не хватает в JavaScript?
был Лучшего управления датами
.
Это привело к появлению предложения Temporal
, предлагающего стандартный глобальный объект для замены объекта Date
и устраняющего ряд проблем, которые на протяжении многих лет причиняли разработчикам много боли при работе с датами в JavaScript.
Работа с датами в JavaScript — это почти всегда страшная задача; приходится иметь дело с небольшими, но раздражающими несоответствиями, такими как сумасшествие, когда месяцы индексируются по нулям, а дни месяца начинаются с 1.
Сложность работы с датами привела к появлению таких популярных библиотек, как Moment, Day.JS и date-fns, пытающихся решить эти проблемы. Однако Temporal API призван решить все эти проблемы нативно.
Temporal
будет поддерживать несколько часовых поясов и негригорианские календари, а также предоставит простой в использовании API, который значительно упростит разбор дат из строк. Кроме того, все объекты Temporal
будут неизменяемыми, что поможет избежать ошибок при случайном изменении даты.
Давайте рассмотрим несколько примеров наиболее полезных методов, предлагаемых Temporal API.
Temporal.Now.Instant()
Temporal.Now.Instant()
вернёт объект DateTime
с точностью до наносекунды. Вы можете указать конкретные даты с помощью метода from
, например:
const olympics = Temporal.Instant.from('2024-07-26T20:24:00+01:00');
Это создаст объект DateTime
, который представляет начало Олимпийских игр в Париже в конце этого года в 20:24 26 июля 2024 года (UTC).
PlainDate()
Он позволяет создать только дату, без времени:
new Temporal.PlainDate(2024, 7, 26);
Temporal.PlainDate.from('2024-07-26');
// Оба возвращают объект PlainDate, представляющий 26 июля 2024 года.
PlainTime()
В дополнение к PlainDate()
мы можем использовать эту функцию для создания просто времени без даты, используя .PlainTime()
:
new Temporal.PlainTime(20, 24, 0);
Temporal.PlainTime.from('20:24:00');
// оба возвращают объект PlainTime 20:24
PlainMonthDay()
Функция PlainMonthDay()
похожа на PlainDate
, но возвращает только месяц и день без информации о годе (полезно для дат, которые повторяются в один и тот же день каждый год, например, Рождество и День святого Валентина):
const valentinesDay = Temporal.PlainMonthDay.from({ month: 2, day: 14 });
PlainYearMonth()
Кроме того, есть PlainYearMonth
, возвращающий только год и месяц (полезно для представления целого месяца года):
const march = Temporal.PlainYearMonth.from({ month: 3, year: 2024 });
Вычисления
С объектами Temporal можно производить различные вычисления. Вы можете добавлять и вычитать различные единицы времени в объекте даты:
const today = Temporal.Now.plainDateISO();
const lastWeek = today.subtract({ days: 7});
const nextWeek = today.add({ days: 7 });
Методы until
и since
позволяют узнать, сколько времени осталось до определённой даты или с момента её наступления. Например, следующий код покажет, сколько дней осталось до Олимпийских игр в Париже:
olympics.until().days
Или сколько часов прошло с Дня святого Валентина
valentinesDay.since().hours
Эти методы возвращают объект Temporal.Duration
, который можно использовать для измерения количества времени, имеющего множество различных единиц измерения и вариантов округления.
Дополнения
Из объекта Date
можно извлечь год, месяц и день, а из объекта Time
— часы, минуты, секунды, миллисекунды, микросекунды и наносекунды (микросекунды и наносекунды недоступны в текущем объекте DateTime
). Например:
olympics.hour;
<< 20
Есть и другие свойства, такие как dayOfWeek
(возвращает 1
для понедельника и 7
для воскресенья), daysInMonth
(возвращает 28
, 29
, 30
или 31
в зависимости от месяца) и daysinYear
(возвращает 365
или 366
в зависимости от високосного года).
Объекты временных дат Temporal
также имеют метод compare
, который можно использовать для упорядочивания дат с помощью различных алгоритмов сортировки.
Temporal
в настоящее время является предложением Stage 3, находящимся в процессе внедрения разработчиками браузеров, так что, похоже, его время пришло (каламбур не удался). С полной документацией можно ознакомиться здесь. Также есть cookbook примеров использования. В паре с API Intl.DateTimeFormat
можно выполнять некоторые очень удобные операции с датами.
Конвейерный оператор
В опросе State of JS 2022 на вопрос Чего, по вашему мнению, сейчас не хватает в JavaScript?
шестым по значимости ответом был "Конвейерный оператор".
Ознакомиться с предложением Конвейерного оператора можно здесь.
Конвейерный оператор — стандартная функция функциональных языков, позволяющая "передавать" значение из одной функции в другую, при этом результат предыдущей функции используется в качестве параметра для следующей (подобно тому, как Fetch API передаёт любые возвращаемые данные из одного промиса в другой).
Например, мы хотим последовательно применить к строке три функции:
- Присоединить строку "Listen up!" к началу исходной строки.
- Добавить три восклицательных знака в конец строки.
- Перевести весь текст в верхний регистр.
Эти функции можно записать следующим образом:
const exclaim = string => string + "!!!"
const listen = string => "Listen up! " + string
const uppercase = string => string.toUpperCase()
Эти функции можно применить, вложив их друг в друга следующим образом:
const text = "Hello World"
uppercase(exclaim(listen(text)))
<< "LISTEN UP! HELLO WORLD!!!"
Но глубокое вложение нескольких вызовов функций может быстро запутать, тем более что значение (text
), передаваемое в качестве аргумента, оказывается глубоко встроенным в выражение, что затрудняет его идентификацию.
Другая проблема вложенности функций заключается в том, что функции применяются в порядке от последнего к первому, то есть сначала применяются внутренние функции. Так, в данном случае к исходному значению text
применяется listen
, затем exclaim
, а самая внешняя функция, uppercase
, будет применена последней. Для больших и сложных функций это становится сложным и не интуитивным.
Альтернативный вариант — использовать цепочку функций следующим образом:
const text = "Hello World"
text.listen().exclaim().uppercase()
Это решает множество проблем, связанных с вложенными функциями. Передаваемый аргумент находится в начале, а каждая функция отображается в том порядке, в котором она применяется, поэтому сначала применяется listen()
, затем exclaim()
, затем uppercase()
.
К сожалению, этот пример не сработает, потому что функции listen
, exclaim
и uppercase
не являются методами класса String
. Их можно было бы добавить путём обезьяньего патча класса String
, но такой метод не одобряется.
Это означает, что, хотя цепочка выглядит намного лучше, чем вложенность функций, её можно использовать только со встроенными функциями (как это часто делается с методами Array
).
Конвейер сочетает в себе простоту использования цепочки, но при этом позволяет использовать её с любыми функциями. Согласно текущему предложению, приведённый пример будет выглядеть следующим образом:
text |> listen(%) |> exclaim(%) |> uppercase(%)
Токен %
— это заполнитель, используемый для представления значения вывода предыдущей функции, хотя вполне вероятно, что в официальном релизе символ %
будет заменён на какой-нибудь другой. Это позволяет использовать в конвейере функции, принимающие более одного аргумента.
Конвейерная обработка сочетает в себе простоту цепочки, но при этом может использоваться с любыми собственными функциями, которые вы написали. Единственное условие — необходимо убедиться, что тип выхода одной функции совпадает с типом входа следующей функции в цепочке.
Конвейерная обработка лучше всего работает с каррированными функциями, принимающими только один аргумент, который передаётся по конвейеру из возвращаемого значения предыдущей функции. Это значительно упрощает функциональное программирование, поскольку небольшие блоки функций можно объединять в цепочки для создания более сложных составных функций. Это также упрощает реализацию частичного применения.
Несмотря на свою популярность, конвейерный оператор с трудом продвигается дальше второй стадии процесса. Это связано с разногласиями, как должна быть выражена нотация, а также с опасениями по поводу производительности памяти и того, как она может работать с await
. Похоже, что комитет постепенно приходит к какому-то соглашению, так что, надеюсь, конвейерный оператор сможет быстро пройти все стадии и появиться в этом году.
К счастью, конвейерный оператор был реализован в Babel начиная с версии 7.15.
Нам хотелось бы, чтобы конвейерный оператор был реализован и внедрён в этом году, поскольку он поможет повысить авторитет JavaScript как серьёзного функционального языка программирования.
Record и Tuple
Предложение Record и Tuple направлено на привнесение неизменяемых структур данных в JavaScript.
Кортежи (Tuple
) похожи на массивы — упорядоченные списки значений — но они неизменяемы. Это означает, что каждое значение в кортеже должно быть либо примитивным значением, либо другой записью (Record
) или кортежем (не массивами или объектами, поскольку они в JavaScript являются изменяемыми).
Кортеж создаётся аналогично литералу массива, но с символом хэш (#
) впереди:
const heroes = #["Batman", "Superman", "Wonder Woman"]
После его создания никакие другие значения не могут быть добавлены или удалены. Значения также не могут быть изменены.
Записи похожи (Record
) на объекты — коллекции пар ключ-значение — но они также неизменяемы. Они создаются так же, как и объекты, но так же, как и кортежи, начинаются с хэша:
const traitors = #{
diane: false,
paul: true,
zac: false,
harry: true
}
В записях по-прежнему будет использоваться точечная нотация для доступа к свойствам и методам:
traitors.paul
<< true
Нотация с квадратными скобками, которую используют массивы, может быть использована и для кортежей:
heroes[1]
<< "Superman"
Но поскольку они неизменяемы, невозможно обновить ни одно из свойств:
traitors.paul = false
<< Error
heroes[1] = "Supergirl"
<< Error
Неизменяемость кортежей и записей означает, что их можно сравнивать с помощью оператора ===
:
heroes === #["Batman", "Superman", "Wonder Woman"];
<< true
Следует отметить, что порядок свойств не имеет значения при проверке равенства записей:
traitors === #{
ross: false,
zac: false,
paul: true,
harry: true
};
// по-прежнему true, даже если порядок людей изменился
<< true
Однако порядок имеет значение для кортежей, поскольку они представляют собой упорядоченный список данных:
heroes === #["Wonder Woman", "Batman", "Superman"];
<< false
На этой странице есть удобный учебник с игровой площадкой, чтобы вы могли освоить работу с записями и кортежами.
RegExp флаг /v
Регулярные выражения включены в JavaScript с версии 3, и с тех пор было сделано множество улучшений (например, поддержка Юникода с флагом u
в ES2015). Предложение флага v
направлено на выполнение всего того, что делает флаг u
, но вносит дополнительные преимущества, которые мы рассмотрим в примерах ниже.
Простое применение флага v
заключается в добавлении /v
в конец регулярного выражения.
Например, следующий код можно использовать для проверки того, является ли символ эмодзи:
const isEmoji = /^\p{RGI_Emoji}$/v;
isEmoji.test("💚");
<< true
isEmoji.test("🐨");
<< true
Для идентификации эмодзи используется шаблон RGI_Emoji
.
Флаг v также позволяет использовать нотацию набора/set в регулярных выражениях. Например, вы можете вычесть один шаблон из другого с помощью оператора --
. Следующий код может быть использован для удаления всех сердечек из набора эмодзи:
const isNotHeartEmoji = /^[\p{RGI_Emoji_Tag_Sequence}--\q{💜💚♥️💙🖤💛🧡🤍🤎}]$/v;
isNotHeartEmoji.test("💚");
<< false
isNotHeartEmoji.test("🐨");
<< true
Вы можете искать пересечение двух шаблонов с помощью &&
. Например, следующий код найдёт пересечение греческих символов и букв:
const GreekLetters = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;
GreekLetters.test('π');
<< true
GreekLetters.test('𐆊');
<< false
Флаг v
также устраняет ряд проблем с нечувствительностью регистра, присущих флагу u
, что делает его гораздо лучшим вариантом для использования практически во всех случаях.
Флаг v
регулярных выражений достиг стадии 4 в 2023 году и был реализован во всех основных браузерах, поэтому вполне ожидаемо, что он войдёт в спецификацию ES2024.
Декораторы
Предложение декораторов нацелено на использование декораторов для нативного расширения классов JavaScript.
Декораторы распространены во многих объектно-ориентированных языках, таких как Python, и уже включены в TypeScript. Они представляют собой стандартную абстракцию метапрограммирования, позволяющую добавлять дополнительную функциональность в функцию или класс, не меняя их структуру. Например, вы можете добавить дополнительную проверку в метод и сделать это, создав декоратор проверки, проверяющий данные, введённые в форму.
Хотя JavaScript позволяет использовать функции для реализации этого паттерна проектирования, большинство объектно-ориентированных программистов предпочли бы более простой и естественный способ достижения этой цели, чтобы облегчить себе жизнь.
Данное предложение добавляет синтаксический сахар, позволяющий с лёгкостью реализовать декоратор внутри класса, не задумываясь о его привязке к классу. Это обеспечивает более чистый способ расширения элементов класса, таких как поля класса, методы класса или аксессоры класса, и может применяться даже ко всему классу.
Декораторы обозначаются префиксом в виде символа @
и всегда располагаются непосредственно перед кодом, который они "декорируют".
Например, декоратор класса будет располагаться непосредственно перед определением класса. В приведённом ниже примере декоратор validation
применяется ко всему классу FormComponent
:
@validation
class FormComponent {
// код класса
}
// Функция декоратора также нуждается в определении
function validation(target) {
// код валидации
}
Декоратор методов класса находится непосредственно перед декорируемым методом. В приведённом ниже примере декоратор validation
применяется к методу submit
:
class FormComponent {
// код класса
@validation
submit(data) {
// код метода
}
}
// Функция декоратора также нуждается в определении
function validation(target) {
// код валидации
}
Определения функций декораторов принимают два параметра: значение и контекст. Аргумент значение указывает на декорируемое значение (например, метод класса), а контекст содержит метаданные о значении, такие как является ли это функцией или нет, его имя, а также является ли оно статическим или приватным. Вы также можете добавить в контекст функцию-инициализатор, выполняющуюся при создании класса.
Предложение по декораторам в настоящее время находится на третьей стадии и уже реализовано в Babel, так что его можно опробовать.
Заключение
А что думаете вы? Что бы хотели увидеть в спецификации в этом году? Все эти функции станут отличным дополнением к JavaScript, так что скрестим пальцы, что они появились в этом году!