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
  • Явно заданный через bind this не переопределяется ничем
  • Это самый надёжный способ зафиксировать 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 так, как ему вздумается, нарушая все законы и все запреты.»

См. также