Функцию можно привязать к объекту, чтобы у нее всегда был один и тот же 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)