[Перевод] Человеко-читаемый JavaScript: история о двух экспертах
Каждый хочет быть экспертом. Но что это хотя бы означает? За годы работы мне встречалось два типа людей, именуемых «экспертами». Эксперт первого типа — это человек, который не только знает в языке каждый винтик, но и непременно все эти винтики использует, независимо от того, приносит ли это пользу. Эксперт второго типа также знает каждую синтаксическую тонкость, но разборчивее подходит к выбору инструмента для решения задачи, учитывая ряд факторов, как связанных, так и не связанных с кодом.
Давайте угадаю, эксперта какого типа вы хотели бы видеть в своей команде. Второго, верно? Это такой разработчик, который стремится выдавать удобочитаемый код, такие строки JavaScript, которые будут понятны другим специалистам, и которые легко будет поддерживать. Но характеристика «удобочитаемый» редко является определяющей — на самом деле, она обычно заключена в глазах смотрящего. Итак, к чему нас это приводит? К чему нужно стремиться, если наша цель — удобочитаемый код? Есть ли в данном случае явно верный или неверный выбор? Зависит от многого.
Очевидный выбор
Чтобы облегчить труд разработчика, TC39 в последние годы добавил множество новых возможностей в ECMAScript, в том числе, многие проверенные паттерны, заимствованные из других языков. Одним из таких нововведений, появившихся в ES2019, является метод Array.prototype.flat (). Он принимает аргумент глубины или Infinity и выравнивает массив. При отсутствии аргументов глубина массива по умолчанию равна 1.
Прежде, чем появилось это дополнение, требовался следующий синтаксис, чтобы выровнять массив до единственного уровня.
let arr = [1, 2, [3, 4]];
[].concat.apply([], arr);
// [1, 2, 3, 4]
Добавляя flat (), можно описать ту же возможность всего одной выразительной функцией.
arr.flat();
// [1, 2, 3, 4]
Вторая строка кода читается проще? Решительно да. На самом деле, с этим согласились бы оба эксперта.
Не каждый разработчик знает о существовании flat (). Но знать об этом заранее и не обязательно, так как flat () — понятный глагол, из которого ясно, что тут происходит. Он гораздо более интуитивен, чем concat.apply ().
Вот тот редкий случай, в котором можно уверенно ответить, какой вариант синтаксиса лучше — новый или старый. Оба эксперта, ознакомившись с двумя вариантами синтаксиса, выберут второй. Выберут ту строку кода, которая короче, четче и удобнее в поддержке.
Но выбор и компромисс не всегда так однозначны.
Проверка на вшивость
Чем чудесен JavaScript, так это своей невероятной многогранностью. Вот почему в Вебе он повсюду. Хорошо это с вашей точки зрения или плохо — уже другой вопрос.
Но такая многогранность тянет за собой парадокс выбора. Один и тот же код можно написать самыми разными способами. Как определить, какой из них «правильный»? К такому решению даже не подступиться, если не знать всех доступных вариантов и не понимать, в чем они не дотягивают.
Давайте попробуем функциональное программирование с map () в качестве примера. Я разберу здесь несколько итераций — все они приведут к одному и тому же результату.
Вот самая лаконичная версия из всех наших примеров с map (). В ней меньше всего символов, и все они умещаются в одну строку. От этой версии будем отталкиваться.
const arr = [1, 2, 3];
let multipliedByTwo = arr.map(el => el * 2);
// multipliedByTwo равно [2, 4, 6]
В следующем примере добавляется всего два символа: скобки. Мы что-нибудь потеряли? А приобрели? Делает ли погоду то, что в функции, имеющей более одного параметра, всегда потребуется использовать скобки? Я считаю — да, делает. Нет ничего дурного, если добавить их здесь, но единообразие кода значительно повысится, когда вам неизбежно придется написать функцию со множеством параметров. На самом деле, на момент написания этих строк, в Prettier данное ограничение оказалось обязательным; там мне не удалось создать стрелочную функцию без скобок.
let multipliedByTwo = arr.map((el) => el * 2);
Идем дальше. Мы добавили фигурные скобки и оператор возврата. Теперь это уже начинает походить на традиционное определение функции. Прямо сейчас может показаться, что ключевое слово, столь же длинное, как и вся логика функции — это стрельба из пушки по воробьям. Но, если в функции будет свыше одной строки, то этот дополнительный синтаксис обязательно понадобится. Мы предполагаем, что у нас не будет никаких других функций длиннее одной строки? Кажется сомнительным.
let multipliedByTwo = arr.map((el) => {
return el * 2;
});
Далее мы вообще убрали стрелочную функцию. Используем тот же синтаксис, что и раньше, но теперь предпочли ключевое слово function. Это интересно, поскольку нет такого сценария, в котором этот синтаксис бы не работал; при любом количестве параметров или строк у нас не возникнет проблем, поэтому здесь мы сильны единообразием. Этот код пространнее, чем наше первое определение, но так ли это плохо? Как это повредит новому программисту или человеку, поднаторевшему не в JavaScript, а в каком-то другом языке? Разве кого-нибудь, хорошо знающего JavaScript, смутит такой синтаксис при сравнении?
let multipliedByTwo = arr.map(function(el) {
return el * 2;
});
Наконец, подходим к последнему варианту: передавать только функцию. И timesTwo можно написать при помощи любого угодного нам синтаксиса. Опять же, нет такого сценария, при котором передача имени функции обернулась бы для нас проблемой. Но сделаем шаг назад и подумаем, а может ли такой код кого-нибудь запутать. Если вы только знакомитесь с данной базой кода, ясно ли вам, что timesTwo — это функция, а не объект? Определенно, map () здесь послужит вам подсказкой, но такую деталь вполне можно и упустить. Как насчет того места, в котором объявляется и инициализируется timesTwo? Легко ли ее найти? Понятно ли, что она делает, и как она влияет на результат? Все эти соображения важны.
const timesTwo = (el) => el * 2;
let multipliedByTwo = arr.map(timesTwo);
Как видите, очевидного ответа здесь нет. Но выбирать верные варианты при формировании вашей базы кода можно лишь при условии, что ты понимаешь все опции и сопряженные с ними ограничения. В частности, знаешь, что для единообразия кода тебе понадобятся и круглые скобки, и фигурные, и ключевые слова return.
Есть ряд вопросов, которыми нужно озаботиться, когда пишешь код. Вопросы производительности — как правило, самые распространенные. Но, когда сравниваешь функционально идентичные фрагменты кода, то выбор нужно делать с оглядкой на людей, которым этот код придется читать.
Может быть, новее — не всегда лучше
Итак, мы рассмотрели ярко выраженный пример, в котором оба эксперта предпочтут более новый синтаксис, даже если он не общеизвестен. Мы также разобрали пример, который ставит много вопросов, но дает не так много ответов. Теперь давайте погрузимся в код, который я написал ранее… и удалил. Этот код превратил меня в эксперта первого типа, когда я решил задачу при помощи малоизвестной синтаксической конструкции, тем самым пренебрегая моими коллегами и удобством поддержки нашей базы кода.
Деструктурирующее присваивание позволяет распаковать значения из объектов (или массивов). Обычно оно выглядит примерно так.
const {node} = exampleObject;
Здесь в одной строке инициализируется переменная, и ей присваивается значение. Но так может и не быть.
let node
;({node} = exampleObject)
В последней строке кода значению присваивается переменная при помощи деструктуризации, но объявление переменной происходит на одну строку выше. Такое предпринимается часто, но многие не осознают, что так можно.
Давайте присмотримся к этому коду. Здесь навязывается несуразная точка с запятой, и это в коде, где точка с запятой не применяется для завершения строк. Здесь команда заключается в круглые скобки, а также добавляются фигурные скобки; совершенно непонятно, что здесь происходит. Читать эту строку сложно, т я как эксперт совершенно не вправе был писать такой код.
let node
node = exampleObject.node
Этот код решает задачу. Он работает, понятно, что в нем делается, и мои коллеги поймут его, никуда не подсматривая. Что касается деструктурирующего синтаксиса, я не должен его применять только потому, что могу применить.
Код — это еще не все
Как мы убедились, решение эксперта-2 редко напрашивается, если исходить только из кода; тем не менее, можно легко различить, какой код должен писать каждый из экспертов. Дело в том, что читать код должны машины, а интерпретировать — люди. Поэтому нужно учитывать и такие факторы, которые связаны не только с кодом!
Работая в команде JavaScript-разработчиков, вы будете иначе подходить к выбору кода, чем при работе в многоязычной команде, члены которой не настолько погружаются в языковые тонкости.
Давайте для примера сравним оператор расширения и concat ().
Оператор расширения был добавлен в ECMAScript несколько лет назад и теперь очень распространился. Это своеобразный вспомогательный синтаксис, при помощи которого можно сделать множество вещей. В частности, сцепить некоторое число массивов.
const arr1 = [1, 2, 3];
const arr2 = [9, 11, 13];
const nums = [...arr1, ...arr2];
При всем потенциале оператора расширения, символ его не самоочевиден. Поэтому, если вы не знаете, что он делает, он не слишком вам поможет. Тогда как оба эксперта вполне могут рассчитывать, что команда JavaScript-профи знакома с таким синтаксисом, эксперт-2, пожалуй, задумается, а можно ли сказать то же о команде многоязычных программистов. Поэтому эксперт-2 может предпочесть метод concat (), поскольку это информативный глагол, который, вероятно, можно понять из контекста кода.
Этот фрагмент кода дает тот же числовой результат, что и вышеприведенный пример с оператором расширения.
const arr1 = [1, 2, 3];
const arr2 = [9, 11, 13];
const nums = arr1.concat(arr2);
Это всего один пример, демонстрирующий, как человеческий фактор влияет на выбор кода. База кода, к которой имеют доступ люди из разных команд, может регулироваться более строгими стандартами, которые не обязательно поспевают за всеми крутыми синтаксическими новинками. Тогда приходится отвлечься от основного исходного кода и учесть другие факторы, касающиеся вашего инструментария и способные усложнить или облегчить жизнь людям, работающим над этим кодом. Есть код, который можно структурировать так, что он будет плохо поддаваться тестированию. Есть код, который загонит вас в угол, и вы не сможете в дальнейшем масштабироваться или добавлять новые возможности. Есть код, в котором страдает производительность, поддерживаются не все браузеры или плохо с доступностью. Все эти факторы учитывает в своих рекомендациях эксперт-2.
Эксперт-2 также учитывает фактор именования. Но будем честны, даже эксперты в большинстве случаев с именованием не справляются.
Заключение
Настоящий эксперт — не тот, кто применяет каждый писк из спецификации, а тот, кто знает спецификацию достаточно хорошо, чтобы рационально развертывать синтаксис и принимать хорошо продуманные решения. Эксперт, дорастающий до такого уровня, может подготовить новых экспертов.
Что же это означает для тех из нас, кто себя считает экспертом или хотя бы стремится в эксперты? Это означает, что, когда пишешь код, нужно задавать себе множество вопросов. Адекватно оценивать тех разработчиков, которые являются твоей целевой аудиторией. Лучший код, который вы можете написать — это код, решающий некоторую сложную задачу, но по определению понятный тем, кто будет читать вашу базу кода.
Да, это очень непросто. И однозначного ответа часто нет. Но вы должны задумываться о вышеизложенном, когда пишете каждую вашу функцию.
Наши виртуалки можно использовать для экспертной разработки на Javascript.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!