Тернарный оператор в JavaScript: за, против, подводные камни

0
559
views

Перевод первой части статьи «Rethinking the JavaScript ternary operator». Автор статьи — Джеймс Синклер, веб-разработчик из Австралии. В настоящее время Джеймс — Senior Software Engineer в Atlassian.

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

С учетом вышесказанного становится ясно, почему многие люди относятся к тернарному оператору с некоторой опаской. Да, конечно, он более краткий, чем if-предложение. Но вместе с тем тернарные операторы легко могут превратить код в нечитаемую абракадабру. Так что самый мудрый совет здесь — подходить к выбору с осторожностью. Отдавайте предпочтение if-предложениям, а тернарные операторы используйте только тогда, когда они не ухудшают читаемость.

Но что, если мы, делая выбор, кое-что упускаем из виду? Не рискуем ли мы выплеснуть из метафорической ванны вместе с водой и младенца? Тернарные операторы и if-предложения похожи, но не эквивалентны. Между ними есть различия, на которые люди зачастую не обращают внимания. И эти различия сказываются на вашем коде.

Проблема использования тернарного оператора

Почему люди относятся к тернарному оператору с такой опаской? Он чем-то плох? Не может же быть такого, чтобы каждый отдельный программист проснулся однажды утром и подумал: «А возненавижу-ка я тернарный оператор!» Очевидно, что у людей есть какие-то причины для такой нелюбви. Давайте рассмотрим некоторые из этих причин.

Тернарный оператор — странный

Одна из причин, почему люди недолюбливают тернарный оператор, состоит в том, что он какой-то странный. То есть странный как оператор. В JavaScript есть множество бинарных операторов, то есть операторов, которые применяются к двум аргументам. Вы наверняка с ними знакомы. Это операторы вроде +, -, * и /. Еще есть булевы операторы вроде &&, || и ===. Существует как минимум 28 бинарных операторов (их количество зависит от того, о какой версии ECMAScript идет речь). Они нам хорошо знакомы и интуитивно понятны. Аргумент слева, символ оператора, аргумент справа. Все просто.

Есть еще унарные операторы, их меньше. Но они тоже нормальные. Вероятно, вам знаком оператор отрицания !. Возможно, вы также пользовались + и - в их унарной форме. Например, -1. Чаще всего они оперируют с выражением, стоящим справа от символа оператора. Но вообще с ними не много проблем.

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

(/* первое выражение*/) ? (/* второе выражение */) : (/* третье выражение */)

На практике мы пишем что-то вроде этого:

const protocol = (request.secure) ? 'http' : 'https';

Если первое выражение истинно, оператор возвращает значение второго выражения. В противном случае возвращается значение третьего. Для обособления этих выражений нам нужны два символа. Никакой другой оператор не составляется из нескольких символов.

Но это не единственная странность. Большинство бинарных операторов имеют дело с какими-то постоянными типами. Арифметические операторы работают с числами. Булевы операторы работают с булевыми выражениями. Побитовые операторы тоже работают с числами. Для всех них тип одинаковый с обеих сторон. Но у тернарного оператора и типы странные. Если применяется тернарный оператор, второе и третье выражение могут иметь любой тип. Но первое выражение интерпретатор всегда считает булевым. Это уникально. И странно для оператора.

Тернарный оператор нелегок для начинающих

Итак, тернарный оператор странный. Не удивительно, что люди критикуют его, утверждая, что он только путает новичков. Тут надо много всего держать в памяти. Если видите знак вопроса, надо поискать глазами двоеточие. И, в отличие от if-предложения, тернарный оператор трудно читать как псевдоанглийский. Допустим, у нас есть вот такое if-предложение:

if (someCondition) {
    takeAction();
} else {
    someOtherAction();
}

Нам не составит труда перевести его в нормальный текст. Если someCondition оценивается как true, вызови функцию takeAction без аргументов. В противном случае вызови функцию someOtherAction без аргументов.

Ничего сложного.

Но тернарный оператор состоит из непонятных символов. Он не читается, как простой английский. Его чтение требует больше усилий. А изучение программирования и без того сложная задача.

Сложность для чтения

Даже если вы не новичок, код с тернарными операторами все равно трудно читать. Эти загадочные символы могут сбить с толку даже самых лучших из нас. Особенно, если сами выражения, к которым применен тернарный оператор, длинные. Посмотрите на пример использования библиотеки Ratio:

const ten = Ratio.fromPair(10, 1);
const maxYVal = Ratio.fromNumber(Math.max(...yValues));
const minYVal = Ratio.fromNumber(Math.min(...yValues));
const yAxisRange = (!maxYVal.minus(minYVal).isZero()) ? ten.pow(maxYVal.minus(minYVal).floorLog10()) : ten.pow(maxYVal.plus(maxYVal.isZero() ? R

С первого взгляда трудно понять, что здесь происходит. Каждое выражение имеет как минимум две цепочки вызова методов. Не говоря уже о том, что в последнем выражении есть вложенный тернарный оператор. Это трудно читать. Никому не советую писать что-либо подобное.

Конечно, мы можем немного улучшить ситуацию, добавив разделение строк. Prettier сделает это так:

const ten = Ratio.fromPair(10, 1);
const maxYVal = Ratio.fromNumber(Math.max(...yValues));
const minYVal = Ratio.fromNumber(Math.min(...yValues));
const yAxisRange = !maxYVal.minus(minYVal).isZero()
    ? ten.pow(maxYVal.minus(minYVal).floorLog10())
    : ten.pow(maxYVal.plus(maxYVal.isZero() ? Ratio.one : maxYVal).floorLog10());

Немножко лучше. Но на общую ситуацию это не слишком повлияло. Мы можем внести еще одно маленькое улучшение, добавив вертикальное выравнивание.

const ten        = Ratio.fromPair(10, 1);
const maxYVal    = Ratio.fromNumber(Math.max(...yValues));
const minYVal    = Ratio.fromNumber(Math.min(...yValues));
const yAxisRange = !maxYVal.minus(minYVal).isZero()
                 ? ten.pow(maxYVal.minus(minYVal).floorLog10())
                 : ten.pow(maxYVal.plus(maxYVal.isZero() ? Ratio.one : maxYVal).floorLog10());

Но читать все равно тяжело. Вообще программисту очень просто перейти некую грань и поместить слишком много всего в тернарные выражения. А чем больше вы в них помещаете, тем сложнее их читать.

Отдельная проблема — вложенные тернарные операторы. При их чтении очень легко пропустить двоеточие. В примере, приведенном выше, немного помогает разделение строк. Но вы легко можете промахнуться и сделать вот так:

const ten        = Ratio.fromPair(10, 1);
const maxYVal    = Ratio.fromNumber(Math.max(...yValues));
const minYVal    = Ratio.fromNumber(Math.min(...yValues));
const yAxisRange = !maxYVal.minus(minYVal).isZero()
                 ? ten.pow(maxYVal.minus(minYVal).floorLog10()) : ten.pow(maxYVal.plus(maxYVal.isZero() ? Ratio.one
                 : maxYVal).floorLog10());

Разумеется, это надуманный пример. Можно сказать, подмена тезиса. Я намеренно написал плохой код, чтобы проиллюстрировать проблему. Но суть остается. Программист запросто может написать нечитаемые тернарные выражения. Особенно со вложенными тернарными операторами. А читаемость имеет значение. Как сказал Мартин Фаулер, «Любой дурак может написать код, понятный компьютеру. Хорошие программисты пишут код, понятный людям».

Мы пишем код, чтобы его читали. И это основная проблема использования тернарных операторов. В них запросто можно засунуть слишком много всего. А когда вы начинаете их вкладывать одни в другие, шансы написать непонятную абракадабру возрастают по экспоненте. Так что я вполне понимаю, почему джуниоров подталкивают к игнорированию тернарного оператора. Куда лучше придерживаться прекрасных, безопасных if-предложений.

Но насколько они безопасны?

Photo by Jack Hunter on Unsplash

Ненадежность if-предложений

Тернарные выражения имеют свои недостатки. И если бы все сводилось только к ним, я бы без вопросов просто отказался от тернарного оператора. Я ведь хочу, чтобы мой код было легко читать другим программистам, включая новичков. Но люди, выступающие резко против использования тернарных операторов, склонны делать два допущения:

  1. Единственная причина использовать тернарные операторы — стремление сделать код кратким или же покрасоваться.
  2. if-предложения прекрасно могут заменить тернарные выражения.

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

Для иллюстрации давайте рассмотрим пример. Вот два варианта кода.

Первый — с if-предложением:

let result;
if (someCondition) {
    result = calculationA();
} else {
    result = calculationB();
}

А второй — с тернарным оператором:

const result = (someCondition) ? calculationA() : calculationB();

Люди склонны считать, что эти два примера эквивалентны. И в каком-то смысле так и есть. В конечном итоге переменной result будет дано какое-то значение. Это будет результат либо calculationA(), либо calculationB().

Но если посмотреть с другой стороны, эти примеры отличаются. И первую подсказку нам дает let в if-предложении.

В чем разница? Если говорить коротко, if-предложение — это предложение, а тернарное выражение — это выражение.

И что это значит?

  • Выражение всегда вычисляется в какое-то значение.
  • Предложение — это просто «отдельный юнит выполнения».

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

Что такое побочный эффект? Это то, что ваш код делает помимо вычисления значения. Варианты того, что он может делать:

  • сетевые вызовы
  • чтение и запись в файлы
  • запросы к базам данных
  • изменение элементов DOM
  • мутация глобальных переменных
  • даже запись в консоль.

Все это — побочные эффекты.

Тут читатель может вопросить: «Ну и что? Кому какое дело? В конечном итоге, мы же и пишем код ради этих побочных эффектов, верно? Важно то, что задача выполнена!»

С одной стороны — да, значение имеет именно то, что задача решена. Важно то, что мы имеем рабочий код. Тут не поспоришь. Но откуда вы знаете, что он рабочий? И откуда вы знаете, что ваша программа делает только то, что, по вашему мнению, должна делать? Как вы можете быть уверены, что она, помимо основной работы, не майнит Dogecoin и не стирает таблицы базы данных?

Это, кстати, основная идея функционального программирования. Мы достигаем уверенности в своем коде путем пристального внимания к побочным эффектам. Там, где это в принципе возможно, мы предпочитаем работать с чистыми функциями. Если функция чиста, мы знаем, что она не делает ничего, кроме вычисления и возврата значения.

Что это означает в контексте if-предложений и тернарных выражений? Это означает, что мы должны относиться к if-предложениям с известной опаской. Давайте еще раз рассмотрим пример, который уже приводили.

if (someCondition) {
    takeAction();
} else {
    someOtherAction();
}

Не важно, какая ветка someCondition сработает. Единственное, что может if-предложение, это послужить причиной побочного эффекта. Оно вызывает либо takeAction(), либо someOtherAction(). Но ни один из вариантов не возвращает значение. (Или, если все-таки возвращают, то это значение ничему не назначается). Для этих функций единственный способ сделать что-то полезное — выйти за пределы блока. Они могут сделать что-то хорошее, вроде мутации переменной во внешнюю зону видимости. Но это все равно побочный эффект.

Можно ли сказать, что я советую никогда не пользоваться if-предложениями? Вовсе нет. Но нужно понимать, что они из себя представляют. Каждый раз, когда видите такое предложение, вы должны спрашивать себя, какой побочный эффект здесь есть. И если вы не можете ответить на этот вопрос, вы не понимаете этот код.

Рассматриваем тернарные выражения еще раз

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

Мы любим выражения за то, что они лучше компонуются, чем предложения. Операторы и функции позволяют нам строить из простых выражений более сложные. Например, пользуясь оператором конкатенации, мы можем создавать сложные строки:

'<h1>' + page.title + '</h1>’;

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

Вы можете спросить, что в этом особенного. Ведь if-предложения тоже компонуются! Мы прекрасно можем вставить внутрь if-предложения цикл for. А в цикл for без всяких проблем можем вставить предложение case-switch. Используя предложения, мы можем создавать другие, более сложные предложения. Что же такого особенного в выражениях?

Преимущество выражений в том, что мы называем ссылочной прозрачностью. Это свойство означает, что мы можем взять значение выражения и использовать его в любом месте, где мы могли бы использовать само это выражение. И мы с полной уверенностью можем сказать, что результат будет таким же. Абсолютно. Всегда. 100%. Каждый раз.

Возможно, вы задаетесь вопросом, как это связано с компоновкой. Ссылочная прозрачность поясняет, чем компоновка предложений отличается от компоновки выражений. Я не придумал лучшей аналогии, чем кубики LEGO и пакеты в супермаркете.

Пакеты и кубики

Предложения компонуются так, как компонуются пакеты. Я прекрасно могу помещать одни пакеты в другие. Причем эти «другие» пакеты могут содержать и какие-то другие вещи. Я даже могу бережно завернуть каждый отдельный предмет в отдельный пакетик, а затем положить их стопочкой в другой пакет. Результат даже может быть эстетично привлекательным. Но все эти пакеты не имеют никаких реальных связей друг с другом. Их объединяет только вложенность. В их объединении нет никакого организационного принципа.

Аналогичным образом можно создавать вложенные предложения. То есть предложения с блоками, например, if-предложения с циклами for. Но они никак не будут связаны между собой. Эти блоки — просто контейнеры для чего угодно. В этом, конечно, ничего плохого нет. Но это другая компоновка, не такая, как у выражений.

Выражения скорее напоминают кубики LEGO. Способы их составления ограничены. Выступы сверху в одних кубиках соединяются с выемками снизу других. Будучи соединенными, кубики формируют новую фигуру. И эту фигуру можно заменить любой другой фигурой, имеющей ту же конфигурацию. Посмотрите на рисунок внизу. У нас есть две скомпонованные фигуры. И хотя они составлены из разных блоков, в результате получились фигуры одинаковой формы. Они взаимозаменяемы. Аналогично взаимозаменяемы выражение и его вычисленное значение. Не важно, как мы его вычислили, важен результат.

Да, эта аналогия не совершенна. Назначение пакетов в супермаркете и кубиков LEGO абсолютно разное. Но это лишь аналогия, она передает идею. Компоновка выражений имеет определенные преимущества. Мы не имеем этих преимуществ, когда компонуем if-предложения. А поскольку тернарный оператор это выражение, он имеет преимущество перед if-предложениями.

Означает ли это, что всегда следует отдавать предпочтение тернарным операторам? Они определенно лучше? К сожалению, ответ — нет. В JavaScript, как и в большинстве других языков, вы пользуетесь полной свободой вызывать побочные эффекты. Даже внутри выражений. Но цена этой свободы — необходимость постоянной бдительности. Вы никогда не знаете, где может проявиться неожиданный побочный эффект. Например:

const result = (someCondition) ? dropDBTables() : mineDogecoin();

Но мы не можем и полностью отказаться от тернарных операторов. Потому что if-предложения — то НЕ «то же самое, только более многословно». И когда вы видите в коде тернарный оператор, задумайтесь: возможно, у автора были причины сделать именно такой выбор. Возможно, у него были и другие резоны помимо краткости.

Конец первой части. Во второй части автор расскажет, как сделать if-предложения более безопасными, а тернарные выражения — более читаемыми.

frontend logo

Хочешь проверить свои знания по фронтенду?

Подпишись на наш канал с тестами по HTML/CSS/JS в Telegram!

×

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here