Функции в 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(); // undefined

JavaScript сам расставляет точки с запятой и после голого 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, асинхронный код и события.

Тесты по теме

Функции в JavaScript: 7 ловушек, на которых спотыкаются даже опытные | KeepSkill