[Перевод] JavaScript ES6: слабые стороны
В июне 2018 года стандарт ECMAScript 2015 (ES6) отметил свой трёхлетний юбилей. В ES6, во-первых, появилось множество новых возможностей JavaScript, во-вторых, с этого стандарта начинается новая эра развития языка. Кроме того, это был последний масштабный релиз JS, так как теперь TC39 применяет схему выпуска небольших ежегодных выпусков стандарта, а не выводит его новую редакцию раз в несколько лет.
Последние 4 года ES6, вполне оправданно, привлекает к себе всеобщее внимание. Автор материала, перевод которого мы сегодня публикуем, говорит, что он, всё это время, благодаря Babel, писал весь код с использованием современного варианта спецификаций JS. Он полагает, что прошло достаточно времени для того, чтобы критически проанализировать новые возможности ES6. В особенности его интересует то, чем он некоторое время пользовался, а потом пользоваться перестал из-за того, что это ухудшало его код.
О слабых сторонах JS
Дуглас Крокфорд, в своей книге «JavaScript: сильные стороны», писал и о том, что можно считать слабыми сторонами языка. Это — нечто такое, чем, по его мнению, пользоваться не стоит. К счастью, среди новшеств ES6 нет ничего столь же неприглядного, как некоторые старые проблемные возможности JS, такие, как оператор нестрогого равенства, выполняющий неявное приведение типов, функция eval()
и инструкция with
. Новые возможности ES6 спроектированы куда лучше. Однако и в нём есть некоторые вещи, которых я избегаю. Те возможности, которые входят в мой список «слабых сторон» JS, попали в этот список по следующим причинам:
- Они, по сути, являются «ловушками». То есть, кажется, что предназначены они для выполнения неких действий, и в большинстве случаев, работают так, как ожидается. Однако иногда они ведут себя неожиданно, что легко может привести к появлению ошибок.
- Они увеличивают объём языка в обмен на небольшую выгоду. Такие возможности дают разработчику какие-то небольшие преимущества, но требуют от того, кто пытается разобраться с его кодом, знания о неких механизмах, обычно где-то скрытых. Это вдвойне справедливо для возможностей API, когда использование подобной возможности означает, что другой код, взаимодействующий с кодом, написанным неким разработчиком, обязан знать о применении этой возможности API.
Теперь, руководствуясь этими соображениями, поговорим о слабых сторонах ES6.
Ключевое слово const
До выхода ES6 переменные в JavaScript можно было объявлять с использованием ключевого слова var
. Кроме того, переменные можно было и вовсе не объявлять, тогда они, даже если используются в функциях, попадают в глобальную область видимости. Роль переменных могут играть свойства объектов, а функции объявляют с использованием ключевого слова function
. У ключевого слова var
есть определённые особенности.
Так, оно позволяет создавать переменные, добавляемые к глобальному объекту, или такие, область видимости которых ограничена функциями. Однако ключевое слово var
не обращает внимания на блоки кода. Кроме того, обратиться к переменной, объявленной с помощью ключевого слова var
можно и в коде, расположенном до команды её объявления. Это явление известно как поднятие переменных. Эти особенности, если их не учитывать, способны приводить к возникновению ошибок. Для того чтобы исправить ситуацию, в ES6 появились два новых ключевых слова для объявления переменных: let
и const
. Они решали основные проблемы var
. А именно, речь идёт о том, что переменные, объявленные с использованием этих ключевых слов, имеют блочную область видимости, как результат, например, переменная, объявленная в цикле, не видна за его пределами. Кроме того, использование let
и const
не допускает обращения к переменным до их объявления. Подобное приведёт к ошибке ReferenceError
. Это было большим шагом вперёд. Однако, появление двух новых ключевых слов, а также их особенности, привели к дополнительной путанице.
Значение переменной (константы), объявленной с помощью ключевого слова const
, нельзя перезаписать после объявления. Это — единственное различие между const
и let
. Выглядит эта новая возможность полезной, и она, действительно, может принести определённую пользу. Проблема заключается в самом ключевом слове const
. То, как ведут себя константы, объявленные с его помощью, не соответствует тому, что большинство разработчиков ассоциируют с понятием «константа».
const CONSTANT = 123;
// Эта команда приведёт к ошибке "TypeError: invalid assignment to const `CONSTANT`"
CONSTANT = 345;
const CONSTANT_ARR = []
CONSTANT_ARR.push(1)
// А эта команда выведет [1] без каких-либо сообщений об ошибках
console.log(CONSTANT_ARR)
Использование ключевого слова const
предотвращает запись в константу нового значения, но не делает объекты, на которые ссылаются подобные константы, иммутабельными. Эта особенность даёт слабую защиту от изменения значений при работе с большинством типов данных. В результате, из-за того, что использование const
может привести к путанице, и из-за того, что при наличии ключевого слова let
наличие const
выглядит избыточным, я решил всегда использовать let
.
Тегированные шаблонные строки
Ключевое слово const
— это пример того, как спецификация создаёт слишком много способов решения слишком малого количества задач. В случае с тегированными шаблонными строками перед нами обратная ситуация. Синтаксис таких строк рассматривался комитетом TC39 как способ решения задач интерполяции строк и работы с многострочными строками. Затем эту возможность решили расширить за счёт использования макросов.
Если вы раньше не встречались с тегированными шаблонными строками, учтите, что они немного напоминают декораторы для строк. Вот пример работы с ними с MDN:
var person = 'Mike';
var age = 28;
function myTag(strings, personExp, ageExp) {
var str0 = strings[0]; // "that "
var str1 = strings[1]; // " is a "
// Технически (в нашем примере)
// после последнего выражения имеется строка,
//но она пуста, поэтому не обращайте на неё внимания.
// var str2 = strings[2];
var ageStr;
if (ageExp > 99){
ageStr = 'centenarian';
} else {
ageStr = 'youngster';
}
return str0 + personExp + str1 + ageStr;
}
var output = myTag`that ${ person } is a ${ age }`;
console.log(output);
// that Mike is a youngster
Тегированные шаблонные строки нельзя назвать совершенно бесполезными. Вот обзор некоторых вариантов их использования. Например, они полезны при очистке HTML-кода. И, в настоящий момент, их применение демонстрирует самый аккуратный подход в ситуациях, когда нужно выполнять одну и ту же операцию над всеми входными данными произвольного строкового шаблона. Однако, нужно такое сравнительно редко, сделать то же самое можно с помощью соответствующего API (хотя такое решение получается длиннее). И, для решения большинства задач, использование API будет не хуже применения тегированных шаблонных строк. Эта функция не добавляет в язык новых возможностей. Она добавляет в него новые подходы к работе с данными, с которыми должны быть знакомы те, кому придётся читать код, написанный с использованием тегированных шаблонных строк. А я стремлюсь к тому, чтобы мой код оставался как можно более чистым и понятным.
Переусложнённые выражения деструктурирующего присваивания
Некоторые возможности языка выглядят отлично, когда используются для решения простых задач, однако, когда задачи усложняются, эти возможности могут выйти из-под контроля. Например, мне нравится тернарный условный оператор:
let conferenceCost = isStudent ? 50 : 200
Однако код, написанный с его помощью, становится сложно понять, если, применяя такой оператор, начать использовать вложенные конструкции:
let conferenceCost = isStudent ? hasDiscountCode ? 25 : 50 : hasDiscountCode ? 100 : 200;
То же самое можно сказать и о деструктурирующем присваивании. Этот механизм позволяет вытаскивать значения переменных из объектов или массивов:
let {a} = {a: 2, b: 3};
let [b] = [4, 5];
console.log(a, b) // 2, 4
Кроме того, при его использовании можно переименовывать переменные, получать вложенные значения, задавать значения по умолчанию:
let {a: val1} = {a: 2, b: 3};
let [{b}] = [{a:3, b:4} , {c: 5, d: 6}];
let {c=6} = {a: 2, c: 5};
let {d=6} = {a: 2, c: 5};
console.log(val1, b, c) // 2, 4, 5, 6
Всё это замечательно — до тех пор, пока дело не дойдёт до построения сложных выражений с использованием всех этих возможностей. Например, в нижеприведённом выражении объявляется 4 переменные: userName
, eventType
, eventDate
, и eventId
. Их значения берутся из разных мест структуры объекта eventRecord
.
let eventRecord = {
user: { name: "Ben M", email: "ben@m.com" },
event: "logged in",
metadata: { date: "10-10-2017" },
id: "123"
};
let {
user: { name: userName = "Unknown" },
event: eventType = "Unknown Event",
metadata: [date: eventDate],
id: eventId
} = obj;
Понять такой код практически невозможно. Эту задачу можно решить с помощью куда более удобочитаемого кода, если воспользоваться несколькими операциями деструктурирования или вовсе от них отказаться.
let eventRecord = {
user: { name: "Ben M", email: "ben@m.com" },
event: "logged in",
metadata: { date: "10-10-2017" },
id: "123"
};
let userName = eventRecord.user.userName || 'Unknown';
let eventDate = eventRecord.metadata.date;
let {event:eventType='UnknownEvent', id:eventId} = eventRecord;
У меня нет чёткого ориентира, указывающего на то, что выражение деструктурирующего присваивания нуждается в переработке. Однако, каждый раз, когда я смотрю на подобное выражение и не могу мгновенно понять, какую задачу оно решает, какие переменные в нём используются, я понимаю, что приходит время упростить код ради улучшения его читабельности.
Дефолтный экспорт
У ES6 есть одна приятная особенность. Заключается она в том, как его разработчики подошли к стандартизации того, что раньше делалось с помощью различных библиотек, нередко конкурирующих друг с другом. Так в спецификации появились классы, промисы, модули. Это — всё то, чем сообщество JS-разработчиков пользовалось до ES6, находя это в сторонних библиотеках. Например, модули ES6 представляют собой отличную замену того, что вылилось в войну форматов AMD/CommonJS, и дают удобный синтаксис для организации импорта.
Модули ES6 поддерживают два основных способа экспорта значений: именованный экспорт (named export) и дефолтный экспорт, или экспорт по умолчанию (default export):
const mainValue = 'This is the default export
export default mainValue
export const secondaryValue = 'This is a secondary value;
export const secondaryValue2 = 'This is another secondary value;
В модуле может использоваться несколько команд именованного экспорта, но лишь одна команда экспорта по умолчанию. При импорте того, что экспортировано с помощью команды экспорта по умолчанию, в импортирующем файле можно дать тому, что экспортировано по умолчанию, любое имя, так как в ходе выполнения этой операции поиска имён не производится. При использовании именованного экспорта нужно использовать имена переменных из экспортирующего файлах, хотя переименование также возможно.
// дефолтный импорт
import renamedMainValue from './the-above-example';
// именованный импорт
import {secondaryValue} from './the-above-example';
// именованный импорт с переименованием
import {secondaryValue as otherValue} from './the-above-example';
Дефолтный экспорт пользовался особым вниманием разработчиков стандарта ES6, и они намеренно создали более простой синтаксис для него. Однако на практике мне удалось выяснить, что пользоваться технологией именованного экспорта предпочтительнее по следующим причинам.
- При использовании именованного экспорта имена экспортируемых переменных, по умолчанию, соответствуют именам импортируемых переменных, что упрощает их поиск для тех, кто не пользуется интеллектуальными инструментами разработки.
- При использовании именованного экспорта программисты, применяющие интеллектуальные инструменты разработки, получают такие удобные возможности, как автоматический импорт.
- Именованный экспорт даёт возможность единообразно экспортировать из модулей всё, что угодно, в нужных количествах. Дефолтный экспорт ограничивает разработчика лишь экспортом одного значения. В качестве обходного пути тут можно применить экспорт объекта с несколькими свойствами. Однако при таком подходе теряется ценность алгоритма tree-shaking, применяемого для уменьшения размеров JS-приложений, собираемых чем-то вроде webpack. Использование исключительно модулей с именованным экспортом упрощает работу.
В целом же можно отметить, что именование сущностей — это хорошая практика, так как она позволяет однозначно идентифицировать их и в коде, и в разговорах об этом коде. Именно поэтому я и использую именованный экспорт.
Итоги
Только что вы узнали о возможностях ES6, которые, по мнению автора этого материала, являются неудачными. Возможно, вы присоединитесь к этому мнению, возможно — нет. Любой язык программирования — это сложная система, возможности которой можно рассматривать с разных точек зрения. Однако мы надеемся на то, что эта статья окажется полезной всем тем, кто стремится писать понятный и качественный код.
Уважаемые читатели! Есть ли в современном JavaScript что-то такое, чего вы стараетесь избегать?