Back

Подводные камни JavaScript: пять проблем, с которыми вы будете сталкиваться снова и снова

Подводные камни JavaScript: пять проблем, с которыми вы будете сталкиваться снова и снова

Вы отправили в продакшн код, который прошёл линтинг, работал в разработке, но всё равно сломался в production. Баг казался очевидным задним числом — пропущенный await, мутированный массив, this, указывающий не туда, куда ожидалось. Эти подводные камни JavaScript сохраняются, потому что гибкость языка создаёт тонкие ловушки, которые современные инструменты не всегда улавливают.

Вот пять распространённых ошибок JS, которые продолжают появляться в реальных кодовых базах, вместе с практическими способами их избежать.

Ключевые выводы

  • Используйте строгое равенство (===), чтобы избежать неожиданного поведения при приведении типов
  • Стрелочные функции сохраняют this из своей внешней области видимости, в то время как обычные функции привязывают this динамически
  • Предпочитайте const и объявляйте переменные в начале их области видимости, чтобы избежать ошибок временной мёртвой зоны
  • Используйте Promise.all для параллельных асинхронных операций и Promise.allSettled, когда вам нужны частичные результаты
  • Используйте неизменяемые методы массивов, такие как toSorted() и structuredClone() для глубокого копирования

Приведение типов всё ещё удивляет

Оператор нестрогого равенства JavaScript (==) выполняет приведение типов, производя результаты, которые кажутся нелогичными, пока вы не поймёте базовый алгоритм.

0 == '0'           // true
0 == ''            // true
'' == '0'          // false
null == undefined  // true
[] == false        // true

Исправление простое: используйте строгое равенство (===) везде. Но приведение типов появляется и в других контекстах. Оператор + выполняет конкатенацию, когда любой из операндов является строкой:

const quantity = '5'
const total = quantity + 3 // '53', а не 8

Современные лучшие практики JavaScript предлагают явное преобразование с помощью Number(), String() или шаблонных литералов, когда намерение имеет значение. Оператор нулевого слияния (??) тоже здесь помогает — он возвращает запасное значение только для null или undefined, в отличие от ||, который рассматривает 0 и '' как ложные значения.

Проблема привязки this

Значение this зависит от того, как вызывается функция, а не от того, где она определена. Это остаётся одной из самых устойчивых проблем JavaScript.

const user = {
  name: 'Alice',
  greet() {
    console.log(this.name)
  }
}

const greet = user.greet
greet() // undefined—'this' теперь указывает на глобальный объект

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

const user = {
  name: 'Alice',
  greet: () => {
    console.log(this.name) // 'this' ссылается на внешнюю область, а не на 'user'
  }
}

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

Поднятие и временная мёртвая зона

Переменные, объявленные с помощью let и const, поднимаются, но не инициализируются, создавая временную мёртвую зону (TDZ), где обращение к ним вызывает ReferenceError:

console.log(x) // ReferenceError
let x = 5

Это отличается от var, который поднимается и инициализируется значением undefined. TDZ существует от начала блока до момента, когда объявление будет вычислено.

Объявления функций поднимаются полностью, но функциональные выражения — нет:

foo() // работает
bar() // TypeError: bar is not a function

function foo() {}
const bar = function() {}

Объявляйте переменные в начале их области видимости и предпочитайте const по умолчанию. Это устраняет сюрпризы TDZ и чётко сигнализирует о намерении.

Подводные камни асинхронности в JavaScript

Забыть await — обычное дело, но более тонкие ошибки с асинхронностью причиняют больше вреда. Последовательные await, когда возможно параллельное выполнение, тратят время впустую:

// Медленно: выполняется последовательно
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()

// Быстро: выполняется параллельно
const [user, posts, comments] = await Promise.all([
  fetchUser(),
  fetchPosts(),
  fetchComments()
])

Ещё одна частая проблема: Promise.all быстро завершается с ошибкой. Если один промис отклоняется, вы теряете все результаты. Используйте Promise.allSettled, когда вам нужны частичные результаты:

const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()])
const successful = results.filter(r => r.status === 'fulfilled')

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

Мутация против неизменяемости в JavaScript

Мутация массивов и объектов создаёт баги, которые сложно отследить, особенно во фреймворках с реактивным состоянием:

const original = [3, 1, 2]
const sorted = original.sort() // Мутирует original!
console.log(original) // [1, 2, 3]

Современный JavaScript предоставляет неизменяемые альтернативы. Используйте toSorted(), toReversed() и with() для массивов. Для объектов синтаксис spread создаёт поверхностные копии:

const sorted = original.toSorted()
const updated = { ...user, name: 'Bob' }

Помните, что spread создаёт поверхностные копии. Вложенные объекты всё ещё разделяют ссылки:

const copy = { ...original }
copy.nested.value = 'changed' // Также изменяет original.nested.value

Для глубокого клонирования используйте structuredClone() или обрабатывайте вложенные структуры явно.

Заключение

Эти пять проблем — приведение типов, привязка this, поднятие, неправильное использование асинхронности и случайная мутация — составляют непропорционально большую долю багов JavaScript. Распознавание их при ревью кода становится автоматическим с практикой.

Включите строгие правила ESLint, такие как eqeqeq и no-floating-promises. Рассмотрите TypeScript для проектов, где важна типобезопасность. Самое главное — пишите код, который делает намерение явным, а не полагается на неявное поведение JavaScript.

Часто задаваемые вопросы

JavaScript включает оба оператора по историческим причинам и для гибкости. Оператор нестрогого равенства (==) выполняет приведение типов перед сравнением, что может быть полезно, но часто даёт неожиданные результаты. Оператор строгого равенства (===) сравнивает и значение, и тип без приведения. Современная лучшая практика настоятельно рекомендует ===, потому что это делает сравнения предсказуемыми и уменьшает количество багов.

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

Временная мёртвая зона (TDZ) — это период между входом в область видимости и точкой, где объявляется переменная let или const. Обращение к переменной в течение этого периода вызывает ReferenceError. Избегайте проблем TDZ, объявляя переменные в начале их области видимости и предпочитая const по умолчанию. Это делает ваш код более предсказуемым и лёгким для чтения.

Используйте Promise.all, когда все промисы должны успешно завершиться для того, чтобы ваша операция имела смысл — он быстро завершается с ошибкой, если любой промис отклоняется. Используйте Promise.allSettled, когда вам нужны результаты от всех промисов независимо от индивидуальных сбоев, например, при получении данных из нескольких опциональных источников. Promise.allSettled возвращает массив объектов, описывающих каждый результат как выполненный (fulfilled) или отклонённый (rejected).

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay