this, call/apply/bind и стрелки

«Что здесь выведет this?» — вопрос, на котором сыпятся даже те, кто пишет на React второй год. Секрет в том, что this определяется не там, где функция объявлена, а тем, как её вызвали.

this — это не переменная функции, а невидимый аргумент, который подставляется в момент вызова. У обычных функций он зависит от способа вызова, у стрелочных его нет вообще: они берут this из окружающей области видимости.

Практическая ценность вопроса — прямая. Именно из-за this обработчик, переданный в onClick без привязки, падает с TypeError. Именно поэтому в классовых React-компонентах в конструкторе писали this.handleClick = this.handleClick.bind(this). И именно поэтому в колбэках все пишут стрелки.

Вопрос

const user = {
  userName: 'Аня',
  greet() {
    // this && this.userName — чтобы пример вёл себя одинаково
    // и в строгом, и в обычном режиме
    console.log('Привет, я ' + (this && this.userName));
  },
};

user.greet();                        // вызов как метод

const detached = user.greet;         // достали функцию из объекта
detached();                          // вызов как обычной функции

const bound = user.greet.bind(user); // привязали контекст навсегда
bound();

Результат:

Привет, я Аня
Привет, я undefined
Привет, я Аня

Функция одна и та же — результат разный. Метод не «принадлежит» объекту: user.greet — это просто ссылка на функцию, лежащая в свойстве. Объект подставляется в this только в тот момент, когда перед скобками вызова стоит точка.

Важная деталь для честного ответа: в строгом режиме (а модули и тела классов — всегда строгие) detached() получит this === undefined, и обращение this.userName бросит TypeError: Cannot read properties of undefined. В нестрогом режиме this подменится на globalThis, и вы тихо получите undefined. В примере выше стоит защита this && this.userName, чтобы вывод не зависел от режима.

Четыре правила, по которым определяется this

Разбирайте любой вызов сверху вниз — первое подошедшее правило и побеждает.

ПравилоКак выглядит вызовЧему равен this
1. newnew User('Аня')Новый созданный объект
2. Явная привязкаfn.call(obj), fn.apply(obj), fn.bind(obj)Переданный obj
3. Метод объектаobj.fn()Объект слева от точки
4. Обычный вызовfn()undefined (strict) или globalThis (нестрогий)
СтрелкалюбойПравила не применяются: this берётся из места объявления

call, apply, bind

Все трое делают одно: подставляют this вручную. Разница — в аргументах и в моменте вызова.

function intro(greeting, punct) {
  return greeting + ', я ' + this.userName + punct;
}

const anya = { userName: 'Аня' };

// call — вызывает сразу, аргументы через запятую
console.log(intro.call(anya, 'Привет', '!'));

// apply — вызывает сразу, аргументы массивом
console.log(intro.apply(anya, ['Здравствуйте', '.']));

// bind — НЕ вызывает, а возвращает новую функцию с приклеенным this
// заодно умеет частичное применение: 'Хай' зафиксирован
const introAnya = intro.bind(anya, 'Хай');
console.log(introAnya('?'));

Результат:

Привет, я Аня!
Здравствуйте, я Аня.
Хай, я Аня?

Мнемоника, которую любят на собеседованиях: call — comma (через запятую), apply — array (массивом), bind — bound (вернёт связанную функцию на потом).

Ловушка: повторный bind не работает

function tell() {
  return this.tag;
}

const first = tell.bind({ tag: 'A' });
const second = first.bind({ tag: 'B' }); // пытаемся перепривязать

console.log(second());

Результат:

A

bind возвращает так называемую bound function — обёртку, которая жёстко зашила this внутрь себя и игнорирует то, что ей передадут снаружи. Победитель — первый bind. То же самое произойдёт с second.call({ tag: 'C' }).

Почему у стрелки нет своего this

Стрелочная функция вообще не создаёт собственное значение this. Когда движок видит this внутри стрелки, он ищет его так же, как искал бы обычную переменную: во внешней области видимости. Отсюда два следствия — одно спасительное, другое опасное.

Опасное: стрелка — плохой метод объекта

const shop = {
  items: ['молоко', 'хлеб'],

  showMethod() {
    console.log('обычный метод: this — это shop?', Array.isArray(this && this.items));
  },

  showArrow: () => {
    // объявлена на верхнем уровне, а не внутри метода:
    // this взят из внешней области, объект shop тут ни при чём
    console.log('стрелка-метод:  this — это shop?', Array.isArray(this && this.items));
  },
};

shop.showMethod();
shop.showArrow();

Результат:

обычный метод: this — это shop? true
стрелка-метод:  this — это shop? false

Объектный литерал не создаёт области видимости. Поэтому this внутри стрелки-метода — это this модуля или скрипта, а не shop. Правило простое: методы объекта — обычными функциями, стрелки — для колбэков внутри методов.

Спасительное: стрелка в колбэке

const timer = {
  ticks: 0,

  startPlain() {
    const self = this;
    setTimeout(function () {
      console.log('обычная функция в колбэке: this === timer?', this === self);
    }, 0);
  },

  startArrow() {
    const self = this;
    setTimeout(() => {
      console.log('стрелка в колбэке:         this === timer?', this === self);
    }, 0);
  },
};

timer.startPlain();
timer.startArrow();

Результат:

обычная функция в колбэке: this === timer? false
стрелка в колбэке:         this === timer? true

Обычная функция, которую setTimeout вызывает как fn(), теряет контекст. Стрелка ничего не теряет — ей нечего терять: она просто продолжает видеть this метода startArrow. До ES6 ровно для этого писали const self = this; — этот приём вы наверняка встретите в старом коде.

Как это работает под капотом: потеря контекста в React

Тела классов всегда исполняются в строгом режиме, поэтому потерянный this там равен undefined — и падение происходит громко.

class Button {
  constructor(label) {
    this.label = label;
    this.handleClickBound = this.handleClick.bind(this); // привязали в конструкторе
  }

  handleClick() {
    console.log('клик по кнопке:', this && this.label);
  }
}

const btn = new Button('Купить');

// имитируем то, что делает браузер или React: просто зовёт fn()
function callAsCallback(fn) {
  fn();
}

callAsCallback(btn.handleClick);      // контекст потерян
callAsCallback(btn.handleClickBound); // контекст сохранён

Результат:

клик по кнопке: undefined
клик по кнопке: Купить

Передавая btn.handleClick, мы передаём голую функцию — точка с объектом остаётся позади. Тот же самый механизм ломает классовые React-компоненты.

class Counter extends React.Component {
  state = { count: 0 };

  // ПЛОХО: обычный метод + передача по ссылке => this === undefined при клике
  handleClickBroken() {
    this.setState({ count: this.state.count + 1 }); // TypeError
  }

  // ХОРОШО (вариант 1): bind в конструкторе
  // ХОРОШО (вариант 2): поле класса со стрелкой — this берётся из экземпляра
  handleClick = () => {
    this.setState((prev) => ({ count: prev.count + 1 }));
  };

  render() {
    return (
      <button onClick={this.handleClick}>
        Кликов: {this.state.count}
      </button>
    );
  }
}

Кстати, именно эта боль — одна из причин, почему функциональные компоненты и хуки вытеснили классы: в функции никакого this просто нет, и терять нечего.

Частые ошибки на собеседовании

  • «this — это объект, в котором объявлена функция». Нет. Он определяется способом вызова, а не местом объявления (кроме стрелок — у них ровно наоборот).
  • «Стрелка привязывает this к объекту». Стрелка вообще не создаёт this и не «привязывает» — она берёт его из внешней области. Поэтому arrow.call(obj) и arrow.bind(obj) не действуют.
  • Стрелка как метод объекта. Литерал объекта не создаёт области видимости — this будет не тем, что вы ждёте.
  • Стрелка как обработчик DOM-события, если вам нужен this === element. Обычная функция получит элемент, стрелка — нет (используйте event.currentTarget).
  • Путать call и apply. Мелочь, но её замечают.
  • Не знать про повторный bind. Классический добивающий вопрос — побеждает первый.

Итоги — шпаргалка

  • this определяется в момент вызова. Порядок правил: newcall/apply/bindobj.method() → обычный вызов.
  • Обычный вызов: undefined в строгом режиме, globalThis — в нестрогом.
  • call — аргументы через запятую, apply — массивом, bind — возвращает новую функцию (и умеет частичное применение).
  • Повторный bind бесполезен: контекст фиксирует первый.
  • У стрелки нет своего this: она берёт его лексически. Идеальна для колбэков, непригодна как метод объекта.
  • Метод, переданный в колбэк по ссылке, теряет контекст. Лечение: bind, стрелка-обёртка или поле класса со стрелкой.
Проверьте себя
1. const greet = user.greet; greet(); — что произойдёт в строгом режиме (модуль или класс), если внутри метода есть обращение к this.name?
AВыведет имя из user — метод помнит объект, в котором объявлен
Bthis будет undefined, и обращение к this.name бросит TypeError
Cthis станет globalThis даже в строгом режиме, вывод будет пустым
DФункция не вызовется: движок запретит вызов метода вне объекта
2. Что выведет: function f() { return this.tag; } const g = f.bind({tag: 'A'}).bind({tag: 'B'}); console.log(g());
AB — побеждает последний bind
BA — повторный bind не переопределяет уже привязанный контекст
Cundefined — двойной bind сбрасывает this
DБросит TypeError: нельзя привязать bound function