JavaScript: глубокое понимание контекста this

Курс объясняет, как работает this в JavaScript в разных режимах и типах вызова функций. Вы научитесь предсказуемо управлять контекстом с помощью bind/call/apply, стрелочных функций и классов, а также избегать типичных ошибок.

1. Что такое this и как он определяется при вызове

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

Зачем вообще нужен this

В JavaScript ключевое слово this — это контекст выполнения функции: значение, к которому функция может обратиться как к текущему объекту.

Главная особенность this в JavaScript:

  • this не привязан к функции навсегда.
  • Значение this определяется в момент вызова функции.
  • Из-за этого одна и та же функция может работать с разным this, в зависимости от того, как её вызвали.

    > "The value of this is determined by how a function is called (runtime binding)." — MDN Web Docs (this)

    Что важно: где вызвали, а не где объявили

    Многие ошибки с this появляются из-за ожидания, что this зависит от места, где написана функция. На самом деле решает call-siteточка вызова.

    Рассмотрим одну и ту же функцию, вызванную разными способами:

    Хотя show — одна и та же функция, this будет разным.

    Четыре базовых правила определения this

    Дальше мы разберём главные правила, по которым движок JavaScript выбирает значение this при вызове.

    !Дерево решений, показывающее приоритет правил привязки this

    Правило по умолчанию

    Если функция вызывается просто по имени, без объекта слева и без специальных механизмов привязки, используется привязка по умолчанию.

    В нестрогом режиме

    В браузере this станет глобальным объектом window (в Node.js — глобальный объект global, но в разных средах детали могут отличаться).

    В строгом режиме

    В строгом режиме this будет undefined. Это полезнее, потому что ошибка проявится сразу.

    Подробнее: MDN Web Docs (Strict mode).

    Неявная привязка: вызов как метода объекта

    Если функция вызывается как метод, то есть в форме obj.fn(), то this становится объектом слева от точки.

    Важно: привязка зависит не от того, где функция лежит, а от того, как она вызвана.

    Типичная ловушка: потеря контекста

    Если метод вытащить и вызвать как обычную функцию, неявная привязка исчезает.

    Причина: fn() — это уже обычный вызов, поэтому работает правило по умолчанию (в strict mode this === undefined).

    Явная привязка: call, apply, bind

    JavaScript позволяет установить this явно.

    call

    fn.call(thisArg, a, b, c) вызывает функцию сразу, подставляя this = thisArg.

    MDN: Function.prototype.call

    apply

    То же, что call, но аргументы передаются массивом.

    MDN: Function.prototype.apply

    bind

    bind не вызывает функцию сразу. Он возвращает новую функцию с навсегда привязанным this.

    MDN: Function.prototype.bind

    Привязка через new: вызов как конструктора

    Если функция вызывается с new, то создаётся новый объект, и this внутри функции указывает на этот новый объект.

    Идея такая:

  • создаётся новый пустой объект
  • this внутри User становится этим объектом
  • свойства записываются в него
  • объект возвращается наружу
  • Приоритет правил (что “сильнее”)

    Если кажется, что применимо несколько правил, нужно помнить приоритет.

  • new (конструктор) — самый высокий приоритет
  • явная привязка (call / apply), а также заранее привязанная функция через bind
  • неявная привязка (вызов как метод obj.fn())
  • правило по умолчанию (обычный вызов)
  • Практический вывод: чтобы понять this, всегда начинайте с поиска точки вызова и формы вызова.

    Краткое резюме

    | Как вызвали функцию | Пример | Каким будет this | |---|---|---| | Как конструктор | new F() | новый созданный объект | | Явно указали | f.call(x) / f.apply(x) | x | | Заранее привязали | const g = f.bind(x) | x | | Как метод объекта | obj.f() | obj | | Обычный вызов | f() | undefined в strict mode, иначе глобальный объект |

    В следующих материалах курса обычно отдельно разбирают частые ловушки (потеря контекста при колбэках), а также особое поведение this у стрелочных функций и в классах.

    2. this в браузере и Node.js: globalThis, strict mode и модули

    this в браузере и Node.js: globalThis, strict mode и модули

    Зачем выделять окружение отдельно

    В предыдущей статье мы разобрали главное правило: значение this выбирается в момент вызова функции и зависит от формы вызова (fn(), obj.fn(), call/apply/bind, new).

    Но есть важная «надстройка»: окружение выполнения (браузер или Node.js) и режим кода (обычный скрипт или модуль) влияют на то,

  • что считается глобальным объектом
  • чему равен this на верхнем уровне файла
  • что произойдёт с this при вызове по умолчанию (fn()) в связке со strict mode
  • Эта статья — про то, как не «переучить» правила из прошлой темы, а правильно применять их в разных средах.

    Глобальный объект и globalThis

    Почему window и global — не универсальны

    Исторически разные среды называли глобальный объект по-разному:

  • в браузере это window (в Web Workers — не window)
  • в Node.js это global
  • Писать универсальный код, опираясь на window или global, неудобно.

    globalThis — стандартное имя глобального объекта

    globalThis — стандартный способ получить глобальный объект в любой среде, где он поддерживается.

    Практический вывод:

  • если вам нужен именно глобальный объект — используйте globalThis
  • если вам нужен именно браузерный объект (например, window.document) — используйте window и понимайте, что это не будет работать в Node.js
  • Источник: MDN (globalThis).

    this на верхнем уровне: «верхний this» в файле

    Важно различать:

  • this внутри функции (он определяется по правилам вызова)
  • this на верхнем уровне файла (вне функций), где правила вызова неприменимы
  • Браузер: обычный скрипт против модуля

    #### Обычный скрипт (не модуль)

    Если код подключён как обычный скрипт (например, через script без type="module"), то на верхнем уровне:

    То есть «верхний this» указывает на глобальный объект.

    #### Модуль (ESM)

    Если файл — модуль (type="module" в браузере), то на верхнем уровне:

    Это сделано специально: модульный код изолированнее и менее склонен к ошибкам из-за неявной работы с глобальной областью.

    Источник: MDN (JavaScript modules).

    Node.js: CommonJS против ES Modules

    #### CommonJS (классический Node.js-формат require)

    В файле CommonJS (обычно .js без режима ESM) верхний this не равен globalThis. На практике он совпадает с module.exports.

    Это частая причина путаницы: в Node.js файл — это модульная «обёртка», а не «глобальный скрипт браузера».

    Справка: Node.js docs (Modules: CommonJS modules).

    #### ES Modules (ESM)

    В Node.js ESM-модуле (например, .mjs или проект с "type": "module") верхний this равен undefined, как и в браузерных модулях:

    Справка: Node.js docs (Modules: ECMAScript modules).

    !Шпаргалка по верхнему this и глобальному объекту в браузере и Node.js

    strict mode и правило по умолчанию: что меняется на самом деле

    Из прошлой статьи: если функция вызывается как fn(), то включается правило по умолчанию.

  • в нестрогом режиме this обычно становится глобальным объектом
  • в строгом режиме this становится undefined
  • Внутри функции: разница видна сразу

    'use strict' важен именно для вызова по умолчанию (fn()), потому что он запрещает неявную подстановку глобального объекта.

    Источник: MDN (Strict mode).

    Модули и strict mode

    У ES-модулей важная особенность: модульный код всегда выполняется в строгом режиме.

    Практические последствия:

  • fn() внутри модуля даст this === undefined (если не сработало неявное/явное правило привязки)
  • верхний this в модуле тоже undefined
  • Это не отменяет правила из прошлой статьи, а делает «мягкое» поведение нестрогого режима менее вероятным.

    Как надёжно писать код, которому не важно окружение

    Не полагайтесь на this как на способ получить глобальный объект

    Иногда встречается анти-паттерн: «получить глобальный объект через this». Это ломается в strict mode и в модулях.

    Вместо этого:

  • используйте globalThis, если вам нужен глобальный объект
  • не используйте глобальный объект как «склад» состояния, если можно обойтись зависимостями и явными параметрами
  • Если вам нужен контекст — закрепляйте его явно

    Если функция должна работать с конкретным объектом:

  • вызывайте как метод: obj.fn()
  • или используйте bind: const fn = original.bind(obj)
  • или call/apply для разового вызова
  • Это переносимо между браузером и Node.js и не зависит от того, модуль у вас или скрипт.

    Краткое резюме

    | Ситуация | Чему равен this | Почему | |---|---|---| | Браузер, верхний уровень обычного скрипта | window | код не модульный, верхний this указывает на глобальный объект | | Браузер, верхний уровень ESM-модуля | undefined | у модулей верхний this не привязан | | Node.js, верхний уровень CommonJS | module.exports | файл выполняется в модульной обёртке CommonJS | | Node.js, верхний уровень ESM | undefined | поведение модулей как в стандарте ESM | | Вызов fn() в strict mode (внутри функции) | undefined | правило по умолчанию в строгом режиме | | Универсальный доступ к глобальному объекту | globalThis | стандартное имя глобального объекта |

    Дальше по курсу обычно логично переходить к тому, где и почему теряется контекст (колбэки, обработчики событий, таймеры) и как это связано со стрелочными функциями и классами.

    3. Правила привязки this: method, function, constructor и indirect call

    Правила привязки this: method, function, constructor и indirect call

    Как эта тема продолжает курс

    В первой статье мы зафиксировали ключевой принцип: this определяется в момент вызова функции и зависит от точки вызова (call-site). Во второй статье мы уточнили, что среда (браузер/Node.js) и формат кода (скрипт/модуль) влияют на глобальный объект и поведение в strict mode.

    Теперь соберём эти идеи в практическую систему:

  • чем отличается вызов как метод (obj.fn()) от обычного вызова функции (fn())
  • как работает вызов как конструктора (new Fn())
  • почему контекст часто “теряется” из-за indirect call (косвенного вызова), даже если точка в коде всё ещё “где-то рядом”
  • Базовая мысль: важна не функция, а форма вызова

    Рассмотрим одну и ту же функцию:

    Это одна и та же функция show, но разные формы вызова дают разный this.

    Method call: вызов как метода (obj.fn())

    Правило

    Если функция вызывается в форме obj.fn(), то this внутри fn будет равен объекту слева от точки.

    Важная деталь: “слева от точки” — это результат выражения

    Слева от точки может стоять не только имя переменной, но и выражение.

    Скобки вокруг a.hello не превращают вызов в косвенный. Форма вызова всё ещё “с точкой”.

    Function call: обычный вызов (fn())

    Правило

    Если функция вызывается как fn(), то срабатывает привязка по умолчанию:

  • в strict mode: this === undefined
  • вне strict mode: this обычно становится глобальным объектом (это зависит от среды, и в современном коде на это лучше не рассчитывать)
  • Практический вывод: если вы ожидаете объект в this, а видите undefined, почти всегда причина в том, что реальный вызов стал fn(), а не obj.fn().

    Constructor call: вызов как конструктора (new Fn())

    Правило

    Вызов с new создаёт новый объект и привязывает this к нему.

    Что важно понимать про new

  • new меняет смысл вызова: это уже не “просто вызвали функцию”
  • внутри функции this — это будущий экземпляр
  • new имеет очень высокий приоритет привязки: если вызов выглядит как new Something(), то this будет новым объектом
  • Источник по new: MDN (new operator).

    Indirect call: когда метод превращается в обычную функцию

    Определение

    Indirect call (косвенный вызов) — это ситуация, когда вы берёте значение функции из объекта, но вызываете его уже без “базы” (без объекта слева от точки). Тогда движок больше не может подставить this как “объект слева”, и вызов превращается в fn().

    Самый простой пример:

    Здесь важно видеть точку вызова:

  • user.hello() — method call
  • fn() — function call
  • Функция та же, но форма вызова изменилась.

    !Наглядно показано, что потеря this происходит из-за формы вызова после “выноса” функции

    Частые шаблоны indirect call

    #### Деструктуризация

    Деструктуризация берёт значение функции, но не сохраняет “объект слева от точки”.

    #### Присваивание в выражении

    Смысл: выражение присваивания возвращает значение (функцию), и вызов происходит уже у результата, без базы.

    #### Оператор запятая

    Оператор запятая вычисляет левую часть, затем правую, и возвращает значение правой части. То есть вы гарантированно превращаете user.hello в “просто функцию”.

    Источник: MDN (Comma operator).

    #### Логические операторы как источник потери контекста

    Оператор && возвращает одно из значений-операндов. Если он возвращает функцию, а потом вы делаете (), это снова вызов без базы.

    Почему это происходит (простыми словами)

    Чтобы сработало правило method call, движку нужно увидеть вызов в форме:

  • “вызови свойство объекта как функцию”
  • То есть нужно, чтобы в момент вызова была связка:

  • объект (база)
  • доступ к свойству (методу)
  • вызов
  • Если вы сначала получаете значение user.hello (функцию), а потом где-то отдельно вызываете её как fn(), то база исчезает, и остаётся только обычный вызов.

    Таблица-шпаргалка по формам вызова

    | Форма вызова | Пример | Что будет thisstrict mode) | |---|---|---| | Method call | obj.fn() | obj | | Function call | fn() | undefined | | Constructor call | new Fn() | новый объект | | Indirect call (вынесли и вызвали) | const f = obj.fn; f() | undefined | | Indirect call (оператор запятая) | (0, obj.fn)() | undefined | | Скобки без потери базы | (obj.fn)() | obj |

    Как не попадаться на indirect call

  • Если функция должна работать с объектом, вызывайте её как метод: obj.fn().
  • Если вам нужно передать метод как колбэк, обычно нужно закрепить контекст:
  • Источник: MDN (Function.prototype.bind).

    Дальше по курсу логично отдельно разобрать “потерю контекста” в обработчиках событий и колбэках, а также почему стрелочные функции ведут себя иначе (у них нет собственного this).

    Краткое резюме

  • obj.fn() даёт this = obj (method call).
  • fn() в strict mode даёт this = undefined (function call).
  • new Fn() даёт this = новый объект (constructor call).
  • Indirect call — это когда вы теряете “объект слева от точки” и вызываете функцию отдельно.
  • Скобки (obj.fn)() не ломают контекст, а вот операции, которые возвращают значение функции (деструктуризация, запятая, присваивание в выражении, &&), часто приводят к потере this.
  • 4. Явное управление контекстом: call, apply и bind

    Явное управление контекстом: call, apply и bind

    Как эта тема продолжает курс

    В прошлых статьях мы закрепили две идеи:

  • this определяется в момент вызова функции (важна точка вызова).
  • контекст легко теряется при indirect call (когда метод превращается в обычный вызов fn()).
  • call, apply и bind — это инструменты, которые позволяют явно задать, каким будет this, даже если форма вызова не помогает (например, при передаче метода как колбэка).

    В терминах правил привязки из первой статьи:

  • call/apply — это явная привязка this на один вызов.
  • bind — это создание новой функции с заранее привязанным this.
  • !Схема показывает различия между call/apply (вызов сразу) и bind (возврат новой функции) и важную оговорку про new.

    Зачем нужно явное управление this

    Типичная проблема выглядит так:

    call: вызвать функцию с заданным this

    Сигнатура и смысл

    fn.call(thisArg, arg1, arg2, ...):

  • сразу вызывает fn;
  • устанавливает this внутри fn равным thisArg;
  • передаёт аргументы через запятую.
  • Источник: Function.prototype.call (MDN).

    Пример: “одалживание” метода

    Иногда удобно взять метод одного типа и применить к другому объекту.

    Здесь slice ожидает, что this будет массивом (или массивоподобным объектом). Через call мы подставляем arguments.

    Что можно передавать как thisArg

  • Объект — будет использован как есть.
  • Примитив ('x', 42, true) — обычно будет временно “обёрнут” в объект (String, Number, Boolean) в нестрогом режиме; в строгом режиме поведение более предсказуемо и ближе к “как передали — так и будет”.
  • null/undefined — важная зона различий со строгим режимом (см. ниже).
  • apply: то же, что call, но аргументы массивом

    Сигнатура и смысл

    fn.apply(thisArg, argsArray):

  • сразу вызывает fn;
  • устанавливает this внутри fn равным thisArg;
  • принимает аргументы одним списком — массивом или массивоподобным объектом.
  • Источник: Function.prototype.apply (MDN).

    Пример: удобная прокладка аргументов

    apply и оператор ...

    Во многих новых кодовых базах роль apply часто заменяет spread:

    Но apply всё ещё полезен, когда:

  • у вас уже есть массив аргументов, и вы хотите вызвать именно с контролем this;
  • вы работаете с массивоподобными структурами;
  • вы пишете обёртки/адаптеры, где аргументы приходят в виде списка.
  • bind: создать новую функцию с привязанным this

    Сигнатура и смысл

    const bound = fn.bind(thisArg, presetArg1, presetArg2, ...):

  • не вызывает fn;
  • возвращает новую функцию;
  • у новой функции this “закреплён” (часто говорят жёстко привязан);
  • можно заранее “пришить” часть аргументов (частичное применение).
  • Источник: Function.prototype.bind (MDN).

    Пример: исправляем потерю контекста в колбэке

    Пример: частичное применение аргументов

    Здесь this не используется, поэтому передают null, а “ценность” bind — в пришитом аргументе 2.

    Важная оговорка: bind и повторная привязка

  • Если функция уже создана через bind, то call/apply обычно не смогут заменить её this.
  • Ещё важнее: new может быть “сильнее” bind

    Если вы используете привязанную функцию как конструктор (new bound()), то создаётся новый объект, и this внутри конструктора будет указывать на этот новый объект.

    При этом “пришитые” аргументы (частичное применение) сохраняются.

    Практический вывод:

  • bind — отличный инструмент для колбэков и методов.
  • но если функция задумана как конструктор, привязка this через bind может вести себя не так, как ожидают, потому что new создаёт свой this.
  • null и undefined как thisArg: строгий режим против нестрогого

    Так как модульный код всегда в строгом режиме (об этом была вторая статья), важно знать типичное правило:

  • в строгом режиме, если вы делаете fn.call(null) или fn.call(undefined), то this внутри fn будет ровно null или undefined;
  • в нестрогом режиме null/undefined часто подменяются на глобальный объект.
  • Демонстрация (строгий режим):

    Практический вывод: в современном коде лучше не использовать “магические” подстановки глобального объекта через null/undefined, а выбирать this осознанно.

    Когда call/apply/bind не помогут

    Есть функции, у которых this работает не так, как у обычных.

    Стрелочные функции

    У стрелочных функций нет собственного this: они “берут” this из внешней области видимости. Поэтому call/apply/bind не меняют их this.

    Источник: Arrow function expressions (MDN).

    Главная идея: call/apply/bind управляют this обычных функций, но не “перепрошивают” модель this у стрелок.

    Практические рекомендации

  • Если нужно вызвать функцию один раз с правильным this, выбирайте call или apply.
  • Если нужно передавать метод как колбэк и не терять this, выбирайте bind.
  • Если аргументы уже в массиве, apply часто удобнее, чем call.
  • Не делайте bind заново в циклах рендера/обработчиках без необходимости: вы создаёте новую функцию каждый раз.
  • Краткое резюме

    | Инструмент | Что делает | Когда использовать | |---|---|---| | call | Вызывает функцию сразу, аргументы через запятую | Разовый вызов с явным this | | apply | Вызывает функцию сразу, аргументы массивом | Разовый вызов, когда аргументы уже списком | | bind | Возвращает новую функцию с привязанным this (и, возможно, аргументами) | Колбэки, обработчики, частичное применение |

    Связь с предыдущими темами курса простая: когда вы видите, что точка вызова превратилась в fn()this стал undefined), call/apply/bind дают способ вернуть контроль над контекстом явно, не полагаясь на форму вызова.

    5. Стрелочные функции: лексический this и когда их использовать

    Стрелочные функции: лексический this и когда их использовать

    Как эта тема связана с предыдущими статьями

    Ранее в курсе мы опирались на правило: this выбирается по форме вызова (call-site): obj.fn(), fn(), new Fn(), call/apply/bind, а также ловушки indirect call, где метод превращается в обычный вызов.

    Стрелочные функции (arrow functions) — важное исключение: у них нет собственного this, поэтому привычные правила привязки this по точке вызова не применяются. Вместо этого стрелка берёт this лексически — из окружающего контекста.

    Источник: MDN (Arrow function expressions).

    Что значит “лексический this”

    Определение

    Лексический this означает:

  • стрелочная функция не создаёт своё значение this;
  • внутри стрелки this будет таким же, как this в той области видимости, где стрелка была создана;
  • на this стрелки влияет не точка вызова, а место объявления (точнее: окружающий this).
  • Это противоположно обычным функциям, где мы всегда начинали анализ с call-site.

    !Диаграмма показывает ключевую разницу: обычные функции получают this от формы вызова, стрелки — от внешнего контекста

    Базовый пример: стрелка внутри метода

    Стрелка полезна, когда вы хотите “захватить” this метода и использовать его в колбэке.

    Почему это работает:

  • scheduleHello() вызван как метод, значит this === user (правило method call из предыдущих тем).
  • стрелка () => { ... } создаётся внутри scheduleHello, поэтому берёт this из scheduleHello.
  • setTimeout вызывает колбэк “как обычную функцию”, но для стрелки это уже не важно: её this не переопределяется.
  • Стрелка против обычной функции: одна и та же задача

    Вариант с обычной функцией (контекст теряется)

    Здесь колбэк — обычная функция. setTimeout вызывает её как fn(), а в strict mode это даёт this === undefined.

    Вариант с bind (контекст закреплён явно)

    Это решение связано с предыдущей статьёй: bind создаёт новую функцию с привязанным this.

    Вариант со стрелкой (контекст берётся лексически)

    Стрелка здесь выступает как “встроенный bind к внешнему this”, но важно помнить: это не bind, а другая модель this.

    Почему call/apply/bind не меняют this у стрелок

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

  • call и apply не заменяют this у стрелки
  • bind не делает стрелке новый this
  • Разбор:

  • makeArrow.call(a) влияет на this обычной функции makeArrow, поэтому стрелка создаётся при this === a.
  • но последующие .call(b) уже не меняют this стрелки.
  • Важные ограничения стрелочных функций

    Стрелки нельзя использовать как конструкторы

    Стрелочная функция не может быть вызвана с new.

    Если вам нужен конструктор или классическая логика “создать экземпляр через new”, используйте function или class.

    У стрелок нет своего arguments

    Стрелка не создаёт собственный объект arguments. Если вам нужен доступ к аргументам:

  • используйте rest-параметры (...args)
  • или используйте обычную функцию
  • Практическое правило: в современном коде вместо arguments обычно выбирают (...args).

    Стрелки и верхний уровень: почему this может быть undefined

    Лексический this берётся из внешнего контекста. Но чему равен внешний this?

  • в ES-модулях (и в браузере, и в Node.js) верхний this равен undefined (модули всегда в строгом режиме)
  • в браузерном обычном скрипте верхний this обычно window
  • Это мы обсуждали во второй статье (про модули и globalThis).

    Пример, который часто удивляет:

    Даже если вызвать obj.arrow() “как метод”, стрелка не получит this = obj, потому что стрелка не использует правило method call. Она берёт this из внешнего контекста. В модуле это будет undefined.

    Вывод: не используйте стрелки как методы объекта, если вам нужен this, указывающий на сам объект.

    Стрелочные функции в обработчиках событий

    В браузерных обработчиках событий часто ожидают, что this — это элемент, на котором произошло событие.

    Обычная функция: this — элемент

    Стрелка: this берётся снаружи, элемент не попадёт в this

    Если вам нужен элемент, используйте:

  • обычную функцию (и тогда this будет элементом)
  • или параметр события event.currentTarget
  • Стрелки в классах: когда это действительно удобно

    В классах есть два распространённых варианта методов.

    Метод прототипа (обычный метод)

    Это та же проблема indirect call: f() — обычный вызов.

    Метод как стрелочное поле (контекст “приклеен” к экземпляру)

    Почему так:

  • стрелка создаётся при создании экземпляра
  • this стрелки — это this конструктора (то есть сам экземпляр)
  • при выносе const f = c.inc контекст не теряется
  • Компромисс:

  • метод-стрелка создаётся заново для каждого экземпляра (это не метод прототипа)
  • но вы получаете удобное поведение для колбэков без bind
  • Когда использовать стрелочные функции

    Хорошие сценарии

  • короткие колбэки, где важна компактность
  • колбэки, которым нужен this внешней функции (часто внутри методов/классов)
  • функциональные операции с данными (map, filter, reduce), где this не нужен вовсе
  • Плохие сценарии

  • методы объектов, которым нужен this, указывающий на объект
  • обработчики событий, если вы хотите использовать this как элемент
  • функции, которые должны работать как конструкторы (new)
  • Краткое резюме

    | Вопрос | Обычная функция | Стрелочная функция | |---|---|---| | Откуда берётся this | из формы вызова (call-site) | из внешнего контекста (лексически) | | Работают ли call/apply/bind для смены this | да | нет | | Можно ли вызывать с new | да (если функция конструктор) | нет | | Подходит ли как метод объекта с this на объект | да | обычно нет | | Полезна ли для колбэков внутри методов/классов | иногда нужно bind | да, часто лучший вариант |

    Главная мысль: стрелка — это не “короткая запись функции”, а функция с другой моделью this. Если вы анализируете this и видите стрелку, не ищите точку вызова: ищите внешний контекст, где стрелка была создана.

    6. this в классах и прототипах: методы, наследование, super

    this в классах и прототипах: методы, наследование, super

    Как эта тема продолжает курс

    В предыдущих материалах мы закрепили основные правила:

  • this определяется в момент вызова (важна точка вызова).
  • this легко теряется при indirect call (вытащили метод и вызвали как fn()).
  • call/apply/bind позволяют управлять контекстом явно.
  • стрелочные функции имеют лексический this и не подчиняются привязке по точке вызова.
  • Классы (class) часто воспринимают как «другую» модель, но в JavaScript это всё та же прототипная система. Поэтому в классах действуют те же правила this, просто добавляются важные детали:

  • где хранятся методы (прототип или экземпляр)
  • что происходит с this при наследовании
  • как работает super и почему он иногда ведёт себя «не как переменная»
  • Источник: MDN (Classes).

    Класс — это прототипы, а не отдельная модель

    Что создаёт class

    Код

    Здесь u.hello() — это вызов как метода, значит this внутри hello равен u.

    Потеря контекста: вынесли метод

    Важно помнить: метод прототипа — это обычная функция, и если она вызывается не как obj.fn(), то this определяется по правилу вызова fn().

    Причина: f() — обычный вызов. В коде классов вы почти всегда находитесь в строгом режиме, поэтому this === undefined.

    Источник: MDN (Strict mode).

    Как исправлять потерю this в классах

    #### Вариант с bind в конструкторе

    Это прямо использует идею из темы про bind: мы создаём новую функцию с «приклеенным» контекстом.

    Источник: MDN (Function.prototype.bind).

    #### Вариант с методом-стрелкой как полем класса

    Почему работает:

  • стрелка берёт this лексически из контекста создания
  • в момент создания экземпляра this в конструкторе — это экземпляр
  • поэтому стрелка «помнит» экземпляр, даже если вызвать её как fn()
  • Компромисс:

  • метод-стрелка создаётся заново для каждого экземпляра (это свойство экземпляра, а не прототипа)
  • зато он удобен для передачи как колбэк без bind
  • Наследование: extends, прототипная цепочка и this

    Что делает extends

    Здесь:

  • super.log() берёт функцию log из Logger.prototype
  • но this внутри Logger.prototype.log будет равен экземпляру a
  • Практический вывод: базовый метод, вызванный через super, обычно работает с данными наследника, потому что this остаётся экземпляром наследника.

    !Как super берёт метод из базового прототипа, но сохраняет this текущего экземпляра

    Тонкость super: это не «свойство объекта», а специальная привязка к прототипу

    Иногда ожидают, что super ведёт себя как «вот этот объект». Но в реальности super работает через внутреннюю привязку метода к классу/прототипу, где метод был объявлен (часто упоминают внутреннее понятие home object).

    В практических терминах это означает:

  • super.method() ищет реализацию не в «текущем объекте», а в прототипе родителя относительно места объявления метода
  • при этом this остаётся тем же, что и в текущем вызове
  • Это может проявиться неожиданно, если вы «переиспользуете» метод в другом объекте.

    Пример идеи:

    Здесь происходят две разные вещи одновременно:

  • this берётся из формы вызова foreign.who() и равен foreign
  • super.who() берёт реализацию из A.prototype, а не из прототипа foreign
  • Практический вывод: если метод использует super, его опаснее «переносить» между объектами как универсальную функцию.

    Статические методы и this

    this в static-методе

    Статический метод вызывается на самом классе:

    Здесь Util.tag('a') — это тоже method call, только объект слева от точки — сам конструктор Util, поэтому this === Util.

    Наследование static-методов и полиморфный this

    Полезная идея: в статике this может быть класс-наследник, что удобно для фабрик и конфигураций.

    Но static тоже теряет this при indirect call

    Причина та же: f() — обычный вызов.

    Практические рекомендации

  • Если метод класса передаётся как колбэк, ожидайте потерю this (это типичный indirect call) и выбирайте:
  • - bind в конструкторе - или метод-стрелку полем класса
  • Обычные методы в теле класса лучше хранить на прототипе (так экономнее по памяти), но это требует дисциплины при передаче методов.
  • super.method() используйте как способ переиспользовать реализацию базового класса, помня, что:
  • - this остаётся текущим экземпляром - сам поиск метода идёт по прототипу родителя относительно места объявления
  • Статические методы — это те же методы, просто вызываются на классе; правила this не меняются.
  • Краткое резюме

    | Сценарий | Где функция | Какой this | Типичная проблема | |---|---|---|---| | Метод класса (prototype method) u.m() | Class.prototype | u | при выносе теряется | | Вынесли метод const f = u.m; f() | функция отдельно | undefined в строгом режиме | indirect call | | Метод-стрелка полем класса | в экземпляре | лексически привязан к экземпляру | больше памяти на экземпляр | | Наследование extends | прототипная цепочка | определяется объектом вызова | обычно не в наследовании, а в call-site | | super.method() | базовый прототип | this текущего экземпляра | переносимость метода с super | | static-метод C.m() | на конструкторе C | C (или наследник) | теряется при const f = C.m |

    7. Практика и анти-паттерны: колбэки, события, таймеры, потеря контекста

    Практика и анти-паттерны: колбэки, события, таймеры, потеря контекста

    Как эта тема продолжает курс

    В предыдущих статьях мы собрали фундамент:

  • this определяется в момент вызова и зависит от формы вызова.
  • Самая частая причина багов — indirect call: метод «оторвали» от объекта, и вызов стал fn(), а не obj.fn().
  • call/apply/bind позволяют управлять this явно.
  • Стрелочные функции берут this лексически и не меняют его через call/apply/bind.
  • В реальной разработке потери контекста почти всегда проявляются не в «учебных» вызовах, а в трёх местах:

  • колбэки (в т.ч. Promise, массивы, таймеры)
  • события (DOM и EventEmitter-подобные модели)
  • передача методов классов как обработчиков
  • Эта статья — практический разбор: почему теряется this, как диагностировать по точке вызова и какие решения являются надёжными, а какие — анти-паттерны.

    !Схема показывает, как метод превращается в обычный вызов при передаче колбэком и почему this теряется

    Главная диагностика: кто реально вызывает ваш колбэк

    Когда вы пишете:

    у вас в голове может быть «вызовется user.hello()». Но движок видит другое:

  • Вы передали значение функции user.hello.
  • Таймер позже вызовет её как обычную функцию: callback().
  • А это уже правило fn() (в модулях и в строгом режиме это почти всегда this === undefined).

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

    Таймеры: setTimeout и setInterval

    Типичный баг: передали метод напрямую

    Это снижает риск потери контекста, но меняет стиль API.

    Колбэки в Promise и асинхронных цепочках

    Анти-паттерн: передать метод в then/catch/finally

    Почему: специфика промисов в том, что они вызывают ваш обработчик как функцию, а не как метод вашего объекта.

    Источник: MDN (Promise.prototype.then).

    Решения

    Выбор между bind и стрелкой-обёрткой обычно такой:

  • если вы многократно переиспользуете обработчик, выгодно один раз сделать bind
  • если это одно место в коде, стрелка-обёртка часто читабельнее
  • Колбэки в массивах: map/filter/reduce и thisArg

    У массивных методов есть важная деталь: многие принимают опциональный thisArg.

    Источник: MDN (Array.prototype.map).

    Пример: thisArg как альтернатива bind

    Практические замечания:

  • thisArg есть не везде и не всегда очевиден в API
  • thisArg не работает для стрелочных функций, потому что у стрелок нет собственного this
  • DOM-события в браузере: addEventListener и this

    Обычная функция: this часто равен элементу

    Почему это работает: браузер вызывает обработчик с установленным this.

    Источник: MDN (EventTarget.addEventListener).

    Стрелка: this не станет элементом

    Это не «особенность браузера», а модель стрелок: this у стрелки лексический.

    Надёжный подход: использовать event.currentTarget

    Если вам нужен элемент, лучше опираться не на this, а на event.currentTarget:

    Плюсы:

  • работает и со стрелками, и с обычными функциями
  • менее магично, чем this
  • Источник: MDN (Event.currentTarget).

    Node.js события: EventEmitter и this

    В Node.js у EventEmitter слушатели обычно вызываются так, что this внутри обработчика (если это обычная функция) указывает на emitter.

    Источник: Node.js docs (Events).

    Но проблема потери контекста появляется снова, как только вы передаёте чужой метод как слушателя:

    Это не «поломка this», а нормальное правило: EventEmitter задаёт свой this слушателю.

    Вывод: если обработчик должен работать с user, привязывайте его к user.

    Классы и обработчики: где this теряется чаще всего

    Методы класса, объявленные в теле класса, лежат на прототипе и являются обычными функциями. Поэтому при передаче как колбэка теряют this так же, как и любой метод объекта.

    Решение 1: bind в конструкторе (один раз)

    Решение 2: метод-стрелка полем класса

    Компромисс: метод-стрелка создаётся на каждый экземпляр (это мы уже обсуждали в теме про стрелки и классы).

    Анти-паттерны и практические ловушки

    Анти-паттерн: bind прямо в addEventListener

    Проблема: вы не сможете корректно снять обработчик, если снова напишете handler.bind(obj), потому что bind создаёт новую функцию каждый раз.

    Надёжный вариант:

    Анти-паттерн: стрелка как метод объекта, если вам нужен this на объект

    Если метод должен работать с user, используйте обычную функцию-метод.

    Анти-паттерн: «сохранить this в self/that», когда есть стрелки и bind

    Исторический приём:

    Сегодня обычно проще и понятнее:

  • стрелка-обёртка, если нужен внешний this
  • или bind, если нужно закрепить this у функции
  • Анти-паттерн: надеяться, что this даст глобальный объект

    В модулях и в строгом режиме this часто undefined. Если нужен глобальный объект, используйте globalThis.

    Источник: MDN (globalThis).

    Шпаргалка решений: что выбирать на практике

    | Ситуация | Симптом | Надёжное решение | Комментарий | |---|---|---|---| | setTimeout(obj.m, 0) | this === undefined | setTimeout(obj.m.bind(obj), 0) | удобно, если нужна именно функция | | setTimeout(obj.m, 0) | this === undefined | setTimeout(() => obj.m(), 0) | удобно, если это одно место | | promise.then(obj.m) | this потерян | .then(obj.m.bind(obj)) или .then(() => obj.m()) | then вызывает как fn() | | arr.map(obj.m) | неверный this | arr.map(obj.m, obj) | используйте thisArg, если он есть | | DOM addEventListener и нужен элемент | this зависит от типа функции | event.currentTarget | работает и со стрелками | | метод класса как обработчик | this потерян | bind в конструкторе или метод-стрелка | выбирайте по стилю и памяти | | Node EventEmitter + чужой метод | this указывает на emitter | user.onPing.bind(user) | EventEmitter задаёт свой this |

    Краткое резюме

  • Потеря this почти всегда означает, что ваш метод был вызван как fn(), а не как obj.fn().
  • Таймеры и промисы обычно вызывают колбэки как обычные функции, поэтому методы нужно оборачивать или привязывать.
  • В DOM событиях this удобен только с обычными функциями, но более надёжно использовать event.currentTarget.
  • В Node.js у EventEmitter this у слушателя часто указывает на emitter, поэтому «чужие» методы почти всегда требуют bind.
  • Из анти-паттернов самые опасные: bind прямо в addEventListener, стрелки как методы объекта, и надежда на глобальный this.