this и контекст вызова
Если замыкания — самая переоценённая «магия» JS у новичков, то this — самая недооценённая. Пока код пишется в объектном стиле, всё выглядит просто: метод — и this — объект. Но стоит передать метод в колбэк, и this «ломается». На собеседованиях это вопрос №1 по языку, а в продакшене — источник багов, которые воспроизводятся только «иногда».
Хорошая новость: механика this предельно детерминирована. В этой главе — пять правил в порядке приоритета, явная привязка, стрелочные функции и то, почему методы классов приходится «биндить».
Главный принцип: this определяется местом ВЫЗОВА
Заголовок раздела «Главный принцип: this определяется местом ВЫЗОВА»Всё, что нужно запомнить: this не имеет отношения к тому, где функция написана. Он зависит исключительно от того, КАК её вызвали. Одна и та же функция может получить разный this при разных вызовах:
function whoAmI() { console.log(this);}
whoAmI(); // глобальный объект (или undefined в strict mode)whoAmI.call({ name: 'объект' }); // { name: 'объект' }
const obj = { method: whoAmI };obj.method(); // obj — вызван как методТеперь пять правил в порядке убывания приоритета. Движок применяет первое подходящее.
Правило 1: Явная привязка — call, apply, bind
Заголовок раздела «Правило 1: Явная привязка — call, apply, bind»Если при вызове используется fn.call(ctx), fn.apply(ctx, args) или fn.bind(ctx), this — переданный контекст. Приоритет выше всех остальных правил.
function introduce(city, hobby) { console.log(`${this.name} из ${city}, увлечение: ${hobby}`);}
const person = { name: 'Влад' };
// call — аргументы спискомintroduce.call(person, 'Ростов', 'DevOps');
// apply — аргументы массивом (полезно, когда аргументы уже в массиве)introduce.apply(person, ['Ростов', 'DevOps']);
// bind — НЕ вызывает, возвращает новую функцию с «прошитым» thisconst introduceVlad = introduce.bind(person);introduceVlad('Москва', 'код');
// bind частичное применение: часть аргументов можно зафиксироватьconst introduceVladRnd = introduce.bind(person, 'Ростов');introduceVladRnd('код'); // «Влад из Ростов, увлечение: код»call и apply эквивалентны, различаются только способом передачи аргументов. bind возвращает новую функцию — это важно для колбэков.
Правило 2: Неявная привязка — вызов метода
Заголовок раздела «Правило 2: Неявная привязка — вызов метода»Если функция вызвана «через точку» — obj.method() — this — объект перед точкой:
const user = { name: 'Аня', greet() { console.log(`Привет, ${this.name}`); },};
user.greet(); // «Привет, Аня» — this === user
const greet = user.greet; // ссылку на функцию можно скопировать!greet(); // «Привет, undefined» — вызов уже НЕ методом, правило не сработалоПоследний случай — потеря контекста: функция та же, но способ вызова изменился. Это и есть главный баг-генератор. Объект перед точкой исчез, движок переходит к следующим правилам.
Правило 3: Конструктор — new
Заголовок раздела «Правило 3: Конструктор — new»Если функция вызвана через new Fn(), создаётся пустой объект, он связывается с this, и, если функция не вернула свой объект, возвращается этот новый объект:
function User(name) { // 1. создаётся this = {} // 2. prototype этого this ссылается на User.prototype this.name = name; // 3. инициализация // 4. return this (неявный)}
const u = new User('Игорь');console.log(u.name); // «Игорь»Правило 4: Дефолтное — глобальный объект или undefined
Заголовок раздела «Правило 4: Дефолтное — глобальный объект или undefined»Если ни одно правило не сработало (обычный вызов fn()), в нестрогом режиме this — глобальный объект (window в браузере, globalThis в Node.js). В строгом режиме ('use strict') — undefined:
function loose() { return this; }function strict() { 'use strict'; return this; }
console.log(loose()); // глобальный объектconsole.log(strict()); // undefined
// Вот почему в strict mode этот код бросает TypeError, а не молча читает window:// function f() { 'use strict'; console.log(this.name); }// f(); // TypeError: Cannot read properties of undefinedМодули (ES-модули) всегда в строгом режиме — поэтому в современном коде «молчаливый глобальный this» почти не встречается.
Правило 5: Лексический this — стрелочные функции
Заголовок раздела «Правило 5: Лексический this — стрелочные функции»Стрелочные функции не имеют собственного this. Они захватывают this из окружения, где были созданы — как обычную переменную через замыкание. Это единственное исключение из правила «this зависит от вызова»:
const team = { name: 'Platform', members: ['Аня', 'Борис'], list() { // Стрелка внутри метода: this взял из list() → team this.members.forEach((m) => { console.log(`${m} из команды ${this.name}`); }); },};
team.list();// «Аня из команды Platform», «Борис из команды Platform»
// С обычной функцией тот же код сломался бы:// forEach(function (m) { ... this.name ... }) — this был бы undefined (strict)Стрелочные функции нельзя использовать как конструктор (new () => {} — SyntaxError) и нельзя перепривязать: bind/call/apply на них не влияют на this (хотя аргументы передать можно):
const arrow = () => console.log(this);arrow.call({ a: 1 }); // не { a: 1 }! this взят из внешнего окруженияПотеря контекста в колбэках: классика багов
Заголовок раздела «Потеря контекста в колбэках: классика багов»Паттерн повторяется бесконечно: метод класса передаётся как обработчик, this теряется:
class Player { constructor(name) { this.name = name; } attack() { console.log(`${this.name} атакует!`); }}
const player = new Player('Геральт');
// Передаём МЕТОД, а не вызов. При клике this будет кнопкой (или undefined)document.querySelector('#attack').addEventListener('click', player.attack);Четыре способа лечения:
// 1. Стрелочный колбэк — this берётся из окружения subscribebutton.addEventListener('click', () => player.attack());
// 2. bind при передаче — создаётся новая привязанная функцияbutton.addEventListener('click', player.attack.bind(player));
// 3. bind в конструкторе — классика React до появления полей классовclass Player { constructor(name) { this.name = name; this.attack = this.attack.bind(this); // attack всегда «свой» } attack() { console.log(`${this.name} атакует!`); }}
// 4. Поле класса со стрелкой (современный синтаксис, babel/TS поддерживают)class Player { name = 'Геральт'; attack = () => { console.log(`${this.name} атакует!`); // this — экземпляр, всегда };}Варианты 3 и 4 — «привязка раз и навсегда»: независимо от того, куда передали метод, this останется экземпляром.
Приоритеты правил на практике: конфликтные случаи
Заголовок раздела «Приоритеты правил на практике: конфликтные случаи»Правила перечислены по приоритету, но что, если два применимы одновременно? Разберём два частых конфликта.
Явная привязка против конструктора. bind выигрывает у new:
function Point(x) { this.x = x; }const BoundPoint = Point.bind({ x: 'захвачен' });
const p = new BoundPoint(5);console.log(p.x); // 5 — new создал СВЕЖИЙ объект, bind-контекст проигнорированЭто редкий случай, но он подтверждает приоритет: конструкторский this (новый объект) важнее захваченного. Спецификация это описывает через [[BoundThis]] игнорирование при конструктивном вызове.
Стрелка против всего. Стрелки игнорируют и call, и new, и точку — их this зафиксирован при создании:
const arrow = () => this;console.log(arrow.call({ a: 1 }) === arrow.apply({ b: 2 })); // true — всегда один и тот же this// new arrow(); // SyntaxError: arrow is not a constructorОтсюда практический вывод для фреймворков: колбэки, определённые стрелками в методах класса, нельзя переопределить извне. Если нужна возможность перепривязки — обычная функция.
Метод, извлечённый дважды. Один и тот же метод может быть привязан к разным объектам независимо — bind создаёт новые функции, не мутируя оригинал:
const userA = { name: 'Аня', greet() { return `Я ${this.name}`; } };const userB = { name: 'Борис' };
const greetA = userA.greet.bind(userA);const greetB = userA.greet.bind(userB);console.log(greetA()); // «Я Аня»console.log(greetB()); // «Я Борис» — та же функция, другой контекстconsole.log(userA.greet()); // «Я Аня» — оригинал не пострадалthis и возвращаемые объекты: тонкость конструктора
Заголовок раздела «this и возвращаемые объекты: тонкость конструктора»Правило 3 (конструктор) имеет исключение: если конструктор явно возвращает объект, new вернёт его вместо свежесозданного this. Возврат примитива игнорируется:
function Custom() { this.plain = true; return { replaced: true }; // объект побеждает this}console.log(new Custom().replaced); // trueconsole.log(new Custom().plain); // undefined
function Primitive() { this.kept = true; return 'строка игнорируется'; // примитив — не в счёт}console.log(new Primitive().kept); // trueЭто легализует паттерн «конструктор-фабрика» (возвращаем кэшированный экземпляр или подтип), но в классах ES6 return объекта запрещён (new Class с return {} бросает TypeError) — классы строже функций-конструкторов.
this в обработчиках DOM и библиотеках
Заголовок раздела «this в обработчиках DOM и библиотеках»DOM задаёт собственные правила: обработчик события вызывается браузером с this === текущий элемент (если это обычная функция):
button.addEventListener('click', function () { console.log(this === button); // true — this = элемент-цель console.log(this.textContent); // текст кнопки});
// Стрелка — this из внешнего окружения, НЕ элемент:button.addEventListener('click', () => { console.log(this); // window/undefined, а не button!});Это часто сюрприз в две стороны: классическая функция получает «лишний» контекст (кнопку вместо своего объекта), стрелка — теряет его. Отсюда практика фреймворков: React синтетические события вообще не полагаются на this DOM-элемента (там своя система компонентов), а jQuery-эпоха строила целые API на $(this).
В современном коде правило простое: обработчики — стрелки или привязанные методы, а DOM-элемент бери из event.currentTarget, а не из this:
button.addEventListener('click', (event) => { const btn = event.currentTarget; // явно, независимо от вида функции btn.disabled = true;});event.currentTarget — элемент, на котором сработал слушатель; event.target — элемент, где событие возникло (может быть вложенным). Путать их — частая причина «не тот this/не тот элемент».
bind против стрелок: какую привязку выбрать
Заголовок раздела «bind против стрелок: какую привязку выбрать»Оба инструмента решают одну задачу — стабильный контекст. Различия в деталях, которые важны при проектировании API:
class Emitter { constructor() { this.events = new Map(); // Привязка в конструкторе: метод можно сравнивать по ссылке this.dispatch = this.dispatch.bind(this); } dispatch(event, payload) { /* ... */ }}
// Экземплярная стрелка: НОВАЯ функция на каждый экземпляр,// между собой не равны даже у одного классаclass Button { handleClick = () => { /* ... */ };}
const b = new Button();const c = new Button();console.log(b.handleClick === c.handleClick); // false — каждому экземпляру свояРазличия в таблице:
| Критерий | bind в конструкторе | Поле-стрелка класса |
|---|---|---|
| Функция | одна на все экземпляры | своя на каждый экземпляр |
| Сравнение по ссылке | a.dispatch === b.dispatch — true |
false |
| Наследование | переопределяется вручную | наследуется как поле |
| Подходит для | методов, идентичность важна | обработчиков, интроспекция |
Когда сравнение по ссылке важно? Отписка от событий (removeEventListener требует ту же функцию), мемоизация методов, тесты с mock-функциями. В этих случаях bind предпочтительнее: одна стабильная функция на класс.
Когда без разницы — короткие обработчики в компонентах — поле-стрелка читается лучше: вся логика рядом с полями, без конструкторной церемонии.
Есть и третий путь — не привязывать вообще, а передавать вызов: onClick={() => btn.handleClick()}. Минус: новая функция-обёртка на каждый рендер (в React — лишние ре-рендеры детей, если они мемоизированы). Для критичных мест — привязка, для остальных — обёртки допустимы.
Привязка методов классов: почему это вообще нужно
Заголовок раздела «Привязка методов классов: почему это вообще нужно»В других языках (Java, C#) методы привязаны к объекту автоматически — ссылка на метод и есть «метод объекта». В JS функция — самостоятельное значение, и метод — лишь «функция, вызванная через точку». Это даёт гибкость (переиспользование, высшие функции), но ценой обязанности следить за this.
Правила гигиены:
- Публичные методы, передаваемые наружу (обработчики, колбэки API), привязывай в конструкторе или делай полями-стрелками.
- Внутренние вызовы
this.helper()внутри класса — безопасны: вызов методом. - Библиотечные функции высшего порядка (
forEach,addEventListener, промисы) обычно вызывают колбэки «голыми» — будь готов.
Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»const f = obj.method; f()— потеря контекста. Лечитсяbind, стрелкой при передаче или полем-стрелкой.- Вложенная обычная функция внутри метода теряет
this. В строгом режиме —undefined. Лечится стрелкой или сохранениемconst self = this(устаревший стиль). - Ожидать, что
bindизменит существующую функцию.bindвозвращает новую; исходная не меняется. - Стрелка как метод объекта или прототипа.
thisбудет не тем объектом. Методы — обычные функции. - Отладка через
console.log(this)внутри колбэка, где ожидался объект. В строгом режиме этоundefined, а не объект перед точкой где-то в другом месте. Проверяй способ вызова, а не место определения. - Двойная привязка в наследовании:
super.method()в стрелке теряетthisподкласса — стрелка захватываетthisлексически, аsuperищется по[[HomeObject]], тонкости есть, но на практике: методы сsuper— обычные функции.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»- Чем определяется значение
this? Только способом вызова. Пять правил по приоритету: явная привязка (call/apply/bind) → метод (точка) → конструктор (new) → дефолт (глобал/undefined) → лексический (стрелки не имеют своего this). - Разница между
call,apply,bind?call(ctx, ...args)иapply(ctx, argsArray)— немедленный вызов, различие в передаче аргументов.bind(ctx, ...args)— возвращает новую функцию без вызова. - Почему стрелочные функции нельзя использовать как методы?
У них нет собственного
this— он захватывается из внешнего лексического окружения, а не из объекта перед точкой. - Как исправить потерю контекста при передаче метода в колбэк? Назови три способа.
Стрелка-обёртка при передаче,
bindпри передаче, привязка в конструкторе (или поле-стрелка класса). - Что выведет код?
const obj = { n: 1, getN: function () { return this.n; } };const fn = obj.getN;console.log(obj.getN(), fn.call(obj), fn());
1 1 undefined (strict)— первые два вызова методом/через call, третий — потеря контекста. - Можно ли перепривязать
thisу стрелочной функции черезbind? Нет.bindу стрелки влияет только на аргументы (при частичном применении),thisостаётся лексическим.
Практика
Заголовок раздела «Практика»- Напиши полифил
myBind(fn, ctx, ...presetArgs): возвращает функцию, при вызове которойfnвыполняется сthis === ctxи аргументами presetArgs + новыми. Критерий: проверь с конструктором (new) — корректный полифил должен сохранять поведениеnew(возможно упрощённо, верни осознанное решение). - Реализуй
pipe(...fns)— композицию функций слева направо черезreduce. Критерий:pipe(add, square)(2)===square(add(2)); сделай так, чтобы внутри каждой функцииthisпробрасывался (используйfn.call(this, ...)). - Найди и исправь баг: в классе
Timerметодtickпередаётся вsetIntervalи падает сTypeError(чтениеthis.seconds). Исправь тремя способами из главы. - Напиши
memoize(fn, ctx): мемоизатор, который при повторном вызове возвращает закэшированный результат, аthisвнутриfn— всегда переданныйctx. Критерий: метод объекта, обёрнутый вmemoize, может передаваться куда угодно без потери контекста. - Создай иерархию:
class Animalс методомdescribe()(используетthis.name),class Dog extends Animalсdescribeчерезsuper. Проверь, что при передачеdog.describeв колбэк и привязке контекст сохраняется, и объясни, где именно движок берётsuper-метод (по[[HomeObject]]).