Практические советы по улучшению кода на JavaScript

Spread the love
  • 5
    Shares

В этой статье я приведу несколько практических советов из своего опыта как улучшить свой код на JavaScript.

Используйте TypeScript

Первое, что вы можете сделать, чтобы улучшить свой код на JS, это не писать на JS. Используйте TypeScript. Для непосвященных TypeScript (TS) – это отдельный язык программирования расширяющий стандартный JS (все, что выполняется в JS, запускается в TS). Долгое время поддержка TS во всей экосистеме была достаточно непоследовательной, ее было сложно использовать и мне было неудобно рекомендовать ее. К счастью, эти дни давно позади, и большинство фреймворков поддерживают TS из коробки. Давайте поговорим о том, почему вам нужно использовать его.

TypeScript обеспечивает «безопасность типов».

Безопасность типов это такой процесс, в котором компилятор проверяет, что все типы используемые «легальным» способом в пределах компилируемого фрагмента кода. Другими словами, если вы создаете функцию foo, которая принимает число:

function foo(someNum: number): number {
  return someNum + 5;
}

Эта функция foo должна вызываться только с числом:

хорошо

console.log(foo(2)); // prints "7"

плохо

console.log(foo("two")); // invalid TS code

Помимо накладных расходов на добавление типов в ваш код, нет никаких минусов в обеспечении безопасности типов. С другой стороны, выгода слишком велика, чтобы ее игнорировать. Безопасность типов обеспечивает дополнительный уровень защиты от распространенных ошибок / багов, что является благословением для таких языков, как JS.

Типы Typescript, облегчают рефакторинг крупных приложений.

Рефакторинг большого JS-приложения может стать настоящим кошмаром. Основная трудность рефакторинга JS связана с тем, что обычный рефакторинг не поддерживает контроль над сигнатурами функций. Например, если у меня может есть функция myAPI, которая используется в 1000 различных мест:

function myAPI(someNum, someString) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}

и вдруг мне нужно немного поменять сигнатуру вызова:

function myAPI(someString, someNum) {
  if (someNum > 0) {
    leakCredentials();
  } else {
    console.log(someString);
  }
}

Я должен быть на 100% уверен, что в каждом месте, где используется эта функция (1000 мест), я корректно обновляю использование. Если я пропущу даже одно место, мои учетные данные используемые внутри функции могут пропасть. Вот тот же сценарий с TS:

до изменения

function myAPITS(someNum: number, someString: string) { ... }

после

function myAPITS(someString: string, someNum: number) { ... }

Как видите, функция myAPITS претерпела те же изменения, что и аналог в JavaScript. В JavaScript этот код работает без ошибки в TypeScript у вас сразу будет ошибка из-за неправильного типа. И из-за «безопасности типов», которую мы обсуждали ранее, эти 1000 случаев будут блокировать компиляцию, и ваши учетные данные не будут потеряны (а это всегда хорошо).

TypeScript облегчает общение в командной архитектуре.

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

interface BasicRequest {
  body: Buffer;
  headers: { [header: string]: string | string[] | undefined; };
  secret: Shhh;
}

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

В целом, TS превратилась в зрелую и более предсказуемую альтернативу чистой JS. Я все еще использую в некоторых проектах ванильный JS, но в большинстве новых проектов, которые я начинаю сегодня, я подключаю TS с самого начала.

Используйте современные функции

JavaScript – один из самых популярных (если не самый) языков программирования в мире. Вы можете ожидать, что 20-летний язык, которым пользуются сотни миллионов людей, к настоящему времени будет «всем понятным и известным», но на самом деле все наоборот. В последнее время в JS было сделано много изменений и дополнений. Как человек, который начал писать JS только в последние 2 года, у меня было преимущество, что я пришел без предвзятости или ожиданий. Это привело к гораздо более прагматичному, нерелигиозному выбору того, какие особенности языка использовать, а какие избегать.

async и await

Долгое время асинхронные, управляемые событиями обратные вызовы (callback) были неизбежной частью разработки JS:

традиционный callback

makeHttpRequest('google.com', function (err, result) {
  if (err) {
    console.log('Oh boy, an error');
  } else {
    console.log(result);
  }
});

Я не собираюсь тратить время на объяснение того, почему вышеизложенное является проблематичным (я описывал это раньше). Чтобы решить проблему с обратными вызовами, в JS была добавлена новая концепция «Promises (Обещания)». Promises позволяют вам писать асинхронную логику, избегая проблем с вложениями, которые ранее преследовали код на основе обратного вызова (callback).

Promises

makeHttpRequest('google.com').then(function (result) {
  console.log(result);
}).catch(function (err) {
  console.log('Oh boy, an error');
});

Самым большим преимуществом Promises по сравнению с обратными вызовами является удобочитаемость и возможность объединения в цепочки через оператор then.

Несмотря на то, что Promises великолепны, они все же оставляют желать лучшего. В конце концов, написание Promises все еще не казалось “родным” для JS. Чтобы исправить это, комитет ECMAScript решил добавить новый метод использования promises, async и await:

async и await

try {
  const result = await makeHttpRequest('google.com');
  console.log(result);
} catch (err) {
  console.log('Oh boy, an error');
}

Единственное предупреждение: все что вы используете с await, должно быть объявлено с async:

функция makeHttpRequest из предыдущего примера

async function makeHttpRequest(url) {
  // ...
}

Также возможно использовать await с Promise напрямую, так как асинхронная функция – это просто необычная оболочка для Promise. Это также означает, что код async / await и код Promise функционально эквивалентны. Так что не стесняйтесь использовать async / await как можно чаще.

let и const

На протяжении большей части существования JS существовал только один способ объявления переменных var. У var есть довольно уникальные / интересные правила относительно того, как он обрабатывает область видимости. Поведение в области видимости var противоречиво и сбивает с толку, что приводит к неожиданному поведению и, следовательно, к ошибкам в течение всего жизненного цикла JS. Но в ES6 есть альтернатива var, const и let. На данный момент не нужно больше использовать var. Любая логика, которая использует var, всегда может быть преобразована в эквивалентный const и let.

Что касается использования const против let, я всегда начинаю с объявления всех переменных с const. const намного более ограничен и «неизменяем», что обычно приводит к лучшему коду. Не существует так уж много «реальных сценариев», где необходимо использовать только let, я бы сказал, что 1/20 переменных я объявляю с помощью let остальные с const.

Я сказал, что const “неизменяем”, но он не работает так же, как const в C / C ++. Что означает const в среде выполнения JavaScript, это ссылка на переменную но это не означает, что содержимое, сохраненное по этой ссылке, никогда не изменится. Для примитивных типов (number, boolean и т. д.) const неизменяемый (потому что это всего один адрес памяти). Но для всех объектов (классов, массивов, dicts) const не гарантирует неизменность.

Стрелочные функции Arrow => Functions

Функции стрелок являются кратким методом объявления анонимных функций в JS. Анонимные функции, описывают функции, которые не имеют явного имени. Обычно анонимные функции передаются как обратный вызов или перехват событий (event hook).

обычная анонимная функция

someMethod(1, function () { // has no name
  console.log('called');
});

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

стрелочная анонимная функция

someMethod(1, () => { // has no name
  console.log('called');
});

Помимо того, что функции стрелок намного более кратки, они также имеют гораздо более практичное поведение при определении области видимости scope. Функция стрелки наследует this от области, в которой они были определены.

В некоторых случаях функции стрелок могут быть еще более краткими:

const added = [0, 1, 2, 3, 4].map((item) => item + 1);
console.log(added) // prints "[1, 2, 3, 4, 5]"

Стрелочные функции, которые находятся в одной строке, включают в себя неявный оператор return. Нет необходимости в скобках или точках с запятой с однострочными стрелочными функциями.

Оператор Spread (распространения) ...

Извлечение пар ключ / значение одного объекта и добавление их в качестве потомков другого объекта – очень распространенный сценарий. Исторически, было несколько способов сделать это, но все эти методы довольно неуклюжи:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
const merged = Object.assign({}, obj1, obj2);
console.log(merged) // prints { dog: 'woof', cat: 'meow' }

Этот шаблон невероятно распространен, поэтому описанный выше подход быстро становится громоздким. Благодаря «оператору Spread» его больше никогда не нужно использовать:

const obj1 = { dog: 'woof' };
const obj2 = { cat: 'meow' };
console.log({ ...obj1, ...obj2 }); // prints { dog: 'woof', cat: 'meow' }

Самое замечательное, что это также без проблем работает с массивами:

const arr1 = [1, 2];
const arr2 = [3, 4];
console.log([ ...arr1, ...arr2 ]); // prints [1, 2, 3, 4]

Вероятно, это не самая важная из недавних функций JS, но она одна из моих любимых.

Шаблонные строки / Шаблонные литералы

Строки являются одной из наиболее распространенных программных конструкций. Вот почему так плохо, что объявление строк все еще плохо поддерживаются во многих языках. Долгое время JS был в семье подобных языков. Но добавление шаблонных литералов ставит JS в отдельную категорию. Шаблонные литералы изначально и удобно решают две самые большие проблемы с написанием строк, добавлением динамического содержимого и написанием строк, которые соединяют несколько строк:

const name = 'Ryland';
const helloString =
`Hello
 ${name}`;

Я думаю, что код говорит сам за себя. Какая удивительная реализация.

Деструктуризация объекта

Деструктуризация объекта – это способ извлечения значений из коллекции данных (объект, массив и т. д.) без необходимости перебирать данные или явно обращаться к их ключам:

старый путь

function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict.dog, myDict.cat);

деструктуризация

function animalParty(dogSound, catSound) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

const { dog, cat } = myDict;
animalParty(dog, cat);

Но подождите, это еще не все. Вы также можете определить деструктурирование в сигнатуре функции:

деструктуризация 2

function animalParty({ dog, cat }) {}

const myDict = {
  dog: 'woof',
  cat: 'meow',
};

animalParty(myDict);

Это также работает с массивами:

деструктуризация 3

[a, b] = [10, 20];

console.log(a); // prints 10

Есть множество других современных функций, которые вы должны использовать. Вот несколько других, которые выделяются для меня:

Всегда предполагайте, что ваша программа будет работать в асинхронной среде

При написании параллельных приложений ваша цель – оптимизировать объем работы, которую вы можете выполнить за один раз. Если у вас есть 4 доступных ядра и ваш код может использовать только одно ядро, 75% вашего потенциала тратится впустую. Это означает, что блокирование синхронных операций – главный враг параллельных вычислений. Но, учитывая, что JS является однопоточным языком, он не работает на нескольких ядрах. Так какой в этом смысл?

JS является однопоточным, но не однофайловым. Даже если он не многопоточный, он все же параллельный (concurrent). Отправка HTTP-запроса может занять несколько секунд или даже минут. Если JS прекратит выполнение кода до тех пор, пока ответ не вернется из запроса, код станет непригодным для использования.

JavaScript решает это с помощью цикла событий. Цикл обработки событий проходит по зарегистрированным событиям и выполняет их на основе внутренней логики планирования / расстановки приоритетов. Это то, что позволяет отправлять тысячи «одновременных» HTTP-запросов или считывать несколько файлов с диска в одно и то же время. Здесь есть одна загвоздка: JavaScript может использовать эту возможность, только если вы используете правильные функции. Самый простой пример – цикл for:

let sum = 0;
const myArray = [1, 2, 3, 4, 5, ... 99, 100];
for (let i = 0; i < myArray.length; i += 1) {
  sum += myArray[i];
}

Ванильный цикл – одна из наименее параллельных конструкций, которая существует в программировании. На моей последней работе я возглавлял команду, которая месяцами пыталась преобразовать традиционные циклы языка R for в автоматически параллельный код. Это в принципе невозможная проблема, которую можно решить только в ожидании улучшения глубокого обучения. Трудность распараллеливания цикла for возникает из-за нескольких проблемных паттернов. Последовательные циклы for очень редки, но только они делают невозможным гарантировать разделимость циклов for:

let runningTotal = 0;
for (let i = 0; i < myArray.length; i += 1) {
  if (i === 50 && runningTotal > 50) {
    runningTotal = 0;
  }
  runningTotal += Math.random() + runningTotal;
}

Этот код выдает ожидаемый результат, только если он выполняется по порядку, итерация за итерацией. Если вы попытаетесь выполнить несколько итераций одновременно, процессор может неправильно выполнить ветвление на основе неточных значений, что приведет к аннулированию результата. У нас был бы другой разговор, если бы это был код на C, так как его использование отличается и есть довольно много хитростей, которые компилятор может сделать с циклами. В JavaScript традиционный цикл for следует использовать только в случае крайней необходимости. В противном случае используйте следующие конструкции:

map

// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url) => makHttpRequest(url));
const results = await Promise.all(resultingPromises);

map с index

// in decreasing relevancy :0
const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
const resultingPromises = urls.map((url, index) => makHttpRequest(url, index));
const results = await Promise.all(resultingPromises);

for-each

const urls = ['google.com', 'yahoo.com', 'aol.com', 'netscape.com'];
// note this is non blocking
urls.forEach(async (url) => {
  try {
    await makHttpRequest(url);
  } catch (err) {
    console.log(`${err} bad practice`);
  }
});

Я объясню, почему это улучшение по сравнению с традиционным циклом for. Вместо выполнения каждой «итерации» по порядку (последовательно), такие конструкции, как map, принимают все элементы и отправляют их в виде отдельных событий в пользовательскую функцию map. Это напрямую сообщает среде выполнения, что отдельные «итерации» не имеют никакой связи или зависимости друг от друга, что позволяет им работать одновременно. Есть много случаев, когда цикл for будет таким же быстродействующим по сравнению с map или forEach. Но я все еще утверждаю, что потеря нескольких миллисекунд сейчас стоит того, чтобы использовать четко определенное API. Таким образом, любые будущие улучшения в реализации этих шаблонов доступа к данным пойдут на пользу вашему коду. Цикл for слишком универсален, чтобы иметь смысл оптимизировать для того же шаблона.

Существуют и другие допустимые варианты асинхронизации вне map и forEach, например for-await-of.

Используйте Lint и применяйте стили кодирования

Код без единого стиля невероятно сложен для чтения и понимания. Поэтому критически важным аспектом написания кода высокого уровня на любом языке является последовательный и разумный стиль. Из-за широты экосистемы JS существует множество опций для линтеров и особенностей стиля. Многие люди спрашивают, должны ли они использовать eslint или prettier. Для меня они служат разным целям, и поэтому должны использоваться вместе. Eslint – это традиционный «линтер», в большинстве случаев он будет выявлять проблемы с вашим кодом, которые имеют меньшее отношение к стилю и больше связаны с корректностью. Например, я использую eslint с правилами AirBNB. При такой конфигурации следующий код приведет к сбою линтера:

var fooVar = 3; // airbnb rules forebid "var"

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

Prettier – это средство форматирования кода. Оно меньше заботится о «правильности» и гораздо больше беспокоится о единообразии и последовательности. Prettier не собирается жаловаться на использование var, но он автоматически выровняет все скобки в вашем коде. В моем личном процессе разработки я всегда выполнял Prettier как последний шаг перед отправкой кода в Git. Во многих случаях даже имеет смысл запускать Prettier автоматически при каждом коммите в репозитарий. Это гарантирует, что весь код, поступающий в систему контроля версий, будет иметь согласованный стиль и структуру.

Тестируйте свой код

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

Test Driver – Ava

AvaJS on Github

Тестовые драйверы – это просто фреймворки, которые дают структуру и утилиты на очень высоком уровне. Они часто используются вместе с другими специальными инструментами тестирования, которые зависят от ваших потребностей в тестировании.

Ava – это правильный баланс выразительности и лаконичности. Параллельная и разрозненная архитектура Ava – причина моей любви к ней. Тесты, которые работают быстрее, экономят время разработчиков и деньги компаний. Ava может похвастаться множеством приятных функций, таких как встроенные утверждения, хотя при этом и остается минимальным.

Альтернативы: Jest, Mocha, Jasmine

Spies (шпионы) и Stubs (заглушки) – Sinon

Sinon on Github

Spies дают нам «функции аналитики», например, сколько раз была вызвана функция, как они были вызваны, и другие проницательные данные.

Sinon – это библиотека, которая делает много вещей, но только несколько из них очень полезны. В частности, sinon выделяется, когда дело доходит до spies и stubs. С большим набором функций и лаконичным синтаксисом. Это особенно важно для stubs, поскольку они частично существуют для экономии места.

Альтернативы: testdouble

Mocks – Nock

Nock on Github

HTTP-mocking – это процесс подделки некоторой части процесса http-запроса, что бы тестировщик мог внедрить пользовательскую логику для имитации поведения сервера.

Http mocking может быть настоящей болью, nock делает его менее болезненным. Nock напрямую переопределяет встроенный запрос nodejs и перехватывает исходящие http-запросы. Это, в свою очередь, дает вам полный контроль над ответом.

Альтернативы: я не знаю ни одного 🙁

Web Automation – Selenium

Selenium on Github

Selenium – это то, к чему я испытываю смешанные чувства по поводу рекомендаций. Поскольку это самый популярный вариант для веб-автоматизации, он имеет огромное сообщество и набор онлайн-ресурсов. К сожалению, кривая обучения довольно крутая, и для реального использования она зависит от множества внешних библиотек. Тем не менее, это единственный реальный бесплатный вариант, поэтому, если вы не занимаетесь веб-автоматизацией корпоративного уровня, Selenium будет вам очень полезен.

Оригинальная статья: Ryland G Practical Ways to Write Better JavaScript


Spread the love
  • 5
    Shares

Добавить комментарий

Ваш e-mail не будет опубликован.