← Назад к вопросам

Как изменить контекст в JavaScript?

2.0 Middle🔥 191 комментариев
#Node.js и JavaScript#Фреймворки и библиотеки

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Изменение контекста в JavaScript

Контекст (context) в JavaScript — это значение this, которое определяет, какой объект используется в качестве контекста выполнения функции. Изменение контекста — это основная техника для правильного использования this.

Что такое контекст (this)?

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

const user = {
  name: 'John',
  greet: function() {
    console.log(`Hello, I'm ${this.name}`); // this = user
  }
};

user.greet(); // "Hello, I'm John"

const greetFunc = user.greet;
greetFunc(); // "Hello, I'm undefined" — потеряли контекст!

Три основных способа изменения контекста

1. Метод call()

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

function introduce(title, city) {
  console.log(`${this.name} is a ${title} from ${city}`);
}

const person = { name: 'Alice' };

// Вызываем функцию с контекстом person
introduce.call(person, 'Developer', 'New York');
// "Alice is a Developer from New York"

Практический пример в backend:

class UserService {
  async getUser(id: number) {
    return db.query('SELECT * FROM users WHERE id = @id', { id });
  }
}

class AdminService {
  // Переиспользуем метод из UserService с другим контекстом
  getAdminUser(id: number) {
    return UserService.prototype.getUser.call(this, id);
  }
}

2. Метод apply()

apply похож на call(), но аргументы передаются как массив.

function introduce(title, city) {
  console.log(`${this.name} is a ${title} from ${city}`);
}

const person = { name: 'Bob' };
const args = ['Engineer', 'London'];

// Вызываем с массивом аргументов
introduce.apply(person, args);
// "Bob is a Engineer from London"

Типичный use case — применение max/min на массив:

const numbers = [5, 2, 8, 1, 9];

const max = Math.max.apply(null, numbers);
// Эквивалент: Math.max(...numbers)

// Или с контекстом
const calculator = {
  numbers: [1, 2, 3, 4, 5],
  getMax() {
    return Math.max.apply(null, this.numbers);
  }
};

calculator.getMax(); // 5

3. Метод bind()

bind создаёт новую функцию с зафиксированным контекстом. Важно: он не вызывает функцию, а возвращает новую.

const user = {
  name: 'Charlie',
  greet: function() {
    return `Hello from ${this.name}`;
  }
};

// Создаём новую функцию с зафиксированным контекстом
const boundGreet = user.greet.bind(user);

// Теперь можем передавать функцию куда угодно
setTimeout(boundGreet, 1000); // "Hello from Charlie"

// Без bind потеряю контекст
setTimeout(user.greet, 1000); // "Hello from undefined"

Где это используется в backend?

1. Обработчики событий в EventEmitter

class UserRepository extends EventEmitter {
  private db: Database;

  constructor(db: Database) {
    super();
    this.db = db;
  }

  async createUser(data: any) {
    const user = await this.db.create('users', data);
    // this.emit вызывает коллбек с неправильным контекстом
    this.emit('user:created', user);
    return user;
  }
}

// Правильное использование bind
const repo = new UserRepository(db);
repo.on('user:created', repo.sendWelcomeEmail.bind(repo));

2. Express middleware и коллбеки

class AuthController {
  constructor(private userService: UserService) {}

  async login(req, res) {
    // this.userService доступен
    const user = await this.userService.findByEmail(req.body.email);
    res.json(user);
  }

  // Если использовать как callback, потеряем контекст
  logout = (req: Request, res: Response) => {
    // Arrow function автоматически сохраняет контекст
    const userId = this.getUserId();
    res.json({ success: true });
  };
}

// Использование
const controller = new AuthController(userService);
router.post('/login', (req, res) => controller.login(req, res)); // Передаём функцию
router.post('/logout', controller.logout); // Arrow function уже имеет контекст

3. Работа с Promise и setTimeout

class DataProcessor {
  private timeout = 5000;

  processAsync() {
    return fetch('/api/data')
      .then(this.handleSuccess.bind(this)) // Зафиксируем контекст
      .catch(this.handleError.bind(this));
  }

  private handleSuccess(response) {
    // this = DataProcessor
    console.log(this.timeout);
  }

  private handleError(error) {
    // this = DataProcessor
    logger.error(error);
  }
}

// Или через arrow function (современный подход)
class DataProcessor {
  private timeout = 5000;

  processAsync() {
    return fetch('/api/data')
      .then((response) => this.handleSuccess(response)) // Arrow function
      .catch((error) => this.handleError(error));
  }
}

Практические примеры

Пример 1: call() для наследования методов

// Класс Animal
class Animal {
  constructor(public name: string) {}
  
  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

// Класс Dog
class Dog extends Animal {
  speak() {
    // Вызываем метод родителя, но с контекстом Dog
    Animal.prototype.speak.call(this);
    console.log(`${this.name} barks`);
  }
}

const dog = new Dog('Buddy');
dog.speak();
// "Buddy makes a sound"
// "Buddy barks"

Пример 2: apply() для логирования

class Logger {
  static wrapMethod(obj: any, methodName: string) {
    const original = obj[methodName];
    
    obj[methodName] = function(...args: any[]) {
      console.log(`Calling ${methodName} with args:`, args);
      const result = original.apply(this, args); // Вызываем оригинальный метод с его контекстом
      console.log(`${methodName} returned:`, result);
      return result;
    };
  }
}

class UserService {
  getUser(id: number) {
    return { id, name: 'John' };
  }
}

const service = new UserService();
Logger.wrapMethod(service, 'getUser');

service.getUser(1); // Выведет логи до и после вызова

Пример 3: bind() для обработчиков событий

class NotificationService extends EventEmitter {
  constructor(private emailService: EmailService) {
    super();
    // Зафиксируем контекст для обработчиков
    this.on('user:registered', this.onUserRegistered.bind(this));
    this.on('user:deleted', this.onUserDeleted.bind(this));
  }

  private onUserRegistered = async (user: User) => {
    // this.emailService доступен
    await this.emailService.sendWelcome(user.email);
  };

  private onUserDeleted = async (user: User) => {
    // this.emailService доступен
    await this.emailService.sendGoodbye(user.email);
  };
}

Сравнение методов

function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Diana' };

// call — вызывает сразу с контекстом
greet.call(person, 'Hello', '!');
// "Hello, Diana!"

// apply — вызывает сразу, аргументы в массиве
greet.apply(person, ['Hi', '?']);
// "Hi, Diana?"

// bind — возвращает новую функцию
const boundGreet = greet.bind(person, 'Hey');
boundGreet('...'); // "Hey, Diana..."

// bind с частичным применением
const heyDiana = greet.bind(person, 'Hey');
setTimeout(() => heyDiana('!'), 1000); // Через 1 сек: "Hey, Diana!"

Современный подход: Arrow Functions

В современном JavaScript предпочитают arrow functions, которые наследуют контекст из внешней области видимости.

class UserController {
  constructor(private userService: UserService) {}

  // Старый способ — нужен bind
  async getUser(req, res) {
    const user = await this.userService.find(req.params.id);
    res.json(user);
  }

  // Новый способ — public arrow field
  getUser = async (req: Request, res: Response) => {
    // this автоматически = UserController
    const user = await this.userService.find(req.params.id);
    res.json(user);
  };
}

// Использование
const controller = new UserController(userService);
router.get('/users/:id', controller.getUser); // Работает без bind!

Золотые правила

  1. call() — когда нужно вызвать функцию с специфичным контекстом один раз
  2. apply() — когда аргументы в массиве или неизвестное количество
  3. bind() — когда нужно сохранить функцию с контекстом для использования позже
  4. Arrow functions — предпочитай arrow functions в классах для автоматического сохранения контекста

Понимание контекста — ключ к написанию надёжного JavaScript и Node.js кода без скрытых ошибок.

Как изменить контекст в JavaScript? | PrepBro