this в callback и API
Важно не как ссылка на метод передана, а как функция в итоге вызвана. API хост-среды (браузер, Node) вправе нарушать правила JS и назначать
thisпо своим правилам — нужно читать спецификацию каждого API.
Что это / Зачем
- Самая частая ловушка с
this— передача метода как callback - Передача ссылки ≠ вызов в dot notation →
thisтеряется - Внешние API (addEventListener, setTimeout, fetch) могут назначать
thisпо-своему - Решение: bind, стрелочная обёртка, либо явное чтение спеки API
Главное правило
// Важно как функция ВЫЗВАНА, не как передана
const obj = { method { return this; } };
obj.method; // dot notation -> this = obj
const fn = obj.method; // только сохранили ссылку
fn; // нет dot notation -> this = undefined
setTimeout(obj.method, 0); // передача ссылки, вызов без точки -> this = undefined
Поведение конкретных API
addEventListener в браузере
document.body.addEventListener('click', function {
console.log(this); // <body> (не undefined!)
});
- По правилам JS
thisбыл быundefined - Но HTML5 спецификация связывает
thisсcurrentTarget - Это не JavaScript, это API хост-среды
setTimeout в Node.js
setTimeout(function {
console.log(this); // объект Timeout, не undefined
}, 0);
- В браузере
thisбыл быundefined - В Node
setTimeout— метод класса, вызывается в dot notation - Снова: разница в хост-среде, не в языке
setTimeout в браузере / чистом V8
setTimeout(function {
console.log(this); // window (non-strict) или undefined (strict)
}, 0);
Решения
1. bind
const obj = { name: 'X', greet { console.log(this.name); } };
setTimeout(obj.greet.bind(obj), 100); // 'X'
button.addEventListener('click', obj.greet.bind(obj));
bindсоздаёт новую функцию с зафиксированнымthis- Высший приоритет — нельзя переопределить даже через API
2. Стрелочная обёртка
setTimeout( => obj.greet, 100);
// Внутри стрелки сохраняем форму dot notation
- Стрелка вызывает
obj.greetв правильной форме thisстрелки не важен, важна форма вызова внутри неё
3. Стрелочная функция как поле класса
class Btn {
constructor(name) { this.name = name; }
handleClick = () => {
console.log(this.name); // лексический this класса
};
}
const b = new Btn('Save');
button.addEventListener('click', b.handleClick); // работает
- Стрелка замыкает
thisэкземпляра при создании - Не зависит от формы вызова
bind перекрывает поведение API
const ctx = { tag: 'custom' };
document.body.addEventListener('click', doClick.bind(ctx));
// this = ctx, не <body>
// API больше не влияет на this
- Явно заданный через
bindthisне переопределяется ничем - Это самый надёжный способ зафиксировать
this
Ключевые правила
- Передача ссылки на метод не сохраняет
this - Важна форма вызова, а не источник функции
- API хост-среды описывают поведение
thisв своей спеке (HTML5, Node docs) bind— самый надёжный способ зафиксироватьthis
Подводные камни
setTimeoutведёт себя по-разному в Node и браузереdocument.body.onclick = obj.methodтеряетthis- В React-классах нужно
bindв конструкторе или class fields со стрелкой - Деструктуризация метода
const { greet } = obj; greet;теряетthis
🎓 Источник: Как работает this в javascript · ⚡ AsForJS
- 📅 2023-05-07 · YouTube · ID:
4tg4qokVS9o - Тезисы:
- Запомни: важно как функция вызвана, не как передана ссылка
- Любое API может назначить
thisкак ему вздумается - HTML5 addEventListener связывает
this = currentTarget— это спека HTML5 - В Node setTimeout — метод класса Timer →
this = объект Timeout bindперекрывает поведение API — высший приоритет- Для API всегда читай спецификацию, не угадывай
- Цитата: «Запомните: как функция вызвана, не как к ней передали ссылку, не как на неё сослались, а как функция вызвана.»
- Цитата: «Согласно стандарту языка JavaScript любое API может назначить this так, как ему вздумается, нарушая все законы и все запреты.»