JavaScript, с момента выхода стандарта ECMAScript 6 (ES6), быстро и динамично развивается. Благодаря тому, что теперь новые версии стандарта ECMA-262 выходят ежегодно, и благодаря титаническому труду всех производителей браузеров, JS стал одним из самых популярных языков программирования в мире.
Недавно я писал о новых возможностях, которые появились в стандарте ES2020. Хотя некоторые из этих возможностей и весьма интересны, среди них нет таких, которые достойны называться «революционными». Это вполне можно понять, учитывая то, что в наши дни спецификация JS обновляется достаточно часто. Получается, что у тех, кто работает над стандартом, просто меньше возможностей для постоянного внедрения в язык чего-то особенного, вроде ES6-модулей или стрелочных функций.
Но это не значит, что нечто исключительно новое в итоге в языке не появится. Собственно говоря, об этом я и хочу сегодня рассказать. Мне хотелось бы поговорить о 4 возможностях, которые, в перспективе, можно будет назвать революционными. Они находятся сейчас на разных стадиях процесса согласования предложений. Это, с одной стороны, означает, что мы можем никогда их в JavaScript и не увидеть, а с другой стороны — наличие подобных предложений даёт нам надежду на то, что мы их, всё же, когда-нибудь встретим в языке.
Декораторы (decorators)
Начнём мы с возможности, которую, вероятно, чаще всего просят включить в язык, и вокруг которой поднято довольно много шума. Пару лет назад о ней писали буквально повсюду. Речь идёт о декораторах.
Возможно, вы уже знакомы с декораторами. Особенно — если вы пишете на TypeScript. Это, в сущности, концепция метапрограммирования, нацеленная на то, чтобы дать разработчику возможность «внедрять» собственный функционал в классы, в их отдельные поля и методы, в итоге делая классы программируемыми.
Взгляните на следующий пример:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
Тут я решил действовать осторожно и привёл пример использования TypeScript-декораторов. Сделал я это, преимущественно, для того, чтобы показать общую идею. В вышеприведённом фрагменте кода мы создали декоратор sealed
и применили его к классу Greeter
. Как несложно заметить, декоратор — это просто функция, у которой есть доступ к конструктору класса, к которому она применена (это — цель). Мы используем ссылку на конструктор, а так же — метод Object.seal()
для того чтобы сделать класс нерасширяемым.
Для того чтобы применить декоратор к классу, мы записываем имя декоратора со значком @
сразу перед объявлением класса. В результате получается, что перед объявлением класса появляется конструкция вида @[name]
, что в нашем случае выглядит как @sealed
.
Проверить работоспособность декоратора можно, скомпилировав этот TS-код с включённой опцией experimentalDecorators
и попробовав изменить прототип класса:
Greeter.prototype.test = "test"; // ERROR
В результате, теперь у вас должно появиться общее понимание того, зачем вообще нужны декораторы. Но мне хотелось бы затронуть тут один непростой вопрос. Он касается того, в каком состоянии сейчас находится согласование предложения по декораторам.
Я решил использовать TypeScript для демонстрации декораторов не без причины. Дело в том, что предложение, касающееся внедрения декораторов в JavaScript, появилось уже несколько лет назад. И оно сейчас «всего лишь» на 2 стадии согласования из 4. И в синтаксис, и в возможности декораторов из этого предложения постоянно вносят изменения. Но это не останавливает JS-сообщество от использования данной концепции. Для того чтобы в этом убедиться, достаточно взглянуть на огромные опенсорсные проекты вроде TypeScript или Angular v2+.
Правда, всё это, с течением времени, и по мере того, как предложение развивается, ведёт к проблеме, связанной с несовместимостью спецификаций. А именно, спецификация декораторов сильно развилась с момента появления соответствующего предложения, но многие проекты всё ещё её не реализовали. Пример на TypeScript, показанный выше, это демонстрация реализации более старой версии предложения. То же самое можно сказать и об Angular, и даже о Babel (хотя, в случае с Babel, можно говорить о том, что сейчас ведётся работа над реализацией более новой версии этой спецификации). В целом же, более новая версия предложения, в которой используется ключевое слово decorator
и синтаксис, пригодный для компоновки, пока ещё не нашла хоть какого-то заметного применения.
В итоге можно сказать, что у декораторов есть потенциальная возможность изменить то, как мы пишем код. И эти изменения уже показывают себя, даже учитывая то, что внедрение декораторов находится на ранней стадии. Правда, учитывая текущее положение дел, декораторы лишь фрагментируют сообщество, и я полагаю, что они пока ещё не готовы к по-настоящему серьёзному использованию. Я посоветовал бы тем, кому декораторы интересны, немного подождать перед их внедрением в продакшн. Моя рекомендация, правда, не относится к тем, кто использует фреймворки, в которых применяются декораторы (вроде Angular).
Области (realms)
Теперь давайте немного сбавим обороты и поговорим об одной возможности, не такой сложной, как декораторы. Речь идёт об областях.
Возможно, вы уже бывали в ситуациях, когда вам нужно запустить собственный код, или код стороннего разработчика, но при этом сделать это так, чтобы этот код не повлиял бы на глобальное окружение. Многие библиотеки, в особенности — браузерные, работают через глобальный объект window
. В результате они могут друг другу мешать в том случае, если в проекте используется слишком много неконтролируемых разработчиком библиотек. Это может приводить к ошибкам.
Сейчас решение этой проблемы в браузере заключается в использовании элементов <iframe>
, а в некоторых особых случаях — в использовании веб-воркеров. В среде Node.js ту же проблему решают с помощью модуля vm
или с помощью дочерних процессов. Для решения подобных проблем и предназначен API Realms.
Этот API направлен на то, чтобы позволить разработчикам создавать отдельные глобальные окружения, называемые областями. В каждой такой области имеются собственные глобальные сущности. Взгляните на следующий пример:
var x = 39;
const realm = new Realm();
realm.globalThis.x; // undefined
realm.globalThis.x = 42; // 42
realm.globalThis.x; // 42
x; // 39
Здесь мы создаём новый объект Realm
, используя соответствующий конструктор. С этого момента у нас есть полный доступ к новой области и к её глобальным объектам через свойство globalThis
(введённое в ES2020). Тут можно видеть, что переменные основного «инкубатора» отделены от переменных в области, которую мы создали.
В целом, API Realms планируется как очень простой механизм, но — механизм полезный. Этот API отличается весьма специфическим набором вариантов использования. Он не даёт нам более высокий уровень безопасности, или возможности по многопоточному выполнению кода. Но он отлично, не создавая при этом лишней нагрузки на систему, справляется со своей основной задачей, с обеспечением базовых возможностей по созданию изолированных окружений.
Предложение по API Realms сейчас находится на 2 стадии согласования. Когда оно, в итоге, попадёт в стандарт, можно будет ожидать того, что оно будет использоваться в «тяжёлых» библиотеках, зависящих от глобальной области видимости, в онлайн-редакторах кода, оформленных в виде «песочниц», в различных приложениях, ориентированных на тестирование.
Do-выражения (do expressions)
Синтаксис JavaScript, как и синтаксис большинства языков программирования, включает в себя инструкции (statements) и выражения (expressions). Самая заметная разница между этими конструкциями заключается в том, что выражения могут быть использованы как значения (то есть — их можно назначать переменным, передавать функциям и так далее), в то время как инструкции в таком качестве использовать нельзя.
Из-за этого различия именно выражениям чаще всего отдают предпочтение, стремясь к написанию более чистого и компактного кода. В JavaScript это можно увидеть, если проанализировать популярность функциональных выражений (function expression), куда входят и стрелочные функции, в сравнении с объявлениями функций (function declaration, function statement). Та же ситуация видна, если сравнить методы для перебора массивов (вроде forEach()
) с циклами. То же самое, в случае с более продвинутыми программистами, справедливо и при сравнении тернарного оператора и инструкции if
.
Предложение по do-выражениям, находящееся на 1 стадии согласования, нацелено на то, чтобы ещё сильнее расширить возможности JS-выражений. И, кстати, не надо путать понятие «do-выражение» с циклами do…while
, так как это — совершенно разные вещи.
Вот пример:
let x = do {
if (foo()) {
f();
} else if (bar()) {
g();
} else {
h();
}
};
Здесь представлен синтаксис из предложения по do-выражениям. В целом, перед нами — фрагмент JS-кода, обёрнутый в конструкцию do {}
. Последнее выражение в этой конструкции «возвращается» в качестве итогового значения всего do-выражения.
Похожий (но не идентичный) эффект достижим с использованием IIFE (Immediately Invoked Function Expression, немедленно вызываемое функциональное выражение). Но в случае с do-выражениями весьма привлекательным кажется их компактный синтаксис. Тут, при схожем функционале, не нужно ни return
, ни каких-то некрасивых вспомогательных конструкций вроде (() => {})()
. Именно поэтому я полагаю, что после того, как do-выражения попадут в стандарт, их воздействие на JavaScript будет сравнимо с воздействием на язык стрелочных функций. Удобство выражений и дружелюбный синтаксис, так сказать, в одной упаковке, выглядят весьма заманчиво.
Сопоставление с образцом (pattern matching)
Эта возможность последняя в данном материале, но она далеко не последняя по значимости. Речь идёт о предложении, направленном на внедрении в язык механизма сопоставления с образцом.
Возможно, вы знакомы с JS-инструкцией switch
. Она похожа на if-else
, но её возможности немного сильнее ограничены, и она, безусловно, лучше подходит для организации выбора одной из множества альтернатив. Вот как она выглядит:
switch (value) {
case 1:
// ...
break;
case 2:
// ...
break;
case 3:
// ...
break;
default:
// ...
break;
}
Лично я считаю инструкцию switch
слабее инструкции if
, так как switch
умеет сравнивать то, что ей передано, только с конкретными значениями. Данное ограничение можно обойти, но я не могу придумать — зачем. Кроме того, инструкция switch
перегружена вспомогательными элементами. В частности, речь идёт об инструкциях break
.
Механизм сопоставления с образцом можно рассматривать как более функциональную, основанную на выражениях, и потенциально более универсальную версию инструкции switch
. Вместо простого сравнения значений, механизм сопоставления с образцом позволяет разработчику сравнивать значения со специальными образцами (шаблонами), которые поддаются глубокой настройке. Вот фрагмент кода, демонстрирующий пример предложенного API:
const getLength = vector => case (vector) {
when { x, y, z } -> Math.hypot(x, y, z)
when { x, y } -> Math.hypot(x, y)
when [...etc] -> vector.length
}
getLength({x: 1, y: 2, z: 3})
Тут используется довольно-таки необычный для JavaScript синтаксис (хотя он основан на синтаксисе, встречающемся в таких языках, как Rust или Scala), у которого есть некоторые общие черты с уже известной нам инструкцией switch
. Здесь, вместо ключевого слова switch
, используется слово case
, которое отмечает начало блока, в котором выполняется проверка значения. Затем, внутри блока, используя ключевое слово when
, мы описываем шаблоны, с помощью которых хотим проверять значения. Синтаксис шаблонов напоминает синтаксис уже имеющегося в языке механизма деструктурирования объектов. Сравнивать значения можно с объектами, содержащими выбранные свойства, их можно сравнивать со значениями свойств объектов и со многими другими сущностями. Для того чтобы узнать подробности о данном механизме — взгляните на этот документ.
После описания шаблона идёт стрелка («flat arrow», ->
), указывающая на выражение (в перспективе — даже на другое значение), которое должно быть вычислено при нахождении совпадения с образцом.
Я полагаю, что наличие в JS подобных возможностей позволит нам писать, так сказать, код нового поколения. Однако предложенный сейчас синтаксис кажется мне несколько тяжеловесным, так как он вводит в язык много совершенно новых конструкций. И тот факт, что данное предложение всё ещё находится на 1 стадии согласования, заставляет меня думать о том, что ему ещё есть куда улучшаться. Эта возможность выглядит весьма многообещающе, но ей предстоит пройти ещё долгий путь прежде чем она попадёт в официальную спецификацию JS.
Итоги
На этом я завершаю рассказ о революционных возможностях JavaScript, которые, возможно, мы увидим в языке в будущем. Есть и другие подобные возможности, например — предложения о внешней стандартной библиотеке и о конвейерном операторе. Но я выбрал для этого материала только то, что показалось мне самым интересным. Учитывайте, что эти возможности всё ещё находятся на стадии предложений. Со временем они могут измениться. А может случиться так, что в стандарт они так никогда и не попадут. Но если вы, в любом случае, хотите быть в числе тех, кто раньше других пользуется подобными возможностями, я советую вам присмотреться к таким проектам, как Babel. Эти проекты дают путёвку в жизнь многим JS-предложениям (в особенности тем, которые связаны с синтаксисом). Это позволяет всем желающим экспериментировать с новейшими возможностями задолго до того, как они появятся в реализациях языка.
Каких возможностей вам больше всего не хватает в JavaScript?
Автор: ru_vds