Всё, что метод получает от «принадлежности» объекту, держится ровно на одной синтаксической детали — на точке в вызове. Если точка есть (obj.method()), this внутри указывает на объект. Если точки нет, а метод куда-то передан или скопирован в переменную — связь моментально разрывается, и функция ведёт себя как самостоятельная. Именно на этой хрупкости построено большинство багов с потерянным this в реальных проектах.
Ниже — три типовых способа «случайно» отделить метод, что с ним происходит, и три инструмента для восстановления контекста.
Сценарий 1: присваивание в переменную
const timer = {
seconds: 0,
tick() {
this.seconds += 1;
console.log(this.seconds);
}
};
timer.tick(); // => 1 (this = timer)
// а теперь оторвали метод:
const runTick = timer.tick;
runTick(); // TypeError: Cannot read properties of undefined
Строчка const runTick = timer.tick копирует ссылку на функцию, но не «прикрепление» к объекту. Последующий runTick() — это уже простой вызов; this внутри — глобальный объект или undefined в строгом режиме (см. подглаву 2.1).
Сценарий 2: метод как callback
Самый частый способ незаметно отделить метод — передать его в другую функцию как аргумент:
class Logger {
constructor(prefix) {
this.prefix = prefix;
}
log(message) {
console.log(`${this.prefix}: ${message}`);
}
}
const errorLog = new Logger('ERROR');
// не работает — this внутри log теряется
setTimeout(errorLog.log, 100, 'network failed');
// или так — тоже не работает
button.addEventListener('click', errorLog.log);
// или в массиве
['ready', 'set', 'go'].forEach(errorLog.log);
Во всех трёх строках JavaScript получает ссылку на метод log, но не на объект errorLog. Когда движок вызовет функцию через setTimeout / event handler / forEach, это будет простой вызов, и this.prefix внутри упадёт с ошибкой.
Сценарий 3: destructuring и деструктурированное присваивание
Более коварный вариант — когда метод «случайно» отделяется через destructuring:
const api = {
baseUrl: 'https://api.example.com',
fetch(path) {
return globalThis.fetch(`${this.baseUrl}${path}`);
}
};
// сработает
api.fetch('/users');
// не сработает — метод отделён
const { fetch } = api;
fetch('/users'); // TypeError: this.baseUrl is undefined
Destructuring — удобный синтаксис для извлечения полей, но с методами он превращается в ловушку: получаешь голую функцию без объекта. Всё то же, что const fetch = api.fetch, только визуально короче.
Способ 1: .bind()
Первый способ — связать метод с его объектом навсегда через .bind() (полный разбор в главе 6):
const boundTick = timer.tick.bind(timer);
boundTick(); // => 2 (this внутри — timer)
// или сразу при передаче
setTimeout(errorLog.log.bind(errorLog), 100, 'network failed');
Метод получает контекст на этапе создания привязки, а не в момент вызова. Даже переданный дальше как callback, метод сохранит правильный this.
Способ 2: стрелочная функция-обёртка
Второй способ — обернуть вызов в стрелочную функцию (см. главу 7):
setTimeout(() => errorLog.log('network failed'), 100);
button.addEventListener('click', () => errorLog.log('button clicked'));
['ready', 'set', 'go'].forEach(msg => errorLog.log(msg));
Стрелочная функция не отделяет метод — внутри неё errorLog.log(...) остаётся полноценным вызовом метода со всей точкой-владельцем. Это самый частый способ решить проблему в современном коде.
Способ 3: class fields с arrow-функциями
Третий способ применим только для классов — определить метод как class-field со стрелочной функцией:
class Logger {
constructor(prefix) {
this.prefix = prefix;
}
// обычный метод — этот this можно потерять
log(message) {
console.log(`${this.prefix}: ${message}`);
}
// стрелочная в class-field — this зафиксирован намертво
logSafe = (message) => {
console.log(`${this.prefix}: ${message}`);
};
}
const errorLog = new Logger('ERROR');
// сработает без .bind() и без обёртки
setTimeout(errorLog.logSafe, 100, 'still works');
Class fields (стандартизированы в ES2022) выполняются в момент создания экземпляра, и стрелочная функция там «захватывает» this лексически. Такой метод фактически хранится как свойство экземпляра, а не как метод прототипа — отделить его от контекста уже невозможно.
React-контекст: до и после hooks
Ключевая история про this в веб-разработке связана с React. До появления hooks (2019 год) компоненты писались как классы, и передача методов в event handlers была нормой:
class TodoItem extends React.Component {
constructor(props) {
super(props);
// так делали в 2016-2018: bind в конструкторе
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.onDelete(this.props.id);
}
render() {
return <li onClick={this.handleClick}>{this.props.text}</li>;
}
}
Позже появилась идея использовать class fields с arrow-функциями — и bind в конструкторе стал не нужен:
class TodoItem extends React.Component {
// class field — this зафиксирован
handleClick = () => {
this.props.onDelete(this.props.id);
};
render() {
return <li onClick={this.handleClick}>{this.props.text}</li>;
}
}
После введения hooks (React 16.8, февраль 2019) функциональные компоненты стали дефолтом, и вопрос про this в React-коде практически исчез. Но старые кодовые базы с классами всё ещё живут — в них паттерн .bind(this) в конструкторе или class-field-arrow остаётся частой встречей.
Как выбирать способ
- Стрелочная обёртка — дефолт для одноразовой передачи метода как callback. Просто, читабельно, работает везде.
- .bind() — когда метод передают многократно и хочется один раз зафиксировать контекст. Или когда нужна именно «связанная функция» как ссылка для последующего removeEventListener.
- Class fields с arrow — для классов, где method-вызов делается через event handlers сотнями раз. Лучший из способов, но работает только в классах и создаёт метод на экземпляр, а не на прототип.
В функциональных React-компонентах и других современных фреймворках проблема отделения метода не возникает вовсе — функциональные компоненты не имеют собственного this. Это одна из причин, почему индустрия ушла от классовых компонентов.