Потеря контекста: типичные случаи

Потеря контекста (context loss) — ситуация, когда this внутри метода перестаёт указывать на ожидаемый объект из-за того, что метод был передан как колбэк или вызван в отрыве от объекта.

Зачем нужно

this в JavaScript — динамический: его значение определяется не тем, где метод определён, а тем, как он вызван. Непонимание этого правила приводит к ошибкам TypeError: Cannot read property ... of undefined, которые сложно отлаживать.

Где используется

  • Обработчики событий DOM (addEventListener)
  • Колбэки в setTimeout, setInterval
  • Методы массивов (map, forEach) с методами класса
  • Деструктуризация методов объекта

Случай 1: Метод как колбэк

class Timer {
  constructor {
    this.count = 0;
  }

  tick {
    this.count++;              // this = ?, зависит от вызова
    console.log(this.count);
  }
}

const timer = new Timer();

// Правильный вызов — this = timer
timer.tick; // 1

// Потеря контекста: tick передаётся как функция, не метод
const tick = timer.tick;
tick; // TypeError: Cannot read properties of undefined (reading 'count')
// или: NaN — в нестрогом режиме this = window/global

Случай 2: setTimeout / setInterval

class Clock {
  constructor {
    this.seconds = 0;
  }

  start {
    // this здесь = экземпляр Clock
    setTimeout(function {
      this.seconds++; // this = undefined (строгий) или window (нестрогий)!
      console.log(this.seconds);
    }, 1000);
  }
}

// Решение 1: стрелочная функция (замыкает this из start)
start {
  setTimeout(() => {
    this.seconds++; // this = экземпляр Clock
    console.log(this.seconds);
  }, 1000);
}

// Решение 2: сохранить ссылку
start {
  const self = this;
  setTimeout(function {
    self.seconds++;
  }, 1000);
}

Случай 3: Обработчик DOM-события

class Button {
  constructor(element) {
    this.label = 'Кнопка';
    this.element = element;
  }

  handleClick {
    // При вызове через DOM: this = элемент, не экземпляр Button
    console.log(this.label); // undefined — потеря контекста
  }

  bind {
    // Проблема:
    this.element.addEventListener('click', this.handleClick);

    // Решение 1: bind
    this.element.addEventListener('click', this.handleClick.bind(this));

    // Решение 2: стрелочная функция
    this.element.addEventListener('click', () => this.handleClick);

    // Решение 3: class field (стрелочный метод)
  }
}

// Решение 3: class fields — метод всегда привязан к экземпляру
class ModernButton {
  label = 'Кнопка';

  handleClick = () => {
    console.log(this.label); // всегда правильный this
  };
}

Случай 4: Деструктуризация метода

const user = {
  name: 'Иван',
  greet {
    return `Привет, ${this.name}!`;
  }
};

user.greet; // 'Привет, Иван!'

const { greet } = user; // отрываем метод от объекта
greet; // TypeError или 'Привет, undefined!'

// Решение: bind при деструктуризации
const { greet: boundGreet } = { greet: user.greet.bind(user) };
boundGreet; // 'Привет, Иван!'

Методы bind, call, apply

function introduce(greeting, punctuation) {
  return `${greeting}, меня зовут ${this.name}${punctuation}`;
}

const person = { name: 'Анна' };

// call — вызов немедленно, аргументы через запятую
introduce.call(person, 'Привет', '!'); // 'Привет, меня зовут Анна!'

// apply — вызов немедленно, аргументы массивом
introduce.apply(person, ['Добрый день', '.']); // 'Добрый день, меня зовут Анна.'

// bind — возвращает новую функцию с фиксированным this
const boundIntroduce = introduce.bind(person, 'Здравствуйте');
boundIntroduce('?'); // 'Здравствуйте, меня зовут Анна?'

Частые ошибки

1. Стрелочная функция как метод объекта

const obj = {
  name: 'Тест',
  // Стрелка замыкает this из контекста создания (window/undefined)
  greet:  => `Привет, ${this.name}` // this НЕ равно obj!
};

obj.greet; // 'Привет, undefined'

// Правильно: обычная функция как метод
const obj = {
  name: 'Тест',
  greet { return `Привет, ${this.name}`; } // this = obj
};

2. Повторный bind не работает

function fn() { return this.x; }
const bound = fn.bind({ x: 1 });
const reBound = bound.bind({ x: 2 });
reBound; // 1 — первый bind нельзя перезаписать

Связанные темы

Ресурсы


⚡ Источник: Как работает this · AsForJS

  • 📅 2023-05-07 · YouTube · ID: 4tg4qokVS9o
  • Тезисы:
    • Потеря — это не баг языка, а строгое следование спеке: важна форма вызова, не передача ссылки
    • setTimeout(obj.method, 1) — передача ссылки, вызов внутри происходит без dot notation → this = undefined
    • Деструктуризация метода = такая же потеря, та же причина
    • API хост-среды могут «магически» восстанавливать this (addEventListener → currentTarget)
    • bind перекрывает любую логику API
  • Цитата: «Запомните: как функция вызвана, не как к ней передали ссылку, не как на неё сослались, а как функция вызвана.»
  • Подробнее: this в callback и API

⚡ Источник: Вирішуємо завдання із співбесід this так, this сяк · AsForJS

  • 📅 2024-04-19 · YouTube · ID: nwrN8FY_cVo
  • Тезисы:
    • На собеседованиях задачи на потерю this сводятся к разбору формы вызова
    • Класс-поле со стрелкой (fn = () => {}) — самый защищённый способ от потери