Язык JavaScript родом из раннего веба. Сначала на нём писали простые скрипты, которые «оживляли» страницы сайтов. Теперь же JS превратился в полноценный язык программирования, который можно использовать даже для разработки серверных проектов.
Современные веб-приложения сильно зависят от JavaScript. Особенно это касается одностраничных приложений (Single-Page Application, SPA). С появлением библиотек и фреймворков, таких как React, Angular и Vue, JavaScript стал одним из основных строительных блоков веб-приложений.
Масштабирование подобных приложений, идёт ли речь об их клиентских или серверных частях, может оказаться весьма непростой задачей. Если в основе таких приложений лежит плохо продуманная архитектура, то их разработчики рано или поздно сталкиваются с определёнными ограничениями. Они тонут в море неприятных неожиданностей.
Автор статьи, перевод которой мы сегодня публикуем, хочет поделиться советами по написанию чистого JavaScript-кода. Он говорит, что статья рассчитана на JS-программистов с любым уровнем подготовки. Но особенно полезной она будет для тех, кто знаком с JavaScript хотя бы на среднем уровне.
1. Изоляция кода
Для того чтобы поддерживать кодовую базу проекта в чистоте, чтобы код было бы легко читать, рекомендуется выделять фрагменты кода в отдельные блоки, основываясь на их предназначении. В качестве таких блоков обычно выступают функции. Я считаю эту рекомендацию самой важной из тех, что могу дать. Если вы пишете функцию, то нужно сразу же ориентироваться на то, чтобы эта функция была бы нацелена на решение какой-то одной задачи. Функция не должна быть рассчитана на решение нескольких задач.
Кроме того, следует стремиться к тому, чтобы вызовы функций не приводили бы к побочным эффектам. В большинстве случаев это означает, что функция не должна менять что-то такое, что объявлено за её пределами. Данные в неё поступают посредством параметров. Ей не следует работать ни с чем другим. Возвращать что-либо из функций нужно с помощью ключевого слова return
.
2. Разбивка кода на модули
Функции, которые используются похожим образом или выполняют похожие действия, можно сгруппировать в одном модуле (или, если хотите, в отдельном классе). Предположим, в вашем проекте нужно выполнять различные вычисления. В такой ситуации разные этапы подобных вычислений имеет смысл выразить в виде отдельных функций (изолированных блоков), вызовы которых можно объединять в цепочки. Однако все эти функции могут быть объявлены в одном файле (то есть — в модуле). Вот пример модуля calculation.js
, который содержит подобные функции:
function add(a, b) {
return a + b
}
function subtract(a, b) {
return a - b
}
module.exports = {
add,
subtract
}
А вот как этим модулем можно воспользоваться в другом файле (назовём его index.js
):
const { add, subtract } = require('./calculations')
console.log(subtract(5, add(3, 2))
Разработчикам фронтенд-приложений можно дать следующие рекомендации. Для экспорта самых важных сущностей, объявленных в модуле, используйте возможности экспорта по умолчанию. Для второстепенных сущностей можно применить именованный экспорт.
3. Используйте несколько параметров функций вместо одного объекта с параметрами
При объявлении функции следует стремиться к использованию нескольких параметров, а не одного объекта с параметрами. Вот пара примеров:
// Хорошо
function displayUser(firstName, lastName, age) {
console.log(`This is ${firstName} ${lastName}. She is ${age} years old.`)
}
// Плохо
function displayUser(user) {
console.log(`This is ${user.firstName} ${user.lastName}. She is ${user.age} years old.`)
}
Наличие у функции нескольких параметров позволяет, взглянув на первую строчку её объявления, сразу же узнать о том, что ей нужно передать. Именно в этом и заключается причина, по которой я даю эту рекомендацию.
Несмотря на то, что при разработке функций нужно стремиться к тому, чтобы каждая из них решала лишь одну задачу, размер кода функций может быть достаточно большим. Если функция принимает единственный объект с параметрами, то для того чтобы узнать о том, что именно она ожидает, может понадобиться просмотреть весь её код, потратив на это немало времени. Иногда может показаться, что при работе с функциями гораздо проще использовать единственный объект с параметрами. Но если писать функции, учитывая возможное будущее масштабирование приложения, лучше использовать несколько параметров.
Надо отметить, что существует определённый предел, после которого использование отдельных параметров теряет смысл. В моём случае это четыре-пять параметров. Если функции нужно так много входных данных, то программисту стоит подумать об использовании объекта с параметрами.
Основная причина подобной рекомендации заключается в том, что отдельные параметры, ожидаемые функцией, должны передаваться ей в определённом порядке. Если некоторые из параметров являются необязательными, то вместо них необходимо передавать функции нечто вроде undefined
или null
. При использовании объекта с параметрами порядок параметров в объекте значения не имеет. При таком подходе можно обойтись и без установки необязательных параметров в undefined
.
4. Деструктурирование
Деструктурирование — полезный механизм, который появился в ES6. Он позволяет извлекать заданные поля из объектов и тут же записывать их в переменные. Им можно пользоваться при работе с объектами и модулями:
// Работа с модулем
const { add, subtract } = require('./calculations')
В частности, при работе с модулями имеет смысл импортировать в некий файл не весь модуль, а лишь необходимые функции, давая им понятные имена. В противном случае придётся обращаться к функциям с использованием переменной, символизирующей модуль.
Аналогичный подход применим и к тем случаям, когда в качестве параметра функции используется единственный объект. Это позволяет, взглянув на первую строку функции, тут же узнать о том, что именно она ожидает получить в виде объекта с параметрами:
function logCountry({name, code, language, currency, population, continent}) {
let msg = `The official language of ${name} `
if(code) msg += `(${code}) `
msg += `is ${language}. ${population} inhabitants pay in ${currency}.`
if(contintent) msg += ` The country is located in ${continent}`
}
logCountry({
name: 'Germany',
code: 'DE',
language 'german',
currency: 'Euro',
population: '82 Million',
})
logCountry({
name: 'China',
language 'mandarin',
currency: 'Renminbi',
population: '1.4 Billion',
continent: 'Asia',
})
Как видите, несмотря на то, что функция принимает единственный объект с параметрами, его деструктурирование позволяет узнать о том, что именно нужно поместить в него при вызове функции. Следующий совет будет посвящён тому, как ещё точнее сообщить пользователю функции о том, что именно она ожидает.
Кстати, деструктурирование можно использовать и при работе с функциональными компонентами React.
5. Задавайте стандартные значения параметров функций
Стандартные значения параметров функций, значения параметров по умолчанию, имеет смысл использовать и при деструктурировании объектов с параметрами, и в тех случаях, когда функции принимают списки параметров. Во-первых, это даёт программисту пример того, что можно передать функции. Во-вторых, это позволяет узнать о том, какие параметры являются обязательными, а какие — необязательными. Дополним объявление функции из предыдущего примера стандартными значениями параметров:
function logCountry({
name = 'United States',
code,
language = 'English',
currency = 'USD',
population = '327 Million',
continent,
}) {
let msg = `The official language of ${name} `
if(code) msg += `(${code}) `
msg += `is ${language}. ${population} inhabitants pay in ${currency}.`
if(contintent) msg += ` The country is located in ${continent}`
}
logCountry({
name: 'Germany',
code: 'DE',
language 'german',
currency: 'Euro',
population: '82 Million',
})
logCountry({
name: 'China',
language 'mandarin',
currency: 'Renminbi',
population: '1.4 Billion',
continent: 'Asia',
})
Очевидно то, что в некоторых случаях, если при вызове функции ей не передали некий важный параметр, нужно выдать ошибку, а не использовать стандартное значение этого параметра. Но часто, однако, описанный здесь приём оказывается очень кстати.
6. Не передавайте функциям ненужные данные
Предыдущая рекомендация ведёт нас к одному интересному выводу. Он заключается в том, что функциям не нужно передавать те данные, которые им не нужны. Если следовать этому правилу, то на разработку функций может понадобиться дополнительное время. Но в долгосрочной перспективе такой подход приведёт к формированию кодовой базы, отличающейся хорошей читабельностью. К тому же, невероятно полезно знать о том, какие именно данные используются в каждом конкретном месте программы.
7. Ограничение числа строк в файлах и максимального уровня вложенности кода
Мне доводилось видеть большие файлы с программным кодом. Очень большие. В некоторых было более 3000 строк. В таких файлах очень сложно ориентироваться.
В результате рекомендуется ограничивать размер файлов, измеряемый в строках кода. Я обычно стремлюсь к тому, чтобы размер моих файлов не превышал бы 100 строк. Иногда, когда сложно бывает разбить некую логику на небольшие фрагменты, размеры моих файлов достигают 200-300 строк. И очень редко их размер доходит до 400 строк. Файлы, размеры которых превышают этот предел, тяжело читать и поддерживать.
В ходе работы над своими проектами смело создавайте новые модули и папки. Структура проектов должна напоминать лес, состоящий из деревьев (групп модулей и файлов модулей) и ветвей (разделов модулей). Стремитесь к тому, чтобы ваши проекты не были бы похожи на горные массивы.
Если же говорить о внешнем виде самих файлов с кодом, то они должны быть похожи на местность с невысокими холмами. Речь идёт о том, что следует избегать больших уровней вложенности кода. Стоит стремиться к тому, чтобы вложенность кода не превышала бы четырёх уровней.
Возможно, соблюдать эти рекомендации поможет применение подходящих правил линтера ESLint.
8. Пользуйтесь инструментами для автоматического форматирования кода
При командной работе над JavaScript-проектами необходимо выработать чёткое руководство по стилю и форматированию кода. Автоматизировать форматирование кода можно с помощью ESLint. Этот линтер предлагает разработчику огромный набор правил, поддающихся настройке. Существует команда eslint --fix
, которая умеет исправлять некоторые ошибки.
Я, однако, рекомендую использовать для автоматизации форматирования кода не ESLint, а Prettier. При таком подходе разработчик может не заботиться о форматировании кода. Ему нужно лишь писать качественные программы. Весь код, автоматически отформатированный с применением единого набора правил, будет выглядеть единообразно.
9. Используйте хорошо продуманные имена переменных
Имя переменной, в идеале, должно отражать её содержимое. Вот несколько рекомендаций по подбору информативных имён переменных.
▍Функции
Обычно функции выполняют какие-то действия. Люди, когда говорят о действиях, используют глаголы. Например — convert (конвертировать) или display (показать). Имена функций рекомендуется формировать так, чтобы они начинались с глагола. Например — convertCurrency
или displayUser
.
▍Массивы
Массивы обычно содержат в себе наборы каких-то значений. В результате к имени переменной, хранящей массив, имеет смысл добавлять букву s
. Например:
const students = ['Eddie', 'Julia', 'Nathan', 'Theresa']
▍Логические значения
Имена логических переменных имеет смысл начинать с is
или has
. Это приближает их к конструкциям, которые имеются в обычном языке. Например, вот вопрос: «Is that person a teacher?». Ответом на него может служить «Yes» или «No». Аналогично можно поступать и подбирая имена для логических переменных:
const isTeacher = true // или false
▍Параметры функций, передаваемых стандартным методам массивов
Вот несколько стандартных методов массивов JavaScript: forEach
, map
, reduce
, filter
. Они позволяют выполнять с массивами некие действия. Им передают функции, которые описывают операции над массивами. Я видел, как многие программисты просто передают таким функциям параметры с именами наподобие el
или element
. Хотя такой подход избавляет программиста от размышлений об именовании подобных параметров, называть их лучше с учётом данных, которые в них оказываются. Например:
const cities = ['Berlin', 'San Francisco', 'Tel Aviv', 'Seoul']
cities.forEach(function(city) {
...
})
▍Идентификаторы
Часто бывает так, что программисту нужно работать с идентификаторами неких наборов данных или объектов. Если подобные идентификаторы являются вложенными, ничего особенного делать с ними не нужно. Я, например, при работе с MongoDB, обычно, перед возвратом объекта фронтенд-приложению, преобразую _id
в id
. При извлечении идентификаторов из объектов рекомендуется формировать их имена, ставя перед id
тип объекта. Например:
const studentId = student.id
// или
const { id: studentId } = student // деструктурирование с переименованием
Исключением из этого правила является работа со ссылками MongoDB в моделях. В подобных случаях поля рекомендуется называть в соответствии с моделями, ссылки на которые в них есть. Это, при заполнении документов, на которые есть ссылки в полях, позволит поддерживать чистоту и единообразие кода:
const StudentSchema = new Schema({
teacher: {
type: Schema.Types.ObjectId,
ref: 'Teacher',
required: true,
},
name: String,
...
})
10. Используйте там, где это возможно, конструкцию async/await
Использование коллбэков ухудшает читабельность кода. Особенно это касается вложенных коллбэков. Промисы немного выправили ситуацию, но я полагаю, что лучше всего читается код, в котором используется конструкция async/await. С таким кодом несложно разобраться даже новичкам и разработчикам, перешедшим на JavaScript с других языков. Самое главное здесь — освоить концепции, лежащие в основе async/await. Не стоит повсюду использовать эту конструкцию только из-за её новизны.
11. Порядок импорта модулей
В рекомендациях 1 и 2 была продемонстрирована важность правильного выбора места хранения кода для обеспечения его поддерживаемости. Аналогичные идеи применимы и к порядку импорта модулей. А именно, речь идёт о том, что логичный порядок импорта модулей делает код понятнее. Я, импортируя модули, придерживаюсь следующей простой схемы:
// Пакеты сторонних разработчиков
import React from 'react'
import styled from 'styled-components'
// Хранилища
import Store from '~/Store
// Компоненты, поддерживающие многократное использование
import Button from '~/components/Button'
// Вспомогательные функции
import { add, subtract } from '~/utils/calculate'
// Субмодули
import Intro from './Intro'
import Selector from './Selector'
Данный пример основан на React. Эту же идею несложно будет перенести и в любое другое окружение разработки.
12. Избегайте использования console.log
Команда console.log
представляет собой простой, быстрый и удобный инструмент для отладки программ. Существуют, конечно, и более продвинутые средства такого рода, но я думаю, что console.log
всё ещё пользуются практически все программисты. Если, используя console.log
для отладки, не убирать вовремя вызовы этой команды, ставшие ненужными, консоль скоро придёт в полный беспорядок. При этом надо отметить, что некоторые команды логирования имеет смысл оставлять даже в коде проектов, полностью готовых к работе. Например — команды, выводящие сообщения об ошибках и предупреждения.
В результате можно сказать, что для отладочных целей вполне можно пользоваться console.log
, а в тех случаях, когда команды логирования планируется использовать в работающих проектах, имеет смысл прибегнуть к специализированным библиотекам. Среди них — loglevel и winston. Кроме того, для борьбы с ненужными командами логирования можно воспользоваться ESLint. Это позволяет выполнять глобальный поиск и удаление подобных команд.
Итоги
Автор этого материала говорит, что всё то, о чём он тут рассказал, хорошо помогает ему в деле поддержания чистоты и масштабируемости кодовой базы его проектов. Надеемся, эти советы пригодятся и вам.
Уважаемые читатели! Что бы вы могли добавить к приведённым здесь 12 советам по написанию чистого и масштабируемого JS-кода?
Автор: ru_vds