Пишем чистый и масштабируемый JavaScript-код: 12 советов +21


Язык 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-кода?




К сожалению, не доступен сервер mySQL