Привязка контекста к функции
При передаче методов объекта в качестве колбэков, например для setTimeout
, возникает проблема — потеря this
.
Потеря this
Как только метод передается отдельно от объекта — this
теряется.
Вот как это может произойти в случае с setTimeout
:
const user = {
firstname: 'Вася',
sayHi() {
console.log(`Привет, ${this.firstname}!`)
}
}
setTimeout(user.sayHi, 1000); // -> Привет, undefined!
Это произошло потому, что setTimeout
получил функцию sayHi
отдельно от объекта user
(именно здесь функция потеряла
контекст). То есть последняя строка может быть переписана как:
let f = user.sayHi;
setTimeout(f, 1000); // контекст user потерян
Решение 1: сделать функцию-обертку
Самый простой вариант решения — это обернуть вызов в анонимную функцию, создав замыкание:
const user = {
firstname: 'Вася',
sayHi() {
console.log(`Привет, ${this.firstname}!`)
}
}
setTimeout(() => user.sayHi(), 1000); // -> Привет, Вася!
В таком коде есть небольшая уязвимость.
Если до момента срабатывания setTimeout
(задержка 1 секунда) в переменную user
будет записано другое значение, тогда
вызов будет не тот, что ожидался.
let user = {
firstname: 'Вася',
sayHi() {
console.log(`Привет, ${this.firstname}!`)
}
}
setTimeout(() => user.sayHi(), 1000);
// ...в течении 1 секунды
user = {
sayHi() {
console.log('Другой пользователь в "setTimeout"')
}
};
// -> Другой пользователь в "setTimeout"
Следующее решение гарантирует, что такого не случится.
Решение 2: привязать контекст с помощью bind
У функций в JavaScript есть встроенный
метод bind, который
позволяет зафиксировать this
.
Базовый синтаксис bind
:
// полный синтаксис представлен ниже
const boundFunc = func.bind(context);
Результатом вызова func.bind(context)
является необычный функциональный объект, который является оберткой над
исходным функциональным объектом. Вызывается как функция и прозрачно передает вызов в func
, при этом
устанавливая this=context
.
Другими словами, вызов boundFunc
подобен вызову func
с фиксированным this
.
Например, здесь funcUser
передает вызов в func
, фиксируя this=user
:
const user = {
firstname: 'Вася',
}
function func() {
console.log(this.firstname);
}
// привязка this к user
const funcUser = func.bind(user);
funcUser(); // -> Вася
Здесь func.bind(user)
— это «связанный вариант» func
, с фиксированным this=user
.
Все аргументы передаются исходному методу func
как есть, например:
const user = {
firstname: 'Вася',
}
function func(phrase) {
console.log(`${phrase}, ${this.firstname}!`);
}
// привязка this к user
const funcUser = func.bind(user);
funcUser('Привет'); // -> Привет, Вася!
// (аргумент "Привет" передан, при этом this = user)
Теперь то же самое с методом объекта:
const user = {
firstname: 'Вася',
sayHi() {
console.log(`Привет, ${this.firstname}!`)
},
}
const sayHi = user.sayHi.bind(user); // (*)
sayHi(); // -> Привет, Вася!
setTimeout(sayHi, 1000); // -> Привет, Вася!
В строке (*)
мы берем метод user.sayHi и привязываем его к user
. Теперь sayHi
— это «связанная» функция, которая
может быть вызвана отдельно или передана в setTimeout
(контекст всегда будет правильным).
Пример с методом объекта, где bind
исправляет только this
, а аргументы передаются как есть:
const user = {
firstname: 'Вася',
say(phrase) {
console.log(`${phrase}, ${this.firstname}!`)
},
}
const say = user.say.bind(user);
say('Привет'); // -> Привет, Вася! (аргумент "Привет" передан в функцию "say")
say('Пока'); // -> Пока, Вася! (аргумент "Пока" передан в функцию "say")
Частичное применение
До сих пор мы привязывали только this
. Но с помощью метода bind
можно привязывать и аргументы.
Полный синтаксис bind
:
let bound = func.bind(context, [arg1], [arg2], ...);
Это позволяет привязать контекст this
и начальные аргументы функции.
Например, есть функция умножения mul(a, b)
:
function mul(a, b) {
return a * b;
}
Воспользуемся bind
, чтобы создать функцию double
на основе mul()
:
function mul(a, b) {
return a * b;
}
const double = mul.bind(null, 2);
console.log(double(3)); // mul(2, 3) -> 6
console.log(double(4)); // mul(2, 4) -> 8
console.log(double(5)); // mul(2, 5) -> 10
Вызов mul.bind(null, 2)
создает новую функцию double
, которая передает вызов mul
, фиксируя null
как контекст,
и 2
— как первый аргумент. Следующие аргументы передаются как есть.
Это называется частичное применение — мы создаем новую функцию, фиксируя некоторые из существующих параметров.
А также, так как мы не используем this
, но для bind
это обязательный параметр, то необходимо передать null
первым
аргументом.
В следующем коде функция triple
умножает значение на три:
function mul(a, b) {
return a * b;
}
const triple = mul.bind(null, 3);
console.log(triple(3)); // mul(3, 3) -> 9
console.log(triple(4)); // mul(3, 4) -> 12
console.log(triple(5)); // mul(3, 5) -> 15
Частичное применение без контекста
Для ситуаций, когда необходимо зафиксировать некоторые аргументы, но не контекст this
используется вспомогательная
функция partial
, которая привязывает только аргументы.
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// использование:
let user = {
firstname: 'John',
say(time, phrase) {
console.log(`${time}, ${this.firstname}: ${phrase}!`);
}
}
// добавляем частично примененный метод с фиксированным именем
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes())
user.sayNow('Hello'); // -> 10:00, John: Hello!
Результатом вызова partial(func[, arg1, arg2...])
будет обертка (*)
, которая вызывает func
с:
- Тем же
this
, который она получает (для вызоваuser.sayNow
— это будетuser
) - Затем передает ей
...argsBound
— аргументы из вызоваpartial
('10:00'
) - Затем передает ей
...args
— аргументы, полученные оберткой ('Hello'
)
Также есть готовый вариант _.partial из библиотеки lodash.
Итого
Метод bind
возвращает «привязанный вариант» функции func
, фиксируя контекст this
и первые аргументы arg1
, arg2
..., если они заданы.
Обычно bind
применяется для фиксации this
в методе объекта, чтобы передать его в качестве колбэка. Например,
для setTimeout
.
Когда мы привязываем аргументы, такая функция называется «частично примененной» или «частичной».
Частичное применение удобно, когда мы не хотим повторять один и тот же аргумент много раз. Например, если есть
функция send(from, to)
и from
все время будет одинаков для нашей задачи, то мы можем создать частично примененную
функцию и дальше работать с ней.
Конспект статьи из учебника по JavaScript — Привязка контекста к функции