Функции в JavaScript: 7 ловушек, на которых спотыкаются даже опытные
Подъём объявлений, потерянный this, стрелочная функция вместо метода, замыкание в цикле и ещё три случая, когда функция ведёт себя не так, как ждёшь. С примерами и с тем, как написать правильно.
Опубликовано 3 ноября 2026 г.
Функции в JavaScript кажутся самой простой частью языка: объявил, вызвал, получил результат. Но именно на них чаще всего ломаются собеседования и появляются баги, которые сложно поймать глазами. Ниже семь случаев, которые встречаются в реальном коде. Попробуй сначала угадать результат, а потом читай объяснение.
1. Объявление поднимается, выражение — нет
greet(); // "Привет"
function greet() {
console.log('Привет');
}Объявление функции (function greet() {}) поднимается целиком: его можно вызвать выше по коду. С функциональным выражением так не получится:
sayHi(); // TypeError: sayHi is not a function
var sayHi = function () {
console.log('Привет');
};
wave(); // ReferenceError: Cannot access 'wave' before initialization
const wave = () => console.log('👋');С var поднимается только переменная, и до присваивания в ней лежит undefined, поэтому вызвать её нельзя. Переменные let и const до своей строки находятся во «временной мёртвой зоне», и обращение к ним сразу падает с ReferenceError.
Как писать: объявляй функции до использования или осознанно опирайся на подъём только у function-объявлений. Смешивать оба стиля в одном файле — верный путь запутаться.
2. return и перенос строки
function getUser() {
return
{
name: 'Анна'
};
}
getUser(); // undefinedJavaScript сам расставляет точки с запятой и после голого return ставит её на конце строки. Функция возвращает undefined, а объект ниже становится обычным блоком кода, который никогда не выполнится. Ошибки при этом нет.
Как писать: открывающая скобка должна стоять на одной строке с return: return {. Prettier и ESLint ловят это автоматически.
3. Метод, который потерял this
const counter = {
count: 0,
inc() {
this.count += 1;
return this.count;
},
};
counter.inc(); // 1
const inc = counter.inc;
inc(); // TypeError: Cannot read properties of undefined (reading 'count')this определяется не тем, где функция написана, а тем, как её вызвали. counter.inc() вызывается через объект, и this — это counter. Когда метод сохраняют в переменную или передают как колбэк, связь с объектом теряется. В строгом режиме и в ES-модулях this становится undefined, и код падает. В старом нестрогом скрипте this указывает на глобальный объект, и ошибки вообще не будет: функция тихо вернёт NaN.
Чаще всего это всплывает в колбэках:
setTimeout(counter.inc, 1000); // this потерян
setTimeout(() => counter.inc(), 1000); // работает
setTimeout(counter.inc.bind(counter), 1000); // тоже работаетКак писать: передавай не сам метод, а стрелочную обёртку, или привязывай this через bind.
4. Стрелочная функция вместо метода
const timer = {
seconds: 10,
label: () => `секунд: ${this.seconds}`,
};
timer.label(); // "секунд: undefined"У стрелочной функции нет своего this, она берёт его из места, где создана. Объект-литерал не создаёт свою область видимости, поэтому this здесь внешний: в обычном скрипте это глобальный объект, а в ES-модуле вообще undefined, и вызов упадёт с ошибкой.
Зато внутри обычного метода стрелочная функция — как раз то, что нужно: она сохраняет this метода.
const timer = {
seconds: 3,
start() {
const id = setInterval(() => {
this.seconds -= 1; // this — это timer
if (this.seconds === 0) clearInterval(id);
}, 1000);
},
};Как писать: методы объектов — обычным синтаксисом start() {}, колбэки внутри них — стрелочными функциями.
5. Замыкание в цикле с var
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3, 3, 3Все три колбэка замыкают одну и ту же переменную i: у var область видимости — вся функция, а не тело цикла. Когда таймеры срабатывают, цикл уже закончился, и i равна 3.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0, 1, 2С let на каждой итерации создаётся новая переменная, и каждый колбэк видит своё значение.
Как писать: в новом коде var не нужен. Если в старом коде видишь var в цикле с колбэками, проверь это место первым.
6. Значение по умолчанию и null
function greet(name = 'гость') {
return `Привет, ${name}`;
}
greet(); // "Привет, гость"
greet(undefined); // "Привет, гость"
greet(null); // "Привет, null"
greet(''); // "Привет, "Значение по умолчанию подставляется только тогда, когда аргумент равен undefined. null, пустая строка и 0 — это настоящие значения, и функция примет их как есть. Это особенно заметно с данными из API, где «пустое» поле часто приходит как null.
Как писать: если null тоже должен означать «нет значения», используй name ?? 'гость' внутри функции.
Приятная деталь: значение по умолчанию вычисляется заново при каждом вызове, поэтому общего изменяемого массива между вызовами, как в Python, здесь не будет:
function add(item, list = []) {
list.push(item);
return list;
}
add(1); // [1]
add(2); // [2]7. map(parseInt)
['1', '2', '3'].map(parseInt); // [1, NaN, NaN]map передаёт в колбэк не только элемент, но и его индекс (и весь массив). А parseInt вторым аргументом принимает систему счисления. В итоге вызываются parseInt('1', 0), parseInt('2', 1) и parseInt('3', 2). Основание 0 означает «определить самому», поэтому первый вызов даёт 1. Системы с основанием 1 не бывает, а цифры 3 нет в двоичной системе — отсюда два NaN.
Как писать: передавай колбэк явно, .map((s) => parseInt(s, 10)), или используй .map(Number), если нужна именно конвертация строки в число.
Что общего у этих ловушек
Почти все они про то, что функция в JavaScript — это значение: её поднимают, передают, вызывают с другим this и с аргументами, которых ты не ждал. Код при этом часто не падает, а тихо возвращает не то. Такое плохо ловится на ревью, зато хорошо проверяется вопросами: смотришь на пять строк и говоришь, что будет в консоли.
В тестах ниже как раз такие вопросы, и к каждому есть разбор ответа. Начальный уровень покрывает функции, значения и сравнения, средний — this, асинхронный код и события.