Bind, Call и Apply в JavaScript

Original author: Erik Kronberg
  • Translation
От переводчика:
Прошу принять во внимание, что приведенный здесь код, возможно, не является хорошими практиками. Тем не менее разбор сниппета из этого поста может оказаться еще одним поводом окунуться в функциональный 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
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 42

    +21
    Не понимаю какой смысл писать
    var amazing = bind(returnFoo, context);
    вместо
    var amazing = returnFoo.bind(context);
    в чем смысл и почему этому надо радоваться, как ребенок?
      +2
      Похоже, кто-то не понял смысла статьи.
        +5
        и не он один.
        Как это рабоает — понятно. А вот что это дает — вообще осознаваемо.
          +5
          Просто неочевидный пример. Вот более частый кейс:
          var _toString = Function.prototype.call.bind(Object.prototype.toString);
          
          _toString({});     // "[object Object]"
          _toString(window); // "[object Window]"
          _toString([]);     // "[object Array]"
          

          Т.е. простой вызов функции _toString() используется вместо:
          Object.prototype.toString.call( [] )
          

          и
          var _toString = Object.prototype.toString
          _toString.call( [] )
          

          что весьма удобнее
            +1
            Действительно удобно, хотя обычно к результату _toString(...) еще .slice(8, -1) применяют. Поэтому проще написать такую функцию:
            var getTypeString = function(obj) {
              return Object.prototype.toString.call(obj).slice(8, -1);
            };
            

            и дальше использовать её.
          0
          В статье нет ни слова о том, зачем это нужно. Очевидно, это нужно затем, что парсить метод .bind() при чтении не очень удобно. Данный приём позволяет упростить чтение.
        +6
        И всё равно в какой-то момент я потерял контекст происходящего.
        Не думаю, что стоит использовать такие трюки в обычном коде.
        Внутри библиотеки — возможно, но в основном коде я бы не стал.
        Буду опять перечитывать до полного понимания.
          +5
          Именно поэтому я написал в начале поста дисклеймер.
          Речь не о том, чтобы использовать такие приемы в продакшне. Для меня это была неплохая пища для размышлений, еще один повод подробнее изучить парадигму функционального программирования. Такие вещи неплохо контрастируют с бытовым повседневным программированием. Именно поэтому я решил перевести оригинальный пост и опубликовать его тут.
            +37
            И всё равно в какой-то момент я потерял контекст происходящего.
            nazarpc.bind(context)
              +2
              <режим зануды>bind не меняет состояние объекта, он его возвращает</режим зануды>
              +2
              Этот bind давно и повсюду есть и используется:
              jQuery: api.jquery.com/jQuery.proxy/
              dojo: dojotoolkit.org/reference-guide/1.7/dojo/hitch.html

              Эта штука чаще всего нужна когда ты хочешь подвесить функцию, юзающую контекст из this (метод класса) на событие, чтобы при обработке события этот же контекст остался. Задача похожа на то, что часто еще решают с помощью var that = this.

              Это уже давно и повсеместно в продакшне.
                0
                api.jquery.com/jQuery.proxy/ это НЕ bind. Забудте вообще про этот метод в jQuery
                  0
                  api.jquery.com/jQuery.proxy/ это ДА bind jQuery.proxy
                    +1
                    habrahabr.ru/post/199456/#comment_6924098
                    jQuery.proxy внутри себя не использует нативный bind. Зачем использовать весьма убогий библиотечный метод, если есть нативная альтернатива?
                      0
                      Например потому что нативной альтернативы нет в IE <= 8
                        0
                        es5-shim полностью реализует Function.prototype.bind и ряд других методов, в том числе фиксит страшный баг в IE8 с Array.prototype.splice
                          0
                          А в чем принципиальная разница их какой именно библиотеки использовать этот bind?
                            0
                            В том, что при использовании полифилов, библиотечная функция будет использоваться только в случае отсутствия такой нативной функции. Т.е. в вашем случае, библиотечная реализация bind (которая, к стати в es5-shim сделана лучше, чем в jQuery) будет использоваться только в IE8, а в остальных браузерах будет использоваться нативная функция.
              +6
              Возможно я буду резковат, но «метод» — обычная перестановка. Практической пользы я не могу придумать, а вот трудности уже перед глазами.

              ИМХО, это не изящный сниппет, а головная боль на стадии поддержки.
              0
              В Lua круче сделано, там 2 формата вызова методов
              obj:func(param1, param3)
              тогда в this попадет obj
              а можно
              obj.func(obj2, param1, param3)
              тогда в this попадет obj2
                +1
                Вообще ок сделано. Никогда не понимал зачем этот this, который по факту — просто неявный параметр функции, придают сакральный смысл.
                0
                Завсегда юзал для себя свой бинд вот такого вида. Работает ну точно так же как результат в статье, за исключение того что заведомо писался так, чтобы не изменять прототип Function (ну и ещё доп аргументы замыкает). Согласен с первым комментарием короче я.

                function bind (context, method) {
                	var prefix = Array.prototype.slice.call(arguments,2);
                	return function(){
                		return method.apply(context, prefix.concat(Array.prototype.slice.call(arguments)));
                	}
                }
                
                  0
                  А где в статье что-то меняет прототип функции?
                    0
                    Не точно выразился. Бинд в статье — часть прототипа, и был ей не всегда.
                    0
                    «JavaScript-шаблоны» — бесподобная книга ;-)
                      0
                      Код выше писал сам, но код простой очень, тут сложно никого не повторить.
                      +1
                      Радует что вы не добавляли это в прототип функции как шим. Я бы все-таки называл функцию по другому, ведь так или иначе программист севший работать с вашим кодом ожидает что bind ведет себя по спецификации, так что я бы не спешил повсеместно это употреблять.
                        –2
                        Ремарочка: неужели вы, садясь разбирать код другого программиста, действительно ждете что его функции будут работать по спецификации? Нет, ну правда?
                          0
                          Да, я именно этого и жду, ибо если я буду лазить в каждый подобный косяк смотря что там и как делают, я буду половину своего времени тратить на «чисто посмотреть а что же делает функция аналогичная спецификации». А это простите никому не интересно. И если функция работает как-то по особому я жду отражения этого в документации.
                          И по моему мои слова не есть открытие для нормальных проектов где работает не 1 человек и переодически вливаются новые
                            0
                            Ну вам повезло работать исключительно в нормальных проектах, иначе вы бы прочувствовали всю ошибочность и времезатратность подхода, который предписывает по умолчанию верить в ответвтенность предыдущего разработчика.
                              0
                              когда программист будет тратить 80% на понимание и исправление предыдущего кода при внесении любых правок у любого проекта встанет вопрос «сделать нормально». Всегда есть дока в общем то, если её нет то проект или не большой и хрень
                        0
                        Т.е. вы не используете встроенный в движок браузера и весьма оптимизированный Function.prototype.bind только из-за того, что когда-то не во всех браузерах был этот метод?
                          +2
                          Ну во первых я, как и все, подвержен инерционности мышления, а во вторых вот в соседней статье удивлялись зачем экономить 100кб подключаемого кода библиотек, каналы же сейчас широкие, ну а я вот не парюсь по поводу скорости работы не самой часто-используемой функции.
                        +1
                        На мой взгляд, перед шагом

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

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

                        к
                        Function.prototype.call(slice, [1,2,3], 0, 1); // => [1]
                        

                        Без него непонятно, что происходит при вызове bind().
                          0
                          Спасибо тебе, добрый человек. Без bind никак не мог решить задачу с addEventListener (и remove в перспективе). А с ним смог так:
                          var foo = {
                              prop: 0,
                              bar: function(){
                                  console.log(++this.prop);
                              },
                              init: function(){
                                  this.bar = this.bar.bind(this);
                                  document.addEventListener("click", this.bar);
                              }
                          };
                          foo.init();
                          foo.bar();
                          
                            0
                            Может быть вызов обработчика в init лучше оформить в виде классического callback-а, поскольку перезапись this.bar в данном случае смысла не имеет:

                                init: function(){
                                    var callback = this.bar.bind(this);
                                    document.addEventListener("click", callback);
                            
                              0
                              Любопытно, кстати, что следующий, казалось бы, эквивалентный вариант привязки контекста уже не работает, так как вызов bar с bind срабатывает уже внутри addEventListener в глобальном контексте:
                                  bar: (function(){
                                      console.log(++this.prop);
                                  }).bind(this),
                                  init: function(){
                                      document.addEventListener("click", this.bar);
                                  }
                              
                            0
                            до меня долго доходило, почему мы в call биндим функцию, но потом понял, что для Array.prototype.slice это и есть функция к которой будет применяться call… по сути мы жестко привязываем call на эту функцию и возвращаем значение в переменную. получается для выражения: slice = Function.prototype.call.bind(Array.prototype.slice); на пальцах можно объяснить так: в переменную slice верни функцию call которая будет вызываться в контексте Array.prototype.slice (так как мы ее забиндили для функции call и теперь внутри функции call — this это Array.prototype.slice). Надеюсь может кому-то поможет в осознании того, что написал автор, хотя прочитав сейчас то, что написал засомневался =)
                              0
                              Все верно) Также не сразу к тому пришел.

                            Only users with full accounts can post comments. Log in, please.