en.javascript.info/1-js/6-objects-more/7-bind/article.md
2015-02-19 23:57:02 +03:00

18 KiB
Raw Blame History

Привязка контекста и карринг: "bind"

Функции в JavaScript никак не привязаны к своему контексту this, с одной стороны, здорово -- это позволяет быть максимально гибкими, одалживать методы и так далее.

Но с другой стороны -- в некоторых случаях контекст может быть потерян. То есть мы вроде как вызываем метод объекта, а на самом деле он получает this = undefined.

Такая ситуация является типичной для начинающих разработчиков, но бывает и у "зубров" тоже. Конечно, "зубры" при этом знают, что с ней делать.

[cut]

Пример потери контекста

В браузере есть встроенная функция setTimeout(func, ms), которая вызывает выполение функции func через ms миллисекунд (=1/1000 секунды).

Мы подробно остановимся на ней и её тонкостях позже, в главе , а пока просто посмотрим пример.

Этот код выведет "Привет" через 1000мс, то есть 1 секунду:

//+ run
setTimeout(function() {
  alert("Привет");
}, 1000);

Попробуем сделать то же самое с методом объекта, следующий код должен выводить имя пользователя через 1 секунду:

//+ run
var user = {
  firstName: "Вася",
  sayHi: function() { 
    alert(this.firstName);
  }
};

*!*
setTimeout( user.sayHi, 1000); // undefined (не Вася!)
*/!*

При запуске кода выше через секунду выводится вовсе не "Вася", а undefined!

Это произошло потому, что в примере выше setTimeout получил функцию user.sayHi, но не её контекст. То есть, последняя строчка аналогична двум таким:

var f = user.sayHi;
setTimeout(f, 1000); // контекст user потеряли

Ситуация довольно типична -- мы хотим передать метод объекта куда-то в другое место кода, откуда он потом может быть вызван. Как бы прикрепить к нему контекст, желательно, с минимумом плясок с бубном и при этом надёжно?

Есть несколько способов решения, среди которых мы, в зависимости от ситуации, можем выбирать.

Решение 1: сделать обёртку

Самый простой вариант решения -- это обернуть вызов в анонимную функцию:

//+ run
var user = {
  firstName: "Вася",
  sayHi: function() { 
    alert(this.firstName);
  }
};

*!*
setTimeout(function() {
 user.sayHi(); // Вася
}, 1000); 
*/!*

Теперь код работает, так как user достаётся из замыкания.

Это решение также позволяет передать дополнительные аргументы:

//+ run
var user = {
  firstName: "Вася",
  sayHi: function(who) { 
    alert(this.firstName + ": Привет, " + who);
  }
};

*!*
setTimeout(function() {
 user.sayHi("Петя"); // Вася: Привет, Петя
}, 1000); 
*/!*

Но тут же появляется и уязвимое место в структуре кода!

А что, если до срабатывания setTimeout (ведь есть целая секунда) в переменную user будет записано другое значение? К примеру, в другом месте кода будет присвоено user=(другой пользователь)... В этом случае вызов неожиданно будет совсем не тот!

Хорошо бы гарантировать правильность контекста.

Решение 2: bind для привязки контекста

Напишем вспомогательную функцию bind(func, context), которая будет жёстко фиксировать контекст для func:

function bind(func, context) {
  return function() { // (*)
    return func.apply(context, arguments); 
  };
}

Посмотрим, что она делает, как работает, на таком примере:

//+ run
function f() { alert(this); }

var g = bind(f, "Context");
g();  // Context

То есть, bind(f, "Context") привязывает "Context" в качестве this для f.

Посмотрим, за счёт чего это происходит.

Результатом bind(f, "Context"), как видно из кода, будет анонимная функция (*).

Вот она отдельно:

function() {   // (*)
  return func.apply(context, arguments); 
};

Если подставить наши конкретные аргументы, то есть f и "Context", то получится так:

function() {   // (*)
  return f.apply("Context", arguments); 
};

Эта функция запишется в переменную g.

Далее, если вызвать g, то вызов будет передан в f, причём f.apply("Context", arguments) передаст в качестве контекста "Context", который и будет выведен.

Если вызвать g с аргументами, то также будет работать:

//+ run
function f(a, b) { 
  alert(this); 
  alert(a + b);
}

var g = bind(f, "Context");
g(1, 2);  // Context, затем 3

Аргументы, которые получила g(...), передаются в f также благодаря методу .apply.

Иными словами, в результате вызова bind(func, context) мы получаем "функцию-обёртку", которая прозрачно передаёт вызов в func, с теми же аргументами, но фиксированным контекстом context.

Вернёмся к user.sayHi. Вариант с bind:

//+ run
function bind(func, context) {
  return function() { 
    return func.apply(context, arguments); 
  };
}

var user = {
  firstName: "Вася",
  sayHi: function() {
    alert(this.firstName);
  }
};

*!*
setTimeout( bind(user.sayHi, user), 1000 );
*/!*

Теперь всё в порядке!

Вызов bind(user.sayHi, user) возвращает такую функцию-обёртку, которая привязывает вызовет user.sayHi к контексту user. Она будет вызвана через 1000мс.

Полученную обёртку можно вызвать и с аргументами -- они пойдут в user.sayHi без изменений, фиксирован лишь контекст.

//+ run
var user = {
  firstName: "Вася",
*!*
  sayHi: function(who) { // здесь у sayHi есть один аргумент
*/!*
    alert(this.firstName + ": Привет, " + who);
  }
};

var sayHi = bind(user.sayHi, user);

*!*
// контекст Вася, а аргумент передаётся "как есть"
sayHi("Петя"); // Вася: Привет, Петя
sayHi("Маша"); // Вася: Привет, Маша
*/!*

В примере выше продемонстрирована другая частая цель использования bind -- "привязать" функцию к контексту, чтобы в дальнейшем "не таскать за собой" объект, а просто вызывать sayHi.

Результат bind можно передавать в любое место кода, вызывать как обычную функцию, он "помнит" свой контекст.

Решение 3: встроенный метод bind [#bind]

В современном JavaScript (или при подключении библиотеки es5-shim для IE8-) у функций уже есть встроенный метод bind, который мы можем использовать.

Он работает примерно так же, как bind, который описан выше.

Изменения очень небольшие:

//+ run
function f(a, b) { 
  alert(this); 
  alert(a + b);
}

*!*
// вместо 
// var g = bind(f, "Context");
var g = f.bind("Context");
*/!*
g(1, 2);  // Context, затем 3

Синтаксис встроенного bind:

var wrapper = func.bind(context[, arg1, arg2...])
`func`
Произвольная функция
`context`
Контекст, который привязывается к `func`
`arg1`, `arg2`, ...
Если указаны аргументы `arg1, arg2...` -- они будут прибавлены к каждому вызову новой функции, причем встанут *перед* теми, которые указаны при вызове.

Результат вызова func.bind(context) аналогичен вызову bind(func, context), описанному выше. То есть, wrapper -- это обёртка, фиксирующая контекст и передающая вызовы в func. Также можно указать аргументы, тогда и они будут фиксированы, но об этом чуть позже.

Пример со встроенным методом bind:

//+ run
var user = {
  firstName: "Вася",
  sayHi: function() { 
    alert(this.firstName);
  }
};

*!*
// setTimeout( bind(user.sayHi, user), 1000 ); 
setTimeout( user.sayHi.bind(user), 1000 ); // аналог через встроенный метод
*/!*

Получили простой и надёжный способ привязать контекст, причём даже встроенный в JavaScript.

Далее мы будем использовать именно встроенный метод bind.

[warn header="bind не похож call/apply"] Методы bind и call/apply близки по синтаксису, но есть важнейшее отличие.

Методы call/apply вызывают функцию с заданным контекстом и аргументами.

А bind не вызывает функцию. Он только возвращает "обёртку", которую мы можем вызвать позже, и которая передаст вызов в исходную функцию, с привязанным контекстом. [/warn]

[smart header="Привязать всё: bindAll"] Если у объекта много методов и мы планируем их активно передавать, то можно привязать контекст для них всех в цикле:

for(var prop in user) {
  if (typeof user[prop] == 'function') {
    user[prop] = user[prop].bind(user);
  }
}

В некоторых JS-фреймворках есть даже встроенные функции для этого, например _.bindAll(obj). [/smart]

Карринг

До этого мы говорили о привязке контекста. Теперь пойдём на шаг дальше. Привязывать можно не только контекст, но и аргументы. Используется это реже, но бывает полезно.

Карринг (currying) или каррирование -- термин функционального программирования, который означает создание новой функции путём фиксирования аргументов существующей.

Как было сказано выше, метод func.bind(context, ...) может создавать обёртку, которая фиксирует не только контекст, но и ряд аргументов функции.

Например, есть функция умножения двух чисел mul(a, b):

function mul(a, b) {
  return a * b;
};

При помощи bind создадим функцию double, удваивающую значения. Это будет вариант функции mul с фиксированным первым аргументом:

//+ run
*!*
// double умножает только на два
var double = mul.bind(null, 2); // контекст фиксируем null, он не используется
*/!*

alert( double(3) );  // = mul(2, 3) = 6
alert( double(4) );  // = mul(2, 4) = 8
alert( double(5) );  // = mul(2, 5) = 10

При вызове double будет передавать свои аргументы исходной функции mul после тех, которые указаны в bind, то есть в данном случае после зафиксированного первого аргумента 2.

Говорят, что double является "частичной функцией" (partial function) от mul.

Другая частичная функция triple утраивает значения:

//+ run
*!*
var triple = mul.bind(null, 3); // контекст фиксируем null, он не используется
*/!*

alert( triple(3) );  // = mul(3, 3) = 9
alert( triple(4) );  // = mul(3, 4) = 12
alert( triple(5) );  // = mul(3, 5) = 15

При помощи bind мы можем получить из функции её "частный вариант" как самостоятельную функцию и дальше передать в setTimeout или сделать с ней что-то ещё.

Наш выигрыш в этом состоит в том, что эта самостоятельная функция, во-первых, имеет понятное имя (double, triple), а во-вторых, повторные вызовы позволяют не указывать каждый раз первый аргумент, он уже фиксирован благодаря bind.

Функция ask для задач

В задачах этого раздела предполагается, что объявлена следующая "функция вопросов" ask:

function ask(question, answer, ok, fail) {
  var result = prompt(question, '');
  if (result.toLowerCase() == answer.toLowerCase()) ok();
  else fail();
}

Её назначение -- задать вопрос question и, если ответ совпадёт с answer, то запустить функцию ok(), а иначе -- функцию fail().

Несмотря на внешнюю простоту, функции такого вида активно используются в реальных проектах. Конечно, они будут сложнее, вместо alert/prompt -- вывод красивого JavaScript-диалога с рамочками, кнопочками и так далее, но это нам сейчас не нужно.

Пример использования:

//+ run
*!*
ask("Выпустить птичку?", "да", fly, die);
*/!*

function fly() { 
  alert('улетела :)');
}

function die() { 
  alert('птичку жалко :(');
}

Итого

  • Функция сама по себе не запоминает контекст выполнения.
  • Чтобы гарантировать правильный контекст для вызова `obj.func()`, нужно использовать функцию-обёртку, задать её через анонимную функцию: ```js setTimeout(function() { obj.func(); }) ```
  • ...Либо использовать `bind`:
    setTimeout( obj.func.bind(obj) );
    
  • Вызов `bind` часто используют для привязки функции к контексту, чтобы затем присвоить её в обычную переменную и вызывать уже без явного указания объекта.
  • Вызов `bind` также позволяет фиксировать первые аргументы функции ("каррировать" её), и таким образом из общей функции получить её "частные" варианты -- чтобы использовать их многократно без повтора одних и тех же аргументов каждый раз.

[head]

[/head]