JavaScript ES6: слабые стороны

в 10:22, , рубрики: javascript, Блог компании RUVDS.com, разработка, Разработка веб-сайтов

В июне 2018 года стандарт ECMAScript 2015 (ES6) отметил свой трёхлетний юбилей. В ES6, во-первых, появилось множество новых возможностей JavaScript, во-вторых, с этого стандарта начинается новая эра развития языка. Кроме того, это был последний масштабный релиз JS, так как теперь TC39 применяет схему выпуска небольших ежегодных выпусков стандарта, а не выводит его новую редакцию раз в несколько лет.

JavaScript ES6: слабые стороны - 1

Последние 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, и они намеренно создали более простой синтаксис для него. Однако на практике мне удалось выяснить, что пользоваться технологией именованного экспорта предпочтительнее по следующим причинам.

  1. При использовании именованного экспорта имена экспортируемых переменных, по умолчанию, соответствуют именам импортируемых переменных, что упрощает их поиск для тех, кто не пользуется интеллектуальными инструментами разработки.
  2. При использовании именованного экспорта программисты, применяющие интеллектуальные инструменты разработки, получают такие удобные возможности, как автоматический импорт.
  3. Именованный экспорт даёт возможность единообразно экспортировать из модулей всё, что угодно, в нужных количествах. Дефолтный экспорт ограничивает разработчика лишь экспортом одного значения. В качестве обходного пути тут можно применить экспорт объекта с несколькими свойствами. Однако при таком подходе теряется ценность алгоритма tree-shaking, применяемого для уменьшения размеров JS-приложений, собираемых чем-то вроде webpack. Использование исключительно модулей с именованным экспортом упрощает работу.

В целом же можно отметить, что именование сущностей — это хорошая практика, так как она позволяет однозначно идентифицировать их и в коде, и в разговорах об этом коде. Именно поэтому я и использую именованный экспорт.

Итоги

Только что вы узнали о возможностях ES6, которые, по мнению автора этого материала, являются неудачными. Возможно, вы присоединитесь к этому мнению, возможно — нет. Любой язык программирования — это сложная система, возможности которой можно рассматривать с разных точек зрения. Однако мы надеемся на то, что эта статья окажется полезной всем тем, кто стремится писать понятный и качественный код.

Уважаемые читатели! Есть ли в современном JavaScript что-то такое, чего вы стараетесь избегать?

Автор: ru_vds

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js