Функцию можно привязать к объекту, чтобы у нее всегда был один и тот же this, вне зависимости от контекста вызова.
Это удобно для передачи функции как параметра, чтобы не передавать вместе с ней ссылку на объект.
Для примера, рассмотрим потерю контекста, с которой мы сталкивались в главе про таймеры: вызовsetTimeout(user.sayHi, 1000) запустит функцию user.sayHi в глобальном контексте, не сохраняетthis:
Проблема, конечно, не только в конкретном свойстве id, а в том, что, так как не передается this, из метода нельзя обратиться к другим свойствам объекта.
Самый простой способ это обойти — сделать вызов через обертку:
2 |
setTimeout(function() { |
Но неужели это единственное решение? Что же теперь — при каждом подобном вызове оборачиватьuser.sayHi?
..Конечно, нет. Можно привязать контекст к функции, так что он будет всегда фиксирован. А как — мы сейчас увидим
Привязка через замыкание
Самый простой способ привязать функцию к правильному this — это… Не использовать this!
Например, обращаться из функции к объекту через замыкание:
Так как функция была «отучена» от this, то ее можно смело передавать куда угодно. Контекст будет правильным.
Современный метод bind
В современном JavaScript для привязки функций есть метод bind. Он поддерживается большинством современных браузеров, за исключением IE<9, но легко эмулируется.
Этот метод позволяет привязать функцию к нужному контексту и даже к аргументам.
Синтаксис bind:
var wrapper = func.bind(context[, arg1, arg2...]) |
func- Произвольная функция
wrapper- Функция-обертка, которую возвращает вызов
bind. Она вызывает func, фиксируя контекст и, если указаны, первые аргументы. context- Обертка
wrapper будет вызывать функцию с контекстом this = context. arg1, arg2, …- Если указаны аргументы
arg1, arg2... — они будут прибавлены к каждому вызову новой функции, причем встанут перед теми, которые указаны при вызове.
Простейший пример, фиксируем только this:
5 |
var user = { name: "Вася" }; |
Использование в конструкторе, для привязки метода sayHi к создаваемому объекту:
Важность: 4
Ответ: "Вася".
Первый вызов f.bind(..Вася..) возвращает «обертку», которая устанавливает контекст для f и передает вызов f.
Следующий вызов bind будет устанавливать контекст уже для этой обертки, это ни на что не влияет.
Чтобы это проще понять, используем наш собственный вариант bind вместо встроенного:
1 |
function bind(func, context) { |
3 |
return func.apply(context, arguments); |
Код станет таким:
5 |
f = bind(f, {name: "Вася"} ); |
6 |
f = bind(f, {name: "Петя"} ); |
Здесь видно, что первый вызов bind, в строке (1), возвращает обертку вокруг f, которая выглядит так (выделена):
1 |
function bind(func, context) { |
3 |
return func.apply(context, arguments); |
В этой обертке нигде не используется this, только func и context. Об этом говорит сайт https://intellect.icu . Посмотрите на код, там нигде нет this.
Поэтому следующий bind в строке (2), который выполняется уже над оберткой и фиксирует в ней this, ни на что не влияет. Какая разница, что будет в качестве thisв функции, которая этот this не использует?
[Открыть задачу в новом окне]
bind с аргументами
Метод bind может создавать обертку, которая фиксирует не только контекст, но и ряд аргументов.
Например, есть функция перемножения mul(a, b):
На ее основе мы можем создать функцию double, которая будет удваивать значения:
6 |
var double = mul.bind(null, 2); |
Вызов mul.bind(null, 2) возвратил обертку, которая фиксирует контекст this = null и первый аргумент 2. Контекст в функциях не используется, поэтому не важно, чему он равен.
Получилась функция double = mul(2, *).
Так же можно создать triple, утраивающую значение:
var triple = mul.bind(null, 3); |
Создание новой функции путем фиксирования аргументов существующей «научно» называется карринг.
Для полноты картины рассмотрим сочетание обеих привязок: контекста и аргументов.
Пусть у объектов User есть метод send(to, message), который умеет посылать пользователю toсообщение message. Создадим функцию для отсылки сообщений Пете от Васи. Для этого нужно зафиксировать контекст и первый аргумент в send:
Зачем такая функция может быть нужна? Ну, например, для того чтобы передать ее в setTimeout или любое другое место программы, где может быть и про пользователей ничего не знают, а нужна какая-нибудь функция одного аргумента для сообщений. И такая вполне подойдет.
Кросс-браузерная эмуляция bind
Для IE<9 и старых версий других браузеров, которые не поддерживают bind, его можно реализовать самостоятельно.
Без поддержки карринга это очень просто.
Вот наша собственная функция привязки bind:
1 |
function bind(func, context) { |
3 |
return func.apply(context, arguments); |
Ее использование:
Вариант bind c каррингом
Чтобы функция bind передавала аргументы, ее нужно «слегка» усложнить:
1 |
function bind(func, context ) { |
2 |
var bindArgs = [].slice.call(arguments, 2); |
4 |
var args = [].slice.call(arguments); |
5 |
var unshiftArgs = bindArgs.concat(args); |
6 |
return func.apply(context, unshiftArgs); |
Страшновато выглядит, да?
Если интересно, работает так (по строкам):
- Вызов
bind сохраняет дополнительные аргументы args (они идут со 2го номера) в массивbindArgs.
- … и возвращает обертку
wrapper.
- Эта обертка делает из
arguments массив args и затем, используя метод concat, прибавляет их к аргументам bindArgs (3).
- Затем передает вызов
func (4).
Использование — в точности, как в примере выше, только вместо send.bind(admin, visitor)вызываем bind(send, admin, visitor).
Вариант bind для методов
Предыдущие версии bind привязывают любую функцию к любому объекту.
Но если нужно привязать к объекту не произвольную функцию, а ту, которая уже есть в объекте, т.е. его метод, то синтаксис можно упростить.
Обычный вызов bind:
var userMethod = bind(user.method, user); |
Альтернативный синтаксис:
var userMethod = bind(user, 'method'); |
Поддержка этого синтаксиса легко встраивается в обычный bind. Для этого достаточно проверять типы первых аргументов:
Раскрыть код расширенного bind
Во фреймворках, как правило, есть свои методы привязок. Например, в jQuery это $.proxy, который работает как описано ранее:
var userMethod = $.proxy(user.method, user); |
var userMethod = $.proxy(user, 'method'); |
…С другой стороны, редакторы, которые поддерживают автодополнение, не очень любят такие «оптимизации». Скажем, при попытке автоматизированного переименования method, они смогут найти его в вызове bind(user.method, user), но не смогут в bind(user, 'method').
Итого
Итоговый, укороченный, код bind для привязки функции или метода объекта:
01 |
function bind(func, context ) { |
02 |
var args = [].slice.call(arguments, 2); |
04 |
if (typeof context == "string") { |
05 |
args.unshift( func[context], func ); |
06 |
return bind.apply(this, args); |
10 |
var unshiftArgs = args.concat( [].slice.call(arguments) ); |
11 |
return func.apply(context, unshiftArgs); |
Синтаксис: bind(func, context, аргументы) или bind(obj, 'method', аргументы).
Также можно использовать func.bind из современного JavaScript, при необходимости добавив кросс-браузерную эмуляцию библиотекой es5-shim:
Комментарии
Оставить комментарий
Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)
Термины: Выполнение скриптов на стороне клиента JavaScript, jqvery, JS фреймворки (Frontend)