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

438 lines
No EOL
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Привязка контекста и карринг: "bind"
Функции в JavaScript никак не привязаны к своему контексту `this`, с одной стороны, здорово -- это позволяет быть максимально гибкими, одалживать методы и так далее.
Но с другой стороны -- в некоторых случаях контекст может быть потерян. То есть мы вроде как вызываем метод объекта, а на самом деле он получает `this = undefined`.
Такая ситуация является типичной для начинающих разработчиков, но бывает и у "зубров" тоже. Конечно, "зубры" при этом знают, что с ней делать.
[cut]
## Пример потери контекста
В браузере есть встроенная функция `setTimeout(func, ms)`, которая вызывает выполение функции `func` через `ms` миллисекунд (=1/1000 секунды).
Мы подробно остановимся на ней и её тонкостях позже, в главе [](/setTimeout-setInterval), а пока просто посмотрим пример.
Этот код выведет "Привет" через 1000мс, то есть 1 секунду:
```js
//+ run
setTimeout(function() {
alert("Привет");
}, 1000);
```
Попробуем сделать то же самое с методом объекта, следующий код должен выводить имя пользователя через 1 секунду:
```js
//+ run
var user = {
firstName: "Вася",
sayHi: function() {
alert(this.firstName);
}
};
*!*
setTimeout( user.sayHi, 1000); // undefined (не Вася!)
*/!*
```
При запуске кода выше через секунду выводится вовсе не `"Вася"`, а `undefined`!
Это произошло потому, что в примере выше `setTimeout` получил функцию `user.sayHi`, но не её контекст. То есть, последняя строчка аналогична двум таким:
```js
var f = user.sayHi;
setTimeout(f, 1000); // контекст user потеряли
```
Ситуация довольно типична -- мы хотим передать метод объекта куда-то в другое место кода, откуда он потом может быть вызван. Как бы прикрепить к нему контекст, желательно, с минимумом плясок с бубном и при этом надёжно?
Есть несколько способов решения, среди которых мы, в зависимости от ситуации, можем выбирать.
## Решение 1: сделать обёртку
Самый простой вариант решения -- это обернуть вызов в анонимную функцию:
```js
//+ run
var user = {
firstName: "Вася",
sayHi: function() {
alert(this.firstName);
}
};
*!*
setTimeout(function() {
user.sayHi(); // Вася
}, 1000);
*/!*
```
Теперь код работает, так как `user` достаётся из замыкания.
Это решение также позволяет передать дополнительные аргументы:
```js
//+ 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`:
```js
function bind(func, context) {
return function() { // (*)
return func.apply(context, arguments);
};
}
```
Посмотрим, что она делает, как работает, на таком примере:
```js
//+ run
function f() { alert(this); }
var g = bind(f, "Context");
g(); // Context
```
То есть, `bind(f, "Context")` привязывает `"Context"` в качестве `this` для `f`.
Посмотрим, за счёт чего это происходит.
Результатом `bind(f, "Context")`, как видно из кода, будет анонимная функция `(*)`.
Вот она отдельно:
```js
function() { // (*)
return func.apply(context, arguments);
};
```
Если подставить наши конкретные аргументы, то есть `f` и `"Context"`, то получится так:
```js
function() { // (*)
return f.apply("Context", arguments);
};
```
Эта функция запишется в переменную `g`.
Далее, если вызвать `g`, то вызов будет передан в `f`, причём `f.apply("Context", arguments)` передаст в качестве контекста `"Context"`, который и будет выведен.
Если вызвать `g` с аргументами, то также будет работать:
```js
//+ 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`:
```js
//+ 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` без изменений, фиксирован лишь контекст.
```js
//+ 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](https://github.com/kriskowal/es5-shim) для IE8-) у функций уже есть встроенный метод [bind](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind), который мы можем использовать.
Он работает примерно так же, как `bind`, который описан выше.
Изменения очень небольшие:
```js
//+ 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`:
```js
var wrapper = func.bind(context[, arg1, arg2...])
```
<dl>
<dt>`func`</dt>
<dd>Произвольная функция</dd>
<dt>`context`</dt>
<dd>Контекст, который привязывается к `func`</dd>
<dt>`arg1`, `arg2`, ...</dt>
<dd>Если указаны аргументы `arg1, arg2...` -- они будут прибавлены к каждому вызову новой функции, причем встанут *перед* теми, которые указаны при вызове.</dd>
</dl>
Результат вызова `func.bind(context)` аналогичен вызову `bind(func, context)`, описанному выше. То есть, `wrapper` -- это обёртка, фиксирующая контекст и передающая вызовы в `func`. Также можно указать аргументы, тогда и они будут фиксированы, но об этом чуть позже.
Пример со встроенным методом `bind`:
```js
//+ 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`"]
Если у объекта много методов и мы планируем их активно передавать, то можно привязать контекст для них всех в цикле:
```js
for(var prop in user) {
if (typeof user[prop] == 'function') {
user[prop] = user[prop].bind(user);
}
}
```
В некоторых JS-фреймворках есть даже встроенные функции для этого, например [_.bindAll(obj)](http://lodash.com/docs#bindAll).
[/smart]
## Карринг
До этого мы говорили о привязке контекста. Теперь пойдём на шаг дальше. Привязывать можно не только контекст, но и аргументы. Используется это реже, но бывает полезно.
[Карринг](http://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) (currying) или *каррирование* -- термин [функционального программирования](http://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), который означает создание новой функции путём фиксирования аргументов существующей.
Как было сказано выше, метод `func.bind(context, ...)` может создавать обёртку, которая фиксирует не только контекст, но и ряд аргументов функции.
Например, есть функция умножения двух чисел `mul(a, b)`:
```js
function mul(a, b) {
return a * b;
};
```
При помощи `bind` создадим функцию `double`, удваивающую значения. Это будет вариант функции `mul` с фиксированным первым аргументом:
```js
//+ 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` утраивает значения:
```js
//+ 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`:
```js
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-диалога с рамочками, кнопочками и так далее, но это нам сейчас не нужно.
Пример использования:
```js
//+ run
*!*
ask("Выпустить птичку?", "да", fly, die);
*/!*
function fly() {
alert('улетела :)');
}
function die() {
alert('птичку жалко :(');
}
```
## Итого
<ul>
<li>Функция сама по себе не запоминает контекст выполнения.</li>
<li>Чтобы гарантировать правильный контекст для вызова `obj.func()`, нужно использовать функцию-обёртку, задать её через анонимную функцию:
```js
setTimeout(function() {
obj.func();
})
```
</li>
<li>...Либо использовать `bind`:
```js
setTimeout( obj.func.bind(obj) );
```
</li>
<li>Вызов `bind` часто используют для привязки функции к контексту, чтобы затем присвоить её в обычную переменную и вызывать уже без явного указания объекта.</li>
<li>Вызов `bind` также позволяет фиксировать первые аргументы функции ("каррировать" её), и таким образом из общей функции получить её "частные" варианты -- чтобы использовать их многократно без повтора одних и тех же аргументов каждый раз.</li>
</ul>
[head]
<script>
function mul(a, b) {
return a * b;
};
function ask(question, correctAnswer, ok, fail) {
var result;
if (typeof correctAnswer == 'boolean') {
result = confirm(question);
} else {
result = prompt(question, '');
}
if (result == correctAnswer) ok()
else fail();
}
function bind(func, context /*, args*/) {
var bindArgs = [].slice.call(arguments, 2); // (1)
function wrapper() { // (2)
var args = [].slice.call(arguments);
var unshiftArgs = bindArgs.concat(args); // (3)
return func.apply(context, unshiftArgs); // (4)
}
return wrapper;
}
</script>
[/head]