Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Связывание (bind) JS: Введение в ООП

JavaScript — асинхронный язык программирования. Из-за этого функции часто вызываются как колбеки других функций. Особенно много этого в браузере, где колбек на колбеке и колбеком погоняет. Пока мы работали с простыми функциями, это не вызывало никаких затруднений, но все меняется при использовании методов.

На этом этапе мы еще не работали с асинхронностью, но это не должно помешать понять идею. В двух словах: функция setTimeout принимает на вход функцию и время, после которого ее надо вызвать. Когда приходит время, она это делает. В общем-то и всё.

Попробуйте запустить такой код:

const printer = {
  name: 'Hexlet',
  print(greeting = 'hello') {
    console.log(`${greeting}, ${this.name}`);
  }
};

// Прямой запуск
printer.print(); // => "hello, Hexlet"

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

// Хотим запустить метод print через секунду
// Обязательно запустите этот код на своем компьютере
// чтобы почувствовать то как работает setTimeout
// 1000 - означает 1000 миллисекунд или 1 секунда
// printer.print - это не вызов, а передача функции
setTimeout(printer.print, 1000);

// Спустя секунду
// => "hello, undefined"

Этот код выдаст hello, undefined. Почему? Потому что внутрь setTimeout() мы передали не объект printer, а функцию print() без объекта. А значит эта функция потеряла связь с самим объектом и ее this больше не указывает на объект. Вот так можно проиллюстрировать то, что происходит:

const print = printer.print;
// Где-то внутри setTimeout
print(); // => "hello, undefined"

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

Такое поведение часто нежелательно. Практически всегда, когда передается метод, подразумевается, что он будет вызван в контексте того объекта, которому он принадлежит. Существует несколько способов добиться такого поведения. Самый простой — обернуть функцию в функцию, пока мы вызываем функцию.

setTimeout(() => printer.print(), 1000);
// Спустя секунду
// => "hello, Hexlet"

// Или без setTimeout
const fn = () => printer.print();
// Все работает потому что print() вызывается из printer
fn(); // => "hello, Hexlet"

Это распространенное решение, которое заодно помогает захватить внешние переменные, когда они нужны для вызова:

// Оборачивание в функцию помогает передать какие-то данные внутрь
const value = 'hi';
setTimeout(() => printer.print(value), 1000);
// => "hi, Hexlet"

Связывание (Bind)

Другой способ — использование метода bind() (переводится как связать). Метод bind() доступен у функций, и в его задачу входит связывание функции с каким-то контекстом. Результатом выполнения bind() будет новая функция, работающая как и исходная функция, но с привязанным к ней контекстом.

// Контекстом является тот же объект printer, в котором определен метод
// Это довольно странно выглядит, но жизнь — сложная штука
// bind вызывается на функции и возвращает функцию
const boundPrint = printer.print.bind(printer);

// Теперь можно так
boundPrint(); // => "hello, Hexlet"
setTimeout(boundPrint, 1000);
// Через секунду
// => "hello, Hexlet"

// Можно вызывать bind прямо по месту
// так как возвращается функция
setTimeout(printer.print.bind(printer), 1000);
// hello, Hexlet

Связанная функция сливается со своим контекстом "намертво". Больше this не поменяется.

Кроме контекста, bind() принимает на вход параметры, которые нужны функции. Причем не сразу все, а любую их часть. bind() подставит их в новую функцию (ту, что вернется из метода bind()) "частично". Эта техника называется "частичное применение функции". Так можно сразу применить нужные аргументы:

setTimeout(printer.print.bind(printer, 'hi'), 1000);
// Через секунду
// => "hi, Hexlet"

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

Apply & Call

bind() полезен там, где привязка контекста и вызов функции происходят в разных местах и, как правило, в разное время. Мы встретимся с таким кодом, когда перейдем к асинхронности в JavaScript.

Иногда вызов функций использующих внутри себя this происходит сразу вместе с привязкой контекста. Это можно сделать напрямую, сразу же вызвав функцию, возвращаемую bind: ...bind(/* контекст */)():

print.bind(printer)('hi'); // => "hi, Hexlet"

а можно использовать специально созданные для этого функции apply() и call():

// func.apply(thisArg, [ argsArray])
const print = printer.print;
print.apply(printer, ['hi']); // hi, Hexlet

// func.call([thisArg[, arg1, arg2, ...argN]])
print.call(printer, 'hi'); // hi, Hexlet

Эти функции внутри себя делают две вещи: меняют контекст и сразу же производят вызов функции. Разница лишь в том, как они работают с аргументами этих функций: apply() – принимает аргументы в виде массива вторым параметром, а call() ждёт на вход позиционные аргументы.

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

// Если контекста нет, то передают null
const numbers = [1, 10, 33, 9, 15]
const max = Math.max.apply(null, numbers); // 33

const numbers = [1, 10, 33, 9, 15]
const max = Math.max.call(null, ...numbers); // 33

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


Дополнительные материалы

  1. apply()

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
30 июня 10 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
30 июня 10 месяцев
Иконка программы Fullstack-разработчик
Профессия
Новый
Разработка фронтенд и бэкенд компонентов веб-приложений
30 июня 16 месяцев

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»