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

Комментарии (19)


  1. CoolCmd
    20.05.2019 13:05

    Я обычно стремлюсь к тому, чтобы размер моих файлов не превышал бы 100 строк.

    речь в статье о достаточно серьезных проектах. с такими мелкими файлами их количество перевалит за 100, 200… автор удачно назвал это лесом. в нем и заблудится недолго.

    почему бы для навигации по содержимому файла не воспользоваться встроенными средствами редактора. например, у Visual Studio (взрослой) есть для этого три списка навигации.


  1. neurocore
    20.05.2019 13:52

    Используйте несколько параметров функций вместо одного объекта с параметрами


    Не согласен. Есть случаи когда параметров функции очень много, тогда лучше передавать объектом, но при этом написать комментарий перед функцией (например в формате JSDoc)


    1. JustDont
      20.05.2019 13:55

      Или уже наконец не писать в голом JS в 2019 году, а взять TS и получить статическую типизацию, которая и без окольных средств типа JSDoc/IDE autocomplete просто не даст дернуть того, что не попадает в тип.


    1. smind
      20.05.2019 14:06

      Так там, так и написано.


  1. dark_ruby
    20.05.2019 13:55

    Используйте строгую типизацию (TS/Flow).


    1. dpischalka
      20.05.2019 15:13

      О да, здесь я неистово плюсую. У хорошего программиста код будет хорош всегда, но типизация всё же сильно помогает. Тоже не разделяю мнение, что в JS типизация не нужна.


  1. Electrohedgehog
    20.05.2019 14:44
    +2

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

    Но особенно полезной она будет для тех, кто знаком с JavaScript хотя бы на среднем уровне.

    То есть предполагается, что владея языком на среднем уровне разработчик не был в курсе что такое модули и как называть переменные? А фраза «хотя бы» намекает что и некоторым гуру не помешало бы ознакомиться с этими секретами мастерства?


  1. miraage
    20.05.2019 15:50
    -1

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

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

    Как раз наоборот.
    Единственный выигрыш от нескольких переменных, вместо объекта, это экономия памяти на аллокации объекта.
    Разница, к слову, весьма ощутимая, когда речь заходит про high perf приложения. Это одна из причин, почему в ядре ReactJS можно редко увидеть функции с единственным аргументом-объектом.


    1. Vasily_T
      21.05.2019 13:50

      «экономия памяти на аллокации объекта» — это Вы тут о чем ??? Мы еще про js говорим?
      Вы точно написали то что написали?


    1. LMSn
      21.05.2019 17:17

      Как раз наоборот.
      Единственный выигрыш от нескольких переменных, вместо объекта, это экономия памяти на аллокации объекта.

      А вы вообще понимаете, что в статье говорится не о мифическом перфомансе и экономии на спичках, а об удобстве работы с кодом? Выигрыш тут в том, что видя названия аргументов (а в идеале, в TS/Flow, еще и их типы), мы сразу можем понять что в функцию нужно пихать. А не какой-нибудь мифический args-объект в котором непонятно что должно содержаться (и не дай бог этот args будет по цепочке передаваться туда-сюда в разные функции, тогда вообще ничего нельзя будет разобрать).


  1. SimpleAutomation
    20.05.2019 18:42

    Многие пункты более подробно рассматриваются в книге "Чистый код" Роберта Мартина. Но в статье интересно применение к es6. В целом полезно повторить.


  1. noodles
    20.05.2019 23:38

    При объявлении функции следует стремиться к использованию нескольких параметров, а не одного объекта с параметрами.

    А другой пропогандирует RORO, и как теперь жить?)


  1. pov
    21.05.2019 08:49

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


  1. Peter03
    21.05.2019 08:49

    Я бы добавил — там где возможно, используйте автогенерацию JS/TypeScript кода.

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


  1. ethancampbell
    21.05.2019 08:49

    Whenever i write my code, i goes well in the beginning but afterwards becomes a big mess.


  1. X-3mal
    21.05.2019 08:49

    П.3 не актуален при написании через TypeScript. После того как его попробовал — любой JS только через TS.
    К п. 12 добавил бы рекомендацию к любому console.log добавлять текстовый десрипшн console.log('this.data', this.data); Тогда не будет проблем с поиском именно этого раздражающего лога, который выводит непонятно что:)


    1. Aingis
      21.05.2019 12:54

      Интересно, почему все путают фичи IDE с фичами языка?


    1. AriesUa
      21.05.2019 13:41

      Поддержу. Не являюсь фаном Typescript. Но после того, как начал его использовать, действительно читабельность и понимание кода значительно возросла.


  1. Anubis
    21.05.2019 10:33

    Насчёт 3 пункта:

    function displayUser(user: IUser) {…
    or
    function displayUser({ firstName, lastName, age }: IUser) {…