en.javascript.info/01-js/06-objects-more/05-bind/article.md
Ilya Kantor f301cb744d init
2014-10-26 22:10:13 +03:00

14 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 достаётся из замыкания.

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

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

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

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

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

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

Параметры:

`func`
Произвольная функция
`context`
Произвольный объект

Результатом вызова bind(func, context) будет, как видно из кода, функция-обёртка, которая передаёт все вызовы func, указывая при этом правильный контекст context.

Чтобы понять, как она работает, нужно вспомнить тему "замыкания" -- здесь bind возвращает анонимную функцию, которая при вызове получает контекст context из внешней области видимости и передаёт вызов в func вместе с этим контекстом context и аргументами arguments.

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

Пример с 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 );
*/!*

Теперь всё в порядке! В setTimeout пошла обёртка, фиксирующая контекст.

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

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

Он позволяет получить обёртку, которая привязывает функцию не только к нужному контексту но, если нужно, то и к аргументам.

Синтаксис bind:

var wrapper = func.bind(context[, arg1, arg2...])
`func`
Произвольная функция
`context`
Обертка `wrapper` будет вызывать функцию с контекстом `this = context`.
`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.

[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 или сделать с ней что-то ещё.

Задачи

Рассмотрим для дальнейших задач "функцию для вопросов" 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('птичку жалко :(');
}

Итого

Функции и контекст к JavaScript -- как шнурки и кроссовки. Если мы куда-то отправляем шнурки (например в setTimeout), то кроссовки сами за ними не побегут.

Нужно либо передать их дополнительно, либо привязать одно к другому вызовом bind, либо завернуть в замыкание.

[head]

[/head]