Интуиция подсказывает: раз функция описана внутри другой функции, значит и this у неё «унаследуется» от внешней. Логично же — вложенность вроде должна что-то значить. Но нет: вложенность обычной функции ничего не даёт. Внутренняя функция — это самостоятельная функция со своим this, который определяется в момент её вызова, а не в момент описания.

Разберём кейс на примере, посмотрим, почему так, и покажем три способа исправить.

Пример ошибки

Классический случай — метод объекта, внутри которого объявлена обычная функция:

const numbers = {
  numberA: 5,
  numberB: 10,
  sum: function() {
    console.log(this === numbers);       // => true
    function calculate() {
      // this в этой функции — window или undefined в строгом режиме
      console.log(this === numbers);     // => false
      return this.numberA + this.numberB;
    }
    return calculate();
  }
};

numbers.sum();                            // => NaN или TypeError в строгом режиме

numbers.sum() — это вызов метода объекта (см. главу 3), поэтому внутри sum контекст — сам объект numbers. Всё правильно.

Но calculate() внутри — это уже простой вызов функции, не вызов метода. Мы вызываем её просто как calculate(), без объекта-владельца. Для JavaScript это тот же самый сценарий, что и в предыдущей подглавеthis становится глобальным объектом (window) или undefined в строгом режиме.

Результат: this.numberA + this.numberB внутри calculate обращается к свойствам window.numberA и window.numberB, которых нет. Получаем undefined + undefined = NaN. В строгом режиме — TypeError: Cannot read properties of undefined.

Ключевая мысль: факт, что calculate определена внутри sum, никак не влияет на её this. Определение и вызов — разные вещи.

Способ 1: явный вызов через .call() или .apply()

Первый способ решить проблему — вызвать внутреннюю функцию через .call(this), передавая нужный контекст явно. Разбор непрямого вызова — в главе 5.

const numbers = {
  numberA: 5,
  numberB: 10,
  sum: function() {
    console.log(this === numbers);       // => true
    function calculate() {
      console.log(this === numbers);     // => true
      return this.numberA + this.numberB;
    }
    // используем .call() для явной передачи контекста
    return calculate.call(this);
  }
};

numbers.sum();                            // => 15

calculate.call(this) вызывает функцию с явно указанным контекстом — тем самым this, который сейчас в sum (то есть numbers). Функция работает как ожидалось.

Работает, но многословно. Каждый внутренний вызов оборачивать в .call(this) — шум.

Способ 2: стрелочная функция

Второй способ элегантнее: заменить обычную вложенную функцию на стрелочную. У стрелочной функции нет своего this — она наследует его лексически из внешней области видимости (подробнее в главе 7).

const numbers = {
  numberA: 5,
  numberB: 10,
  sum: function() {
    console.log(this === numbers);       // => true
    const calculate = () => {
      console.log(this === numbers);     // => true (унаследовано из sum)
      return this.numberA + this.numberB;
    };
    return calculate();
  }
};

numbers.sum();                            // => 15

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

Способ 3: bind() — прибитый контекст

Третий способ — создать «связанную» функцию через .bind(this), у которой this навсегда установлен в переданное значение. Разбор в главе 6.

const numbers = {
  numberA: 5,
  numberB: 10,
  sum: function() {
    const calculate = function() {
      return this.numberA + this.numberB;
    }.bind(this);   // навсегда связываем calculate с текущим this

    return calculate();
  }
};

numbers.sum();                            // => 15

В современном коде реже, чем стрелочная функция, но встречается — особенно в React-классах до появления hooks (там this.method = this.method.bind(this) в конструкторе был обычной практикой).

Как выбирать способ

Для нового кода:

  • Стрелочная функция — дефолт в 90% случаев. Читабельно, без лишних символов, современно.
  • bind — когда нужна ссылка на функцию, которую передашь наружу (event listener, callback), и хочешь один раз зафиксировать контекст.
  • .call(this) — когда функция уже определена (не можешь сделать стрелочной), и нужно один раз вызвать с нужным контекстом.

В legacy-коде до ES6 встречается ещё один приём — сохранить this в локальную переменную с говорящим именем (self, that, _this) и использовать её вместо this. В новом коде так не пишут — стрелочная функция делает то же компактнее и без риска забыть «присвоить».

В коде React с классами до появления hooks проблема this в callbacks была особенно частой. Сейчас с функциональными компонентами и хуками эта проблема практически исчезла — хуки и стрелочные функции не имеют собственного this. Но старый код с классами всё ещё встречается, поэтому знать способы решения важно.