Работа с коллекциями в JavaScript становится ужасающей, когда многое происходит в функциональном блоке.

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

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

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

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

Здесь мы рассмотрим 5 антипаттернов, которые следует избегать, работая с коллекциями в JavaScript.

Многие примеры кода представляют собой программную парадигму, известную как функциональное программирование. Как охарактеризовал ее Эрик Эллиот (Erich Elliot), автор книг по программированию: ”Это процесс построения софта, основанный на комбинировании чистых функций с избеганием разделяемого состояния, изменяемых данных и побочных эффектов”. В этом материале мы часто будем упоминать именно побочные эффекты и изменяемые состояния.

1. Преждевременная передача функций в качестве прямых аргументов

Первый антипаттерн — преждевременная передача функций в качестве прямых аргументов в методы массивов, совершающих циклы по коллекциям.

Вот вам простой пример:

function add(nums, callback) {
  const result = nums[0] + nums[1]
  console.log(result)
  if (callback) {
    callback(result)
  }
}

const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]

numbers.forEach(add)

Так почему же это антипаттерн?

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

Вместо необходимости проделывать следующее:

const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]

numbers.forEach(function(nums, callback) {
  const result = nums[0] + nums[1]
  console.log(result)
  
  if (callback) {
    callback(result)
  }
})

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

const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]numbers.forEach(add)

В идеальном мире работать со всеми функциями JavaScript без необходимости прикладывать усилия было бы идеальным решением.

Но на деле выходит, что преждевременная передача обработчиков таким образом может привести к ошибкам. Давайте взглянем еще раз на наш предыдущий пример:

function add(nums, callback) {
  const result = nums[0] + nums[1]
  console.log(result)
  
  if (callback) {
    callback(result)
  }
}

const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]

numbers.forEach(add)

Функция add ожидает массив, в котором первый и второй индексы являются числами, добавляет их и проверяет наличие обратного вызова, задействуя его при обнаружении. Проблема здесь в том, что callback может в итоге быть вызван в качестве number, что в результате закончится ошибкой:

2. Полагаться на упорядочивание функциями-итераторами вроде Map и Filter

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

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

Мне доводилось видеть код, который делал нечто похожее на:

let count = 0

frogs.forEach((frog) => {
  if (count === frogs.length - 1) {
    window.alert(
      `You have reached the last frog. There a total of ${count} frogs`,
    )
  }
  
  count++
})

Для многих случаев такой вариант подходит замечательно. Однако, если мы взглянем получше, то окажется, что это не самый безопасный подход, т.к. все что угодно в глобальной области видимости может обновить count. Если это произойдет и где-нибудь в коде count окажется случайно уменьшен на единицу, то window.alert никогда не сможет запуститься.

Такие ситуации могут приводить и к более печальным последствиям в асинхронных операциях:

function someAsyncFunc(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, timeout)
  })
}

const promises = [someAsyncFunc, someAsyncFunc, someAsyncFunc, someAsyncFunc]

let count = 0

promises.forEach((promise) => {
  count++
  
  promise(count).then(() => {
    console.log(count)
  })
})

Результат:

Те из вас, кто имеет больше опыта в работе с JavaScript, вероятно поймут, почему в логе мы получили четыре числа 4 вместо 1, 2, 3, 4. Суть в том, что во избежание конкурентности лучше использовать второй аргумент (обращение к которому обычно происходит, как к текущему index), который большинство функций получает при итерации коллекций.

promises.forEach((promise, index) => {
  promise(index).then(() => {
    console.log(index)
  })
})

Результат:

3. Преждевременная оптимизация

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

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

Как об этом выражается Дональд Кнут: “Настоящая проблема в том, что программисты тратят слишком много времени, переживая об эффективности не в тех местах и не в то время; поспешная оптимизация является корнем всех зол (по меньшей мере большинства из них) в программировании.”

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

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

4. Полагаться на State

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

Вот пример антипаттерна при работе с состояниями в коллекциях:

let toadsCount = 0

frogs.forEach((frog) => {
  if (frog.skin === 'dry') {
    toadsCount++
  }
})

Это пример побочного эффекта, за появлением которого следует тщательно следить, т.к. он может вызвать следующие проблемы:

  • Вызвать другие неожиданные побочные эффекты (очень опасно);
  • Повысить использование памяти;
  • Снизить качество работы приложения;
  • Усложнить читаемость и понимание кода;
  • Усложнить тестирование кода.

Так как же лучше всего написать указанный код для избежания появления побочного эффекта? 

Когда вы работаете с коллекциями и при этом сталкиваетесь с состоянием, помните, что можно использовать определенные методы, которые обеспечат новую ссылку на что-либо (наподобие объектов). 

Вот пример использования метода reduce:

const toadsCount = frogs.reduce((accumulator, frog) => {
  if (newFrog.skin === 'dry') {
    accumulator++
  }
  return accumulator
}, 0)

Здесь мы взаимодействуем с неким состоянием внутри его блока, но мы также применяем второй аргумент к .reduce, где значение может быть создано заново поверх инициализации. Такой подход окажется лучше предыдущего, т.к. мы ничего не изменяем вне области видимости.

5. Изменение аргументов

Это важный принцип в JavaScript, которому следует уделить внимание, особенно в контексте функционального программирования. Существует два понятия в отношении аргументов — изменяемый и неизменяемый. Вот пример:

const frogs = [
  { name: 'tony', isToad: false },
  { name: 'bobby', isToad: true },
  { name: 'lisa', isToad: false },
  { name: 'sally', isToad: true },
]

const toToads = frogs.map((frog) => {
  if (!frog.isToad) {
    frog.isToad = true
  }
  
  return frog
})

Мы ожидаем, что значение toToads вернет новый массив frogs, которые все были преобразованы в жаб (toads) посредством переключения их свойства isToad в true.

Но здесь есть важный нюанс. Когда мы изменили некоторые из объектов frog, указав frog.isToad = true, мы также ненамеренно изменили их и внутри массива frogs.

Теперь мы видим, что все frogs стали жабами, т.к. произошло изменение:

Это происходит потому, что все объекты в JavaScript передаются по ссылкам. Что, если мы обозначили один и тот же объект в 10 разных местах кода?

Например, мы назначили эту ссылку для 10 различных переменных на протяжении всего кода, а затем изменили седьмую переменную в некоторой точке. В итоге все другие переменные, которые содержат ссылку на тот же указатель в памяти, будут изменены:

const bobby = {
  name: 'bobby',
  age: 15,
  gender: 'male',
}

function stepOneYearIntoFuture(person) {
  person.age++
  return person
}

const doppleGanger = bobby
const doppleGanger2 = bobby
const doppleGanger3 = bobby
const doppleGanger4 = bobby
const doppleGanger5 = bobby
const doppleGanger6 = bobby
const doppleGanger7 = bobby
const doppleGanger8 = bobby
const doppleGanger9 = bobby
const doppleGanger10 = bobby

stepOneYearIntoFuture(doppleGanger7)

console.log(doppleGanger)
console.log(doppleGanger2)
console.log(doppleGanger4)
console.log(doppleGanger7)
console.log(doppleGanger10)

doppleGanger5.age = 3

console.log(doppleGanger)
console.log(doppleGanger2)
console.log(doppleGanger4)
console.log(doppleGanger7)
console.log(doppleGanger10)

Результат:

Вместо этого при необходимости мы можем создавать новые ссылки каждый раз, когда хотим изменить их:

const doppleGanger = { ...bobby }
const doppleGanger2 = { ...bobby }
const doppleGanger3 = { ...bobby }
const doppleGanger4 = { ...bobby }
const doppleGanger5 = { ...bobby }
const doppleGanger6 = { ...bobby }
const doppleGanger7 = { ...bobby }
const doppleGanger8 = { ...bobby }
const doppleGanger9 = { ...bobby }
const doppleGanger10 = { ...bobby }

Результат:

Читайте также:


Перевод статьи jsmanifest: 5 Anti-Patterns to Avoid When Working With Collections in JavaScript

Предыдущая статьяГамма-функция - интуиция, определение, примеры
Следующая статья5 правил кода