Топ ошибок начинающих JavaScript разработчиков: продвинутый уровень
Когда вы уже освоили базовый синтаксис JavaScript и написали пару скриптов для браузера или Node.js, кажется, что самое сложное позади. Но именно на этапе перехода к реальным проектам (часто на серверной стороне с Node.js) разработчики сталкиваются с коварными ошибками, которые не ломают код сразу, но приводят к трудноуловимым багам, утечкам памяти и неожиданному поведению. В этой статье мы разберём топ-4 продвинутых ошибки, которые допускают начинающие JavaScript-разработчики, и покажем, как их избежать.
1. Непонимание работы замыканий в циклах (Closures in Loops)
Классическая проблема, которая перекочевала из браузерного JavaScript в Node.js. Многие знают, что var имеет функциональную, а не блочную область видимости, но ошибка возникает даже с let при неправильном использовании асинхронных колбэков.
Проблема: Вы создаёте массив функций в цикле и ожидаете, что каждая функция запомнит своё значение счётчика. Вместо этого все функции ссылаются на последнее значение.
// ❌ Ошибка: все вызовы вернут 3
const functions = [];
for (var i = 0; i < 3; i++) {
functions.push(() => console.log(i));
}
functions.forEach(fn => fn()); // 3, 3, 3
Почему так происходит? Замыкание захватывает не значение переменной i, а ссылку на саму переменную. К моменту вызова функции цикл уже завершён, и i равна 3.
Решение (для Node.js): Используйте let для блочной области видимости или создайте новое замыкание через IIFE (Immediately Invoked Function Expression).
// ✅ Исправление с let
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(() => console.log(i));
}
functions.forEach(fn => fn()); // 0, 1, 2
// ✅ Исправление с замыканием (immediately invoked function)
for (var i = 0; i < 3; i++) {
(function(j) {
functions.push(() => console.log(j));
})(i);
}
2. Игнорирование асинхронности: «Гонка» промисов и потеря контекста this
Node.js — это асинхронная среда. Одна из самых частых ошибок — попытка использовать результат асинхронной операции до её завершения или потеря контекста this внутри методов.
Пример 1: Гонка промисов (Race condition)
// ❌ Ошибка: результат может быть undefined
let userData;
fetchUserData().then(data => {
userData = data;
});
console.log(userData); // undefined — синхронный код выполнился раньше
Пример 2: Потеря this в колбэке
class Logger {
constructor(prefix) {
this.prefix = prefix;
}
logLater() {
setTimeout(function() {
console.log(this.prefix); // undefined — this ссылается на глобальный объект
}, 100);
}
}
const logger = new Logger('INFO:');
logger.logLater(); // undefined
Решение: Всегда используйте async/await для последовательных операций и стрелочные функции для сохранения контекста.
// ✅ Исправление с async/await
async function getUserData() {
const userData = await fetchUserData();
console.log(userData); // теперь данные точно есть
}
// ✅ Исправление с arrow function
class Logger {
constructor(prefix) {
this.prefix = prefix;
}
logLater() {
setTimeout(() => {
console.log(this.prefix); // INFO:
}, 100);
}
}
3. Неправильная обработка ошибок в цепочках промисов и стримах
В Node.js многие операции возвращают промисы или стримы. Начинающие разработчики часто забывают обрабатывать ошибки в середине цепочки или в стримах, что приводит к «тихим» падениям приложения.
Проблема с промисами:
// ❌ Ошибка: ошибка в then не будет поймана
fetchData()
.then(data => {
// Если здесь выбросить исключение, catch не сработает,
// если он не привязан к этому then
return processData(data);
})
.catch(err => console.error('Ошибка:', err));
// Если processData вернёт rejected промис, catch выше поймает.
// Но если processData выбросит синхронное исключение, то catch не сработает,
// так как исключение произойдёт до возврата промиса.
Решение: Всегда используйте .catch() в конце цепочки и оборачивайте синхронный код в try/catch внутри async функции.
// ✅ Исправление с async/await
async function getProcessedData() {
try {
const data = await fetchData();
const result = await processData(data); // processData может быть синхронной
return result;
} catch (error) {
console.error('Ошибка обработки:', error);
throw error; // пробрасываем дальше, если нужно
}
}
Проблема со стримами (Streams): В Node.js часто забывают обрабатывать событие 'error' на стримах.
// ❌ Ошибка: необработанная ошибка в стриме
const fs = require('fs');
const readStream = fs.createReadStream('nonexistent.txt');
readStream.pipe(process.stdout); // Вызовет unhandled error и убьёт процесс
// ✅ Исправление
readStream.on('error', (err) => {
console.error('Ошибка чтения файла:', err.message);
});
readStream.pipe(process.stdout);
4. Утечки памяти из-за забытых обработчиков событий и глобальных переменных
В долгоживущих Node.js-приложениях (серверах) утечки памяти критичны. Две основные причины — глобальные переменные и неправильное управление слушателями событий (EventEmitter).
Пример 1: Глобальные переменные
// ❌ Ошибка: переменная cache никогда не очищается
function processRequest(data) {
if (!global.cache) {