Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Привязка функции к объекту и карринг: "bind/bindLate"

  1. Привязка через замыкание
  2. Современный метод bind
  3. Кросс-браузерная эмуляция bind
    1. Вариант bind c аргументами
    2. Вариант bind для методов
  4. Позднее связывание для методов
  5. Итого

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

..Конечно, нет. Можно привязать его к объекту, и он всегда будет носить контекст «с собой». А как — мы сейчас увидим Wink

Привязка через замыкание

Самый простой способ привязать функцию к правильному 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;
}

Работает он так:

  1. Вызов bind сохраняет дополнительные аргументы (со 2й позиции) в массив args
  2. … и возвращает обертку wrapper.
  3. Эта обёртка, используя метод 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(); // *!*Новый метод!*/!*

В частности, позднее связывание позволяет привязать к объекту метод, которого ещё нет!

Конечно, предполагается, что к моменту вызова он уже будет определён Wink.

Например:

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);
  };
}


Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!