- Привязка через замыкание
- Современный метод
bind - Кросс-браузерная эмуляция
bind - Позднее связывание для методов
- Итого
Функцию можно привязать к объекту, чтобы у неё всегда был один и тот же this, вне зависимости от контекста вызова.
Это удобно для передачи функции как параметра, чтобы не передавать вместе с ней ссылку на объект.
Кроме того, можно создать обёртку вокруг неё, которая фиксирует ряд аргументов. Это называется карринг.
Проиллюстрируем проблему, связанную с потерей контекста, на примере.
Вызов setTimeout(user.sayHi, 1000) запустит функцию user.sayHi вызовется в глобальном контексте, не передав this. Мы уже сталкивались с этим в главе про таймеры:
function User(id) {
this.id = id;
this.sayHi = function() { alert(this.id); };
}
var user = new User(12345);
*!*
setTimeout(user.sayHi, 1000); // выведет "undefined" (ожидается 12345)
*/!*
Самый простой способ это обойти — сделать вызов через обёртку, которая явно вызывает метод в контексте объекта:
// анонимная фунция-обёртка
setTimeout(*!*function() { user.sayHi() }*/!*, 1000);
Но что, если user.sayHi нужно передать не только в setTimeout, а и в другие функции тоже? Не в одном месте кода, а в нескольких?
Неужели нужно каждый раз оборачивать его в function() { ... }?
..Конечно, нет. Можно привязать его к объекту, и он всегда будет носить контекст «с собой». А как — мы сейчас увидим ![]()
Привязка через замыкание
Самый простой способ привязать функцию к правильному this — это.. не использовать this!
То есть, обращаться к объекту через замыкание:
function User(id) {
this.id = id;
*!*var self = this;*/!* // сохранить this в замыкании
this.sayHi = function() { alert(*!*self.id*/!*); };
}
var user = new User(12345);
*!*
setTimeout(user.sayHi, 1000); // выведет "12345" (всё ок)
*/!*
Так как функция была «отучена» от this, то её можно смело передавать куда угодно. Контекст будет правильным.
Современный метод bind
В современном JavaScript для привязки функций есть метод bind. Он поддерживается большинством современных браузеров, за исключением IE<9 и Safari, но легко эмулируется.
Этот метод позволяет привязать функцию к нужному контексту, и даже к аргументам.
С его использованием вызов будет выглядеть так:
setTimeout( user.sayHi.bind(user), 1000);
Синтаксис bind:
- func.bind(context[, arg1, arg2…])
- Создаёт новую функцию, которая вызывает
funcв контекстеcontext. Если указаны аргументыarg1, arg2...— они будут прибавлены к каждому вызову новой функции, причем встанут перед теми, которые указаны при вызове.
Посмотрим, как работает привязка, на следующем примере.
В этом коде из обычной функции send создаётся обёртка userSend, привязанная к объекту user:
var user = { toString: function() { return "Вася" } };
function send(who, message) {
alert( "от " + this + ': ' + who + ', ' + message );
}
*!*var userSend = send.bind(user);*/!*
*!*
userSend('Петя', 'привет!'); // "от Вася: Петя, привет!"
*/!*
Используя дополнительные аргументы bind, мы можем создать функцию-обёртку для сообщений Пете:
var user = { toString: function() { return "Вася" } };
function send(who, message) {
alert( 'от ' + this + ': ' + who + ', ' + message );
}
// зафиксировать this и первый аргумент
*!*var userPeteSend = send.bind(user, 'Петя');*/!*
*!*
userPeteSend('привет');// send в контексте user с аргументами 'Петя', 'привет'
userPeteSend('пока'); // send в контексте user с аргументами 'Петя', 'пока'
*/!*
Кросс-браузерная эмуляция bind
Для браузеров, которые не поддерживают bind, его можно реализовать самостоятельно.
Без поддержки дополнительных аргументов это очень просто:
function bind(func, context) {
return function() {
return func.apply(context, arguments);
};
}
Использование:
var user = { toString: function() { return "Вася" } };
function send(who, message) {
alert( 'от ' + this + ': ' + who + ', ' + message );
}
*!*var userSend = bind(send, user)*/!*;
userSend('Петя', 'привет!'); // "от Вася: Петя, привет!"
Вариант bind c аргументами
Более полный вариант, с контекстом и аргументами:
function bind(func, context /*, args*/) {
var args = [].slice.call(arguments, 2); // (1)
function wrapper() { // (2)
var unshiftArgs = args.concat( [].slice.call(arguments) ); // (3)
return func.apply(context, unshiftArgs); // (4)
}
return wrapper;
}
Работает он так:
- Вызов
bindсохраняет дополнительные аргументы (со 2й позиции) в массивargs - … и возвращает обертку
wrapper. - Эта обёртка, используя метод concat, прибавляет свои аргументы
argumentsк массивуargsи передаёт вызовfunc(4).
Использование:
var user = { toString: function() { return "Вася" } };
function send(who, message) {
alert( 'от ' + this + ': ' + who + ', ' + message );
}
var userPeteSend = *!*bind(send, user, 'Петя')*/!*;
userPeteSend('пока!'); // "от Вася: Петя, пока!"
Вариант bind для методов
Предыдущие версии bind привязывают любую функцию к любому объекту.
Но часто возникает ситуация, когда нужно привязать к объекту не произвольную функцию, а ту, которая уже есть в объекте, т.е. его метод.
Конечно, это можно сделать так:
var userMethod = bind(user.method, user);
Но чтобы не повторять два раза "user", можно добавить поддержку альтернативного синтаксиса bind, специально для методов.
Синтаксис: bind(context, 'method'), например:
var userMethod = bind(user, 'method');
Поддержка этого синтаксиса встраивается в обычный bind следующим образом:
function bind(func, context /*, args*/) {
var args = [].slice.call(arguments, 2);
*!*
if (typeof context == "string") { // (*)
args.unshift(func[context], context);
return bind.apply(this, args); // (**)
}
*/!*
function wrapper() {
var unshiftArgs = args.concat( [].slice.call(arguments) );
return func.apply(context, unshiftArgs);
}
return wrapper;
}
Если второй аргумент — строка (*), то находим соответствующий метод объекта и перезапускаем bind в стандартном синтаксисе (**).
Позднее связывание для методов
Все рассмотренные предыдущие варианты bind называются «ранним связыванием», поскольку фиксируют привязку сразу же.
Другими словами, если метод объекта, который привязали, кто-то переопределит — привязка по-прежнему будет ориентироваться на старый метод.
Например:
function bind(func, context) {
return function() {
return func.apply(context, arguments);
};
}
var user = {
sayHi: function() { alert('Первая привязка!'); }
}
// *!*привязали метод к объекту*/!*
*!*
var userSayHi = bind(user, 'sayHi');
*/!*
// *!*понадобилось переопределить метод*/!*
user.sayHi = function() { alert('Метод изменён!'); }
// *!*будет вызван старый метод, а хотелось бы - новый!*/!*
userSayHi(); // *!*Первая привязка!*/!*
«Позднее связывание» позволяет вызвать метод объекта не тот, который был на момент привязки, а тот, который есть сейчас.
Реализуем его как bindLate(obj, method):
obj- Объект
method- Название метода (строка)
function bindLate(context, funcName) {
return function() {
return context[funcName].apply(context, arguments);
};
}
Как видно, по синтаксису — изменений нет: получает функцию и имя метода, возвращает обёртку.
Но поиск метода в объекте: context[funcName], осуществляется при вызове, самой обёрткой. Поэтому, если метод переопределили — будет использован всегда последний вариант.
Пример:
function bindLate(context, funcName) {
return function() {
return context[funcName].apply(context, arguments);
};
}
var user = {
sayHi: function() { alert('Первая привязка'); }
}
*!*
var userSayHi = bindLate(user, 'sayHi');
*/!*
user.sayHi = function() { alert('Новый метод!'); }
userSayHi(); // *!*Новый метод!*/!*
В частности, позднее связывание позволяет привязать к объекту метод, которого ещё нет!
Конечно, предполагается, что к моменту вызова он уже будет определён
.
Например:
function bindLate(context, funcName) {
return function() {
return context[funcName].apply(context, arguments);
};
}
// *!*метода нет*/!*
var user = { };
// *!*..а привязка возможна!*/!*
*!*
var userSayHi = bindLate(user, 'sayHi');
*/!*
// по ходу выполнения добавили метод..
user.sayHi = function() { alert('Привет!'); }
userSayHi(); // Метод работает: *!*Привет!*/!*
В некотором смысле, позднее связывание всегда превосходит раннее. Оно удобнее и надежнее, так как всегда вызывает нужный метод, который в объекте сейчас.
Но оно влечет и небольшие накладные расходы — поиск метода при каждом вызове.
Итого
Итоговый код bind для привязки функции или метода объекта:
function bind(func, context /*, args*/) {
var args = [].slice.call(arguments, 2);
if (typeof context == "string") {
args.unshift(func[context], context);
return bind.apply(this, args);
}
function wrapper() {
var unshiftArgs = args.concat( [].slice.call(arguments) );
return func.apply(context, unshiftArgs);
}
return wrapper;
}
Синтаксис: bind(func, context, аргументы) или bind(obj, 'method', аргументы).
Также можно использовать func.bind из современного JavaScript, при необходимости добавив кросс-браузерную эмуляцию библиотекой es5-shim:
function f() { alert(this.value); }
var g = f.bind( {value: 5} );
g(); // 5
Позднее связывание ищет функцию в объекте в момент вызова.
Оно используется для привязки в тех случаях, когда метод может быть переопределён после привязки или на момент привязки не существует.
Обёртка для позднего связывания (без передачи аргументов, её можно добавить):
function bindLate(context, funcName) {
return function() {
return context[funcName].apply(context, arguments);
};
}
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.