438 lines
No EOL
18 KiB
Markdown
438 lines
No EOL
18 KiB
Markdown
# Привязка контекста и карринг: "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] |