Якорь в реальности: Всё о привязке контекста this в JavaScript (и почему он вечно теряется)
Вы когда-нибудь теряли ключи в собственной квартире? Вроде только что держали в руках, а через секунду - исчезли. И вы ходите, ищете, а они лежат на самом видном месте, просто вы смотрели не туда.
С this в JavaScript та же история. Вы только что им пользовались, всё работало. А потом передали метод в колбэк - и this потерялся. И вы ищете, отлаживаете, добавляете console.log, а проблема в том, что функция забыла, кому она принадлежит.
Сегодня мы разберёмся, почему this теряется, как его привязать намертво и какие есть способы зафиксировать контекст. Это знание превратит хаос в порядок, а ошибки undefined is not a function - в понимание.
Часть 1. Почему this теряется? (Короткая история)
В JavaScript this определяется в момент вызова функции, а не в момент её создания. Это ключевое отличие от многих других языков.
javascript
const user = {
name: "Анна",
greet() {
console.log(`Привет, ${this.name}`);
}
};
user.greet(); // "Привет, Анна" (this = user)
const greetFunc = user.greet;
greetFunc(); // "Привет, undefined" (this = window/undefined)
Что произошло? Когда мы вызвали greetFunc() как обычную функцию, она потеряла связь с объектом user. this стал глобальным объектом (или undefined в строгом режиме).
Часть 2. Способы привязки контекста
2.1 bind - постоянная привязка (навсегда)
bind создаёт новую функцию, у которой this навсегда привязан к указанному объекту.
javascript
const user = { name: "Анна" };
function greet() {
console.log(`Привет, ${this.name}`);
}
const boundGreet = greet.bind(user);
boundGreet(); // "Привет, Анна"
// Даже если попытаться переопределить через call/apply, не получится
boundGreet.call({ name: "Борис" }); // "Привет, Анна" (не изменилось!)
bind с аргументами (частичное применение):
javascript
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2); // привязываем первый аргумент
console.log(double(5)); // 10
console.log(double(10)); // 20
const triple = multiply.bind(null, 3);
console.log(triple(5)); // 15
2.2 call и apply - временная привязка (один раз)
call и apply вызывают функцию сразу с указанным this. Они не создают новую функцию.
javascript
const user1 = { name: "Анна" };
const user2 = { name: "Борис" };
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
// call — аргументы через запятую
greet.call(user1, "Привет"); // "Привет, Анна"
// apply - аргументы массивом
greet.apply(user2, ["Здравствуйте"]); // "Здравствуйте, Борис"
2.3 Стрелочные функции - лексический this
Стрелочные функции не имеют своего this. Они берут его из внешнего контекста.
javascript
const user = {
name: "Анна",
greet: () => {
console.log(`Привет, ${this.name}`); // this = глобальный объект
}
};
user.greet(); // "Привет, undefined"
// А вот так работает (метод + стрелка внутри)
const user2 = {
name: "Анна",
greet() {
setTimeout(() => {
console.log(`Привет, ${this.name}`); // this = user2
}, 100);
}
};
user2.greet(); // "Привет, Анна" (через 100мс)
Часть 3. Классические сценарии потери контекста
3.1 Передача метода как колбэка
javascript
const user = {
name: "Анна",
greet() {
console.log(`Привет, ${this.name}`);
}
};
// Проблема
setTimeout(user.greet, 1000); // "Привет, undefined"
// Решения
setTimeout(user.greet.bind(user), 1000); // bind
setTimeout(() => user.greet(), 1000); // стрелка
setTimeout(function() { user.greet(); }, 1000); // обёртка
3.2 Обработчики событий
javascript
class Button {
constructor(label) {
this.label = label;
}
handleClick() {
console.log(`Нажата кнопка ${this.label}`);
}
}
const btn = new Button("Отправить");
document.getElementById("myBtn").addEventListener("click", btn.handleClick);
// "Нажата кнопка undefined" (this = элемент DOM)
// Решения
document.getElementById("myBtn").addEventListener("click", btn.handleClick.bind(btn));
document.getElementById("myBtn").addEventListener("click", () => btn.handleClick());
3.3 Деструктуризация
javascript
const user = {
name: "Анна",
greet() {
console.log(`Привет, ${this.name}`);
}
};
const { greet } = user;
greet(); // "Привет, undefined" (потеряли контекст)
// Решение
const boundGreet = user.greet.bind(user);
boundGreet();
Часть 4. Привязка контекста в классах
4.1 Классический способ (bind в конструкторе)
javascript
class User {
constructor(name) {
this.name = name;
this.greet = this.greet.bind(this); // привязываем в конструкторе
}
greet() {
console.log(`Привет, ${this.name}`);
}
}
const user = new User("Анна");
const greet = user.greet;
greet(); // "Привет, Анна" (работает!)
4.2 Стрелочные методы (современный способ)
javascript
class User {
constructor(name) {
this.name = name;
}
greet = () => {
console.log(`Привет, ${this.name}`);
}
}
const user = new User("Анна");
const greet = user.greet;
greet(); // "Привет, Анна" (работает!)
4.3 Публичные поля (аналогично стрелкам)
javascript
class User {
name;
constructor(name) {
this.name = name;
}
greet = function() {
console.log(`Привет, ${this.name}`);
}.bind(this); // тоже работает
}
Часть 5. bind vs стрелочная функция: что выбрать?
javascript
// bind полезен для частичного применения
const add = (a, b) => a + b;
const add5 = add.bind(null, 5);
console.log(add5(3)); // 8
// Стрелка не умеет привязывать аргументы
// const add5 = (b) => add(5, b); // приходится писать обёртку
Часть 6. Частичное применение (каррирование) через bind
javascript
function formatDate(format, date) {
// форматирует дату
return `${format}: ${date.toISOString()}`;
}
const yyyymmdd = formatDate.bind(null, "YYYY-MM-DD");
const timestamp = formatDate.bind(null, "TIMESTAMP");
console.log(yyyymmdd(new Date())); // "YYYY-MM-DD: 2024-01-15..."
console.log(timestamp(new Date())); // "TIMESTAMP: 2024-01-15..."
// Множественное частичное применение
function multiply(a, b, c) {
return a * b * c;
}
const multiplyBy2 = multiply.bind(null, 2);
const multiplyBy2And3 = multiplyBy2.bind(null, 3);
console.log(multiplyBy2And3(5)); // 2 * 3 * 5 = 30
Часть 7. Привязка к null или undefined
При привязке к null или undefined, this в нестрогом режиме заменяется на глобальный объект. В строгом режиме остаётся null/undefined.
javascript
"use strict";
function test() {
console.log(this);
}
const bound = test.bind(null);
bound(); // null (в строгом режиме)
// В нестрогом режиме было бы window
Часть 8. Реализация bind (полифилл для понимания)
javascript
function myBind(fn, context, ...boundArgs) {
return function(...callArgs) {
return fn.apply(context, [...boundArgs, ...callArgs]);
};
}
const user = { name: "Анна" };
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const boundGreet = myBind(greet, user, "Привет");
boundGreet("!"); // "Привет, Анна!"
Часть 9. Реальные кейсы
9.1 Фабрика функций с предустановленными параметрами
javascript
function createApiRequest(baseUrl, endpoint, params) {
return fetch(`${baseUrl}/${endpoint}?${new URLSearchParams(params)}`);
}
const apiV1 = createApiRequest.bind(null, "https://api.example.com/v1");
const getUsers = apiV1.bind(null, "users");
const getProducts = apiV1.bind(null, "products");
getUsers({ limit: 10 }).then(res => res.json());
getProducts({ category: "electronics" }).then(res => res.json());
9.2 Безопасная передача методов в асинхронный код
javascript
class DataProcessor {
constructor(data) {
this.data = data;
this.process = this.process.bind(this);
}
process(callback) {
// Асинхронная обработка
setTimeout(() => {
const result = this.data.map(x => x * 2);
callback(result);
}, 100);
}
}
const processor = new DataProcessor([1, 2, 3]);
// Можно безопасно передавать метод
setTimeout(processor.process, 1000, (result) => {
console.log(result); // [2, 4, 6]
});
9.3 Создание "умных" сеттеров
javascript
class Config {
constructor() {
this.settings = {};
this.set = this.set.bind(this);
}
set(key, value) {
this.settings[key] = value;
return this; // для цепочек
}
get(key) {
return this.settings[key];
}
}
const config = new Config();
const setter = config.set; // можно передать куда угодно
setter("theme", "dark");
setter("language", "ru");
console.log(config.get("theme")); // "dark"
Часть 10. Подводные камни
Камень #1: bind нельзя переопределить
javascript
const obj = { name: "Анна" };
function greet() { console.log(this.name); }
const bound = greet.bind(obj);
const rebound = bound.bind({ name: "Борис" });
rebound(); // "Анна" (первый bind сильнее)
Камень #2: Стрелочные функции нельзя привязать
javascript
const arrow = () => console.log(this);
const bound = arrow.bind({ name: "Анна" });
bound(); // window (не изменилось!)
Камень #3: Потеря контекста в цепочке промисов
javascript
class API {
constructor() {
this.baseUrl = "https://api.example.com";
}
fetch(endpoint) {
return fetch(`${this.baseUrl}/${endpoint}`)
.then(this.handleResponse); // ❌ потеря this
}
handleResponse(response) {
console.log(this.baseUrl); // undefined
return response.json();
}
}
// Исправление
fetch(endpoint) {
return fetch(`${this.baseUrl}/${endpoint}`)
.then(this.handleResponse.bind(this));
}
// Или через стрелку
fetch(endpoint) {
return fetch(`${this.baseUrl}/${endpoint}`)
.then(res => this.handleResponse(res));
}
Часть 11. Сравнение методов привязки
Итог: Манифест привязки контекста
- this теряется - при передаче метода как колбэка, в таймерах, в обработчиках событий.
- bind - создаёт новую функцию с навсегда привязанным this.
- call / apply - вызывают функцию один раз с указанным this.
- Стрелочные функции - не имеют своего this, берут из внешнего контекста.
- В классах - используйте bind в конструкторе или стрелочные поля.
- Частичное применение - bind позволяет фиксировать аргументы.
- bind нельзя переопределить - привязка работает навсегда.
Финальный тест (что выведет?):
javascript
const obj = {
name: "Анна",
getName() {
return this.name;
}
};
const fn1 = obj.getName;
const fn2 = obj.getName.bind(obj);
const fn3 = () => obj.getName();
console.log(fn1());
console.log(fn2());
console.log(fn3());
const bound = obj.getName.bind({ name: "Борис" });
console.log(bound());
const obj2 = { name: "Вика", getName: bound };
console.log(obj2.getName());
Ответы: undefined, "Анна", "Анна", "Борис", "Борис" (bind сильнее вызова через объект).
Привязка контекста - это навык, который отличает джуниора от мидла. Когда вы перестаёте гадать, почему this вдруг стал undefined, и начинаете осознанно управлять контекстом, код становится предсказуемым и надёжным. Используйте bind, call, apply и стрелочные функции с умом, и ваш this всегда будет там, где нужно.