Pull to refresh

Bind, Call и Apply в JavaScript

Reading time 3 min
Views 121K
Original author: Erik Kronberg
От переводчика:
Прошу принять во внимание, что приведенный здесь код, возможно, не является хорошими практиками. Тем не менее разбор сниппета из этого поста может оказаться еще одним поводом окунуться в функциональный JavaScript.


Недавно я увидел изящный JS сниппет в этом твите.
var bind = Function.prototype.call.bind(Function.prototype.bind); // #fp

Взглянув на него, я смог догадаться, что он делает. Он превращает x.y(z) в y(x, z). Радуясь как ребенок, я показал его своим коллегам. Они спросили меня, что же тут происходит. Я открыл рот, чтобы объяснить и… не смог сказать ни слова. Я развернулся и ушел.

В большинстве случаев, взглянув на хорошо написанный код, можно легко догадаться, что он делает. Имея какой-то опыт в функциональном JavaScript, прочитав «Functional JavaScript» и «JavaScript Allongé» (обе замечательны), у меня не возникло особых трудностей в его прочтении. Но как объяснить этот код кому-то без опыта функционального программирования?

Я решил поэтапно разобраться на простых примерах что же тут происходит. Результат был таков:

// Создадим простой объект, чтобы использовать его в качестве контекста
var context = { foo: "bar" };

// Функция, которая возвращает свойство «foo» контекста «this»
function returnFoo () {
  return this.foo;
}

// Свойства не существует в текущей области видимости, поэтому undefined
returnFoo(); // => undefined

// Но если мы свяжем эту функцию с контекстом
var bound = returnFoo.bind(context);

// Свойство теперь в области видимости
bound(); // => "bar"

//
// Так работает Function.prototype.bind.
// Так как returnFoo — это функция, она наследует прототип Function.prototype
//

// Существует несколько способов связывания функции с контекстом
// Call и apply позволяют вам вызывать функцию с нужным контекстом
returnFoo.call(context); // => bar
returnFoo.apply(context); // => bar

// Так же можно вложить функцию в объект
context.returnFoo = returnFoo;
context.returnFoo(); // => bar

//
// Теперь давайте немного усложним
//

// В Array.prototype есть замечательный метод slice.
// При вызове на массиве он возвращает копию массива
// от начального индекса до конечного (исключительно)
[1,2,3].slice(0,1); // => [1]

// Мы берем slice и присваиваем его локальной переменной
var slice = Array.prototype.slice;

// slice теперь оторван от контекста. Из-за того, что Array.prototype.slice
// работает с данным ему контекстом «this», метод больше не работает
slice(0, 1); // => TypeError: can't convert undefined to object
slice([1,2,3], 0, 1); // => TypeError: ...

// Но мы можем использовать apply и call, они позволяют нам передавать нужный контекст
slice.call([1,2,3], 0, 1); // => [1]

// Apply работает как call, но принимает аргументы в виде массива
slice.apply([1,2,3], [0,1]); // => [1]

// Немного надоедает использовать .call каждый раз. Может воспользоваться bind?
// Точно! Давайте привяжем функцию call к контексту slice. 
slice = Function.prototype.call.bind(Array.prototype.slice);

// Теперь slice использует первый аргумент в качестве контекста
slice([1,2,3], 0, 1); // => [1]

//
// Неплохо, правда? Но у меня осталась еще кое-что.
//

// Давайте проделаем с самим bind то же,
// что мы делали со slice
var bind = Function.prototype.call.bind(Function.prototype.bind);

// Обдумайте то, что мы только что сделали.
// Что происходит? Мы оборачиваем call, возвращая функцию, которая принимает функцию и контекст
// и возвращает связанную с контекстом функцию.

// Вернемся к нашему первоначальному примеру
var context = { foo: "bar" };
function returnFoo () {
  return this.foo;
}

// И к нашему новому bind
var amazing = bind(returnFoo, context);
amazing(); // => bar

// Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
Tags:
Hubs:
+39
Comments 42
Comments Comments 42

Articles