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

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

Здравствуйте!  В продолжении темы указателя this Функции в JavaScript в этом уроке рассмотрим ситуации, связанные с потерей контекста.

Иногда возникают такие ситуации, когда функция теряет свой контекст и ее указатель this уже указывает в никуда.  Как бороться с этим явлением и что такое карринг об этом  мы как раз  и поговорим в этой статье.

Потеря контекста и карринг javascript

 

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

В браузере есть встроенная функция setTimeout(func, ms), это так  называемый таймер позволяет выполнить функцию по истесению некоторого времени.

Я подробно  расскажу о ней позже в уроках по  setTimeout и setInterval, а пока давайте рассмотрим  пример.

Этот код просто  выведет «Привет мир» через 2000 мс, то есть 2 секунды:

setTimeout(function() {
  alert( "Привет мир" );
}, 2000);

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

var user = {
  firstName: "Василий",
  say: function() {
    alert( this.firstName );
  }
};
setTimeout(user.say, 2000); //выведется  undefined (а вовсе не Василий!)

В примере выше через 2 секунды выведется вовсе не «Василий», а undefined!
Это происходит потому, что в примере setTimeout получил функцию user.say, но не её контекст:

let func = user.say;
setTimeout(func, 2000); // контекст user потерян

Такая ситуация довольно расспространена – вы хотите передать метод объекта куда-то в другую функцию, откуда он потом может быть впоследствии вызван. Но при этом к нему надо как-то прикрепить контекст.
Существует несколько способов решения этой задачи. Давайте их рассмотрим.

Решение 1: вызов через функцию обёртку

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

var user = {
  firstName: "Василий",
  say: function() {
    alert( this.firstName );
  }
};
setTimeout(function() {
  user.say(); // Василий
}, 1000);

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

var user = {
  firstName: "Василий",
  say: function(who) {
    alert( this.firstName + ": Привет, " + who );
  }
};

setTimeout(function() {
  user.say("Петя"); // Василий: Привет, Петя
}, 2000);

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

Читайте также  Расширение объектов. Свойство Prototype

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

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

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

Итак давайте поглядим что она делает и как работает примере:

function func() {
  alert( this );
}
var g = bind(func, "Context");
g(); // Context

Таким образом bind(func, «Context») привязывает «Context» в качестве this для func.
Давайте разбираться что собственно происходит.
Результатом bind(func, «Context»), как видно из кода, будет анонимная функция (*).
Вот она отдельно:

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

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

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

То эта функция запишется в переменную g.
Далее, когда вы вызываете g, то вызов будет передан в f, причём f.apply(«Context», arguments) передаст в качестве контекста «Context», который и будет собственно выведен.
Если попробовать вызвать g с аргументами, то также будет работать:

function func(a, b) {
  alert( this );
  alert( a + b );
}
var g = bind(func, "Context");
g(1, 2); // Context, затем 3

Аргументы, которые получила g(…), передаются в func также благодаря методу .apply. Подробнее об apply писал здесь
Другими словами, в результате вызова bind(func, context) мы сздадим «функцию-обёртку», которая прозрачно передаст вызов в func, с теми же аргументами, но с фиксированным контекстом context.
Вернёмся к user.say. Вариант с bind:

function bind(func, context) {
  return function() {
    return func.apply(context, arguments);
  };
}
var user = {
  firstName: "Вася",
  say: function() {
    alert( this.firstName );
  }
};
setTimeout(bind(user.say, user), 1000);

Теперь всё будет работать как надо.
Вызов bind(user.say, user) возвращает функцию-обёртку, которая привязывает user.say к контексту user. Она и будет вызвана через 1с.
Полученную функцию-обёртку можно вызвать и с аргументами – они пойдут в user.say без изменений, фиксирован лишь контекст.

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

В примере вы видите другую частую цель использования bind – «привязать» функцию к контексту и просто вызывать say.
Результат bind можно передавать в любое место кода, вызывать как обычную функцию, он «помнит» свой контекст.

Читайте также  Объекты: как передать значение по ссылке

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

В современном JavaScript у функций уже есть встроенный метод bind, который также можно использовать.
Но при это имеет небольшие изменения:

function func(a, b) {
  alert( this );
  alert( a + b );
}
// вместо
// var g = bind(func, "Context");
var g = func.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:
var user = {
firstName: «Василий»,
say: function() {
alert( this.firstName );
}
};
// setTimeout( bind(user.say, user), 1000 );
setTimeout(user.say.bind(user), 1000); // аналог через встроенный метод

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

Методы bind и call/apply близки по синтаксису, но имеется важнейшее отличие.

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

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

Карринг

До этого мы с вами говорили о привязке контекста. Теперь пойдём на шаг дальше. Привязывать можно не только контекст, но и также и аргументы. Используется это реже, но иногда бывает полезно.
Карринг (currying) или каррирование – термин функционального программирования, который означает создание новой функции путём фиксирования аргументов существующей.
Как было сказано выше, метод func.bind(context, …) может создавать обёртку, которая фиксирует не только контекст, но и также ряд аргументов функции.
Например, пусть есть функция умножения двух чисел mult(a, b):

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

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

// doubleMult умножает только на два
var doubleMult = mult.bind(null, 2); // контекст фиксируем null, он не используется
alert( doubleMult(3) ); // = mult(2, 3) = 6
alert( doubleMult(4) ); // = mult(2, 4) = 8
alert( doubleMult(5) ); // = mult(2, 5) = 10

При вызове doubleMult будет передавать свои аргументы исходной функции mul после тех, которые указаны в bind, то есть в данном случае после зафиксированного первого аргумента 2.
Говорят, что doubleMult будет как бы «частичной функцией» (partial function) от mult.

Читайте также  Перебор свойств в объектах

Итоги

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

Или использовать bind:

setTimeout(obj.func.bind(obj));
  • Вызов bind часто принято использовать для привязки функции к контексту, чтобы затем присвоить её в обычную переменную и вызывать уже без явного указания объекта.
  • Вызов bind также позволяет фиксировать первые аргументы функции («каррировать» её), и таким образом из общей функции получить её «частные» варианты – чтобы использовать их многократно без повтора одних и тех же аргументов каждый раз.

Задачи

Что выведет этот код и почему.

function sayHi() {
  alert( this.name );
}
sayHi.test = 5;
alert( sayHi.test ); // 5
var bound = sayHi.bind({
  name: "Вася"
});
alert( bound.test );

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

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Плюсануть
Поделиться

Об авторе

admin administrator

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: