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. new | new 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определяется в момент вызова. Порядок правил:new→call/apply/bind→obj.method()→ обычный вызов.- Обычный вызов:
undefinedв строгом режиме,globalThis— в нестрогом. call— аргументы через запятую,apply— массивом,bind— возвращает новую функцию (и умеет частичное применение).- Повторный
bindбесполезен: контекст фиксирует первый. - У стрелки нет своего
this: она берёт его лексически. Идеальна для колбэков, непригодна как метод объекта. - Метод, переданный в колбэк по ссылке, теряет контекст. Лечение:
bind, стрелка-обёртка или поле класса со стрелкой.