Привязка контекста (this) к функции в javascript и частичное применение функций

  • Tutorial
В предыдущем посте я описал, что this в javascript не привязывается к объекту, а зависит от контекста вызова. На практике же часто возникает необходимость в том, чтобы this внутри функции всегда ссылался на конкретный объект.
В данной статье мы рассмотрим два подхода для решения данной задачи.
1. jQuery.proxy — подход с использованием популярной библиотеки jQuery
2. Function.prototype.bind — подход, добавленный в JavaScript 1.8.5. Рассмотрим также его применение для карринга (частичного применения функции) и некоторые тонкости работы, о которых знают единицы.


Введение


Рассмотрим простой объект, содержащий свойство x и метод f, который выводит в консоль значение this.x
var object = {
    x: 3,
    f: function() {
        console.log(this.x);
    }
}

Как я указывал в предыдущем посте, при вызове object.f() в консоли будет выведено число 3. Предположим теперь, что нам нужно вывести данное число через 1 секунду.
setTimeout(object.f, 1000); // выведет undefined

//простой способ это обойти — сделать вызов через обёртку:
setTimeout(function() { object.f(); }, 1000); // выведет 3

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

1. jQuery.proxy


jQuery.proxy(function, context);
jQuery.proxy(context, name);

Ни для кого не секрет, что jQuery — очень популярная библиотека javascript, поэтому вначале мы рассмотрим применение jQuery.proxy для привязки контекста к функции.
jQuery.proxy возвращает новую функцию, которая при вызове вызывает оригинальную функцию function в контексте context. С использованием jQuery.proxy вышеописанную задачу можно решить так:
setTimeout($.proxy(object.f, object), 1000); // выведет 3

Если нам нужно указать несколько раз одинаковый callback, то вместо дублирования
setTimeout($.proxy(object.f, object), 1000);
setTimeout($.proxy(object.f, object), 2000);
setTimeout($.proxy(object.f, object), 3000);

лучше вынести результат работы $.proxy в отдельную переменную
var fn = $.proxy(object.f, object);
setTimeout(fn, 1000);
setTimeout(fn, 2000);
setTimeout(fn, 3000);


Обратим теперь внимание на то, что мы дважды указали object внутри $.proxy (первый раз метод объекта — object.f, второй — передаваемй контекст — object). Может быть есть возможность избежать дублирования? Ответ — да. Для таких случаев в $.proxy добавлена альтернативная возможность передачи параметров — первым параметром должен быть объект, а вторым — название его метода. Пример:
var fn = $.proxy(object, "f");
setTimeout(fn, 1000);

Обратите внимание на то, что название метода передается в виде строки.

2. Function.prototype.bind


func.bind(context[, arg1[, arg2[, ...]]])


Перейдем к рассмотрению Function.prototype.bind. Данный метод был добавлен в JavaScript 1.8.5.
Совместимость с браузерами
Firefox (Gecko): 4.0 (2)
Chrome: 7
Internet Explorer: 9
Opera: 11.60
Safari: 5.1.4

Эмуляция Function.prototype.bind из Mozilla Developer Network
Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      // closest thing possible to the ECMAScript 5 internal IsCallable function
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }
 
    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, 
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP && oThis
                                 ? this
                                 : oThis,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };
 
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
 
    return fBound;
  };


Function.prototype.bind имеет 2 назначения — статическая привязка контекста к функции и частичное применение функции.
По сути bind создаёт новую функцию, которая вызывает func в контексте context. Если указаны аргументы arg1, arg2… — они будут прибавлены к каждому вызову новой функции, причем встанут перед теми, которые указаны при вызове новой функции.

2.1. Привязка контекста

Использовать bind для привязки контекста очень просто, достаточно рассмотреть пример:
function f() {
    console.log(this.x);
}
var bound = f.bind({x: 3}); // bound - новая функция - "обертка", у которой this ссылается на объект {x:3}
bound();// Выведет 3


Таким образом пример из введения можно записать в следующем виде:
var object = {
    x: 3,
    f: function() {
        console.log(this.x);
    }
}
setTimeout(object.f.bind(object), 1000); // выведет 3


2.2. Частичное применение функций

Для упрощения рассмотрим сразу пример использования bind для частичного применения функций
function f(x, y, z) {
    console.log(x + y + z);
}
var bound = f.bind(null, 3, 5); // напомню что первый параметр - это контекст для функции, поскольку мы не используем this в функции f, то контекст не имеет значения - поэтому в данном случае передан null
bound(7); // распечатает 15 (3 + 5 + 7)
bound(17); // распечатает 25 (3 + 5 + 17)


Как видно из примера — суть частичного применения функций проста — создание новой функции с уменьшенным количеством аргументов, за счет «фиксации» первых аргументов с помощью функции bind.
На этом можно было бы закончить статью, но… Функции, полученные с использованием метода bind имеют некоторые особенности в поведении

2.3. Особенности bind

В комментариях к предыдущей статье было приведено 2 примера, касающихся bind (раз, два).
Я решил сделать микс из этих примеров, попутно изменив строковые значения, чтобы было проще с ними разбираться.
Пример (попробуйте угадать ответы)
function ClassA() {
  console.log(this.x, arguments)
}

ClassA.prototype.x = "fromProtoA";

var ClassB = ClassA.bind({x : "fromBind"}, "bindArg");

ClassB.prototype = {x : "fromProtoB" };

new ClassA("callArg");
new ClassB("callArg");
ClassB("callArg");
ClassB.call({x: "fromCall"}, 'callArg');

Ответы
fromProtoA [«callArg»]
fromProtoA [«bindArg», «callArg»]
fromBind [«bindArg», «callArg»]
fromBind [«bindArg», «callArg»]

Прежде чем разобрать — я перечислю основные особенности bind в соответствии со стандартом.

2.3.1. Внутренние свойств

У объектов Function, созданных посредством Function.prototype.bind, отсутствует свойство prototype или внутренние свойства [[Code]], [[FormalParameters]] и [[Scope]].

Это ограничение отличает built-in реализацию bind от вручную определенных методов (например, вариант из MDN)

2.3.2. call и apply

Поведение методов call и apply отличается от стандартного поведения для функций, а именно:
boundFn.[[Call]] = function (thisValue, extraArgs):
 
  var
      boundArgs = boundFn.[[BoundArgs]],
      boundThis = boundFn.[[BoundThis]],
      targetFn = boundFn.[[TargetFunction]],
      args = boundArgs.concat(extraArgs);
 
      return targetFn.[[Call]](boundThis, args);

В коде видно, что thisValue не используется нигде. Таким образом подменить контекст вызова для функций полученных с помощью Function.prototype.bind с использованием call и applyнельзя!

2.3.3. В конструкторе

В конструкторе this ссылается на новый (создаваемый) объект. Иначе говоря, контекст заданный при помощи bind, просто игнорируется. Конструктор вызывает обычный [[Call]] исходной функции.
Важно! Если в конструкторе отсутствует return this, то возвращаемое значение в общем случае неопределено и зависит от возвращаемого значения новой функции!

Разбор примера

function ClassA() {
  console.log(this.x, arguments)
}

ClassA.prototype.x = "fromProtoA";

var ClassB = ClassA.bind({x : "fromBind"}, "bindArg");

// исходя из 2.3.1, эта строчка не делает ровным счетом ничего в built-in реализациях Function.prototype.bind
// Но в ручной реализации bind (например, как в MDN) эта строчка сыграет роль 
ClassB.prototype = {x : "fromProtoB" };

// Тут все просто - никакой bind мы еще не использовали
// Результат: fromProtoA ["callArg"]
new ClassA("callArg"); 

// Исходя из 2.3.3 - this ссылается на новый объект. поскольку в bind был задан параметр bindArg, то в выводе аргументов он займет первое место
// Результат:  fromProtoA ["bindArg", "callArg"]
// При ручной реализации bind результат будет другой: fromBind ["bindArg", "callArg"]. 
new ClassB("callArg");

// Обычный вызов bind функции, поэтому в качестве контекста будет {x : "fromBind"}, первым параметром bindArg (заданный через bind), вторым - "callArg"
// Результат: fromBind ["bindArg", "callArg"]
ClassB("callArg");

// Из пункта 2.3.2. следует, что при вызове метода call на функции, полученной с использованием bind передаваемый контекст игнорируется.
// Результат: fromBind ["bindArg", "callArg"]
ClassB.call({x: "fromCall"}, 'callArg');


Заключение


В данном посте я постарался описать основные методы привязывания контекста к функциям, а также описал некоторые особенности в работе Function.prototype.bind, при этом я старался оставить только важные детали (с моей точки зрения).
Если вы заметили ошибки/неточности или хотите что-то уточнить/добавить — напишите в ЛС, поправлю
Поделиться публикацией

Комментарии 45

    +5
    Перешедшие на кофескрипт от неправильного контекста не страдают, используя при объявлении функции => вместо -> мы получаем bind на текущий объект :)
      0
      ага. и контекст зависит от того, есть чёрточка или нету. А на самом деле под капотом всё тот же Бинд.
        0
        Вы не правы. Нет там бинда (пруф)
          0
          Согласен) Кстати, в JS.next думают что-то в этом же роде сделать.
            +1
            Коллеги, а вам не кажется, что жирная стрелка — не лучший символ, т.к. путается с обычной стрелкой? Я лично путаю периодически при чтении кода. Мб лучше была бы «жирная и длинная» — ==>?

            Но больше всего путает, конечно, чередование of/in при итерировании объекта/массива — довольно часто путается, глупые затыки из-за этого происходят.
              +1
              Про стрелки не соглашусь, сложности отличия -> от => никогда не замечал, а вот то, что of для хешей, а in для массивов, запомнилось не сразу.
                +1
                А не наоборот?
        +2
        Присоединяюсь к мнению как coffee адепт :), пропихиваю его везде где могу. А я вот эта фича с сохранением контекста вообще бомба — никаких бинд-функций и засорения прототипов нативных классов.
        Автору респект, за то что поделился с теми, кто еще не изучил поведение вокруг контекта — такие знания относятся к фундаментальным и знать их нужно. Я, пользуясь coffee, был бы не так уверен в том, что делаю если б не имел крепких знаний по чистому JS.
          0
          Так ведь понимание, когда нужно сохранять контекст, в кофе всё равно нужно, если только вы всегда не используете =>, что совсем неправильно :)
            0
            Это да, понимание нужно, согласен. Прокидывать контекст там где этого делать не надо или ошибка (например, при рефакторинге убрали this, а стрелку забыли), или просто непонимание. В кофе важна внимательность к тому, что пишется.
              0
              > В кофе важна внимательность к тому, что пишется.

              Я думаю, это к любому языку программирования (и не только) применимо :)
                0
                :) согласен, все бы этому следовали, было бы счастье. Бывает откроешь чей-то код или свой старый и думаешь «бааа, по рукам бы надавать»
        0
        Я обычно вот так это дело решаю

        Function.prototype.bind = function(bind) {
        	var self = this;
        	return function(){
        		var args = Array.prototype.slice.call(arguments);
        		return self.apply(bind || null, args);
        	};
        };
        
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Тогда не стоит забывать о том, что
            15.3.4.3: In Edition 3, a TypeError is thrown if the second argument passed to Function.prototype.apply is neither an array object nor an arguments object. In Edition 5, the second argument may be any kind of generic array-like object that has a valid length property.


            К примеру, чтобы вот этот пример работал везде:

            Math.max.apply(null, {length: 2, 0: 0, 1: 1}); // 1
            


            Нужно подправить Function.prototype.apply:

            (function(apply) {
              try {
                Function.apply(null, {length: 0});
              }
              catch (error) {
                Function.prototype.apply = function(context, object)
                  {
                    if (Object.prototype.toString.call(object) !== '[object Array]')
                      object = Array.prototype.slice.call(object);
                    
                    return apply.call(this, context, object);
                  }
              }
            }(Function.prototype.apply));
            
            
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                А вот к чему здесь альтернативная реализация apply (которые, в отличие от bind, о котором говорится в топике, поддерживается уже повсюду), честно говоря, не совсем понял.

                Во-первых, не всюду, а только в ES5 реализациях (к примеру, IE7- базируются на ES3).
                Во-вторых, я слишком бегло прочитал ваш код, поэтому приношу извинения.
            +1
            В вашей реализации отсутствует карринг.

            Вот пример вашей реализации с каррингом:
            Function.prototype.bind = function(bind) {
                var slice = Array.prototype.slice, self = this, args = slice.call(arguments,1);
                return function(){
                    return self.apply(bind || null, args.concat(slice.call(arguments)));
                };
            };
            
              0
              Пожалуйста не путайте частичное применение функции с каррированием.
                0
                Спасибо, действительно лучше было бы написать частичное применение, но эта функция может быть использована и для каррирования.
            0
            берём вот эти две функции:
            github.com/nin-jin/fenix/blob/master/Factory.jsm
            github.com/nin-jin/fenix/blob/master/autoBind.jsm

            и получаем:

            var Man= Factory( new function( ){
            this.name= 'anonymous'
            this.init= function( name ){
            this.name= name
            return this
            }
            this.scream= function( ){
            alert( this.name + '!' )
            return this
            }
            } )

            var ninjin= Man( 'Nin Jin' )
            setTimeout( ninjin.scream, 1000 ) // кричит «Nin Jin!»
              0
              У меня такой вопрос:
              var fn = $.proxy(object.f, object);
              setTimeout(fn, 3000);


              зачем использовать дополнительную объект-обёртку, если можно так:
              var fn = function(){ object.f() };
              setTimeout(fn, 3000);


              пс. не бейте сильно ибо я не знаю разницы
                +1
                В данном конктретном случае — разницы особо нету. Но так в топике рассматривается более общие случае на этом примере — читайте ниже. Например,

                var fn = $.proxy(object.f, object);
                window.addElementListener('load', fn, 3000);
                
                // vs
                
                var fn = function(){ object.f() };
                window.addElementListener('load', fn, 3000);
                


                Функция может возвращать результат — надо добавить ретурн:

                var fn = function(){ return object.f() };
                window.addElementListener('load', fn, 3000);
                


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

                var fn = function(){ return object.f.apply(object, argument) };
                window.addElementListener('load', fn, 3000);
                


                В этом месте приходит понимание, что стоит всё это вынести в метод:

                var fn = object.f.bind( object );
                window.addElementListener('load', fn, 3000);
                

                  0
                  Понял, спасибо, «новое понимание в мозг успешно добавлено». У меня обычно проще связи на практике.
                    0
                    Что за add<strong>Element</strong>Listener? Java? Судя по аргументам, вроде, нет. Но тогда что́?!
                      0
                      Парсер Хабрахабра наносит внезапный удар. Чем ему кошерный strong-то не угодил?
                        0
                        В теге <code> можно использовать любые символы, и они не будут восприниматься как html.
                          0
                          А, верно, виноват.
                        0
                        ошибочка. addEventListener, конечно
                          0
                          Уф-ф-ф, а я-то напугался, что у меня такой пробел.
                      0
                      Зачастую, когда это возможно, второй вариант и используют. Дело привычки.
                      Но написанный вами код не всегда будет работать:
                      — когда в f передаётся параметр
                      — когда в f передаётся неизвестное число параметров
                      — когда f объявлена не в object, а где-то ещё, и надо просто вызвать f в контексте object
                      — когда f объявлена не в object, а где-то ещё, и надо просто вызвать f в контексте object, при этом передав туда параметры
                      Конечно, на каждый из этих случаев можно написать обёртку для f в 1-2 строки, но порой проще вызывать proxy/bind.
                        0
                        опс… пока писал, ответили выше
                          0
                          Понятно, спасибо. На практике jQuery юзать приходится не так часто.
                            0
                            Ну на самом деле дело в jQuery. Я вот не использую жЗапрос вообще, но каррингом играю часто — вв крупных приложениях за счёт этого код сокращается.
                        0
                        Ошибочка:
                        > bounded(17); // распечатает 25 (3 + 5 +27)
                        а нужно:
                        bounded(17); // распечатает 25 (3 + 5 + 17)
                          0
                          thx, fixed
                          +2
                          Добавлю, что в Underscore.js есть свой метод bind.
                          Кроме того, есть очень удобный метод bindAll, при помощи которого можно перманентно привязать контекст для всех (или выбранных) функций объекта.

                          var object = {
                              x: 3,
                              f: function() {
                                  console.log(this.x);
                              }
                          }
                          _.bindAll(object);
                          

                          Всё, теперь функция f всегда будет выполняться в контексте объекта object.
                            0
                            Я бы не стал начинать с jQuery объяснение таких базовых особенностей языка. Их надо освоить сперва в голом виде без всяких библиотек, чтобы понимать, как оно работает внутри. А уже потом можно про красивые обёртки.
                              0
                              Использовал такой вариант, писал давно, сейчас не нужен
                              Полифилл
                              Function.prototype.bind = function(thisArg) {
                               function boun() {
                                var arg = part.concat(Array.prototype.slice.call(arguments));
                                return this instanceof boun
                                 ? Function("_", "return new _(" + arg + ")")(self)
                                 : self.apply(thisArg, arg);
                               }
                               var part = Array.prototype.slice.call(arguments, 1), self = this;
                               return boun;
                              };
                              
                              /* Привязывание функций */
                              function bind_fun() {
                               function add($A, $B, $C) {
                                var $D = $A + $B + $C;
                                console.log('Аргументы: %s\nРезультат: %s\n',
                                             Array.prototype.slice.call(arguments), $D);
                                return $D;
                               }
                               bind = add.bind(null, 3, 5);   //
                               bind(7);                       // ==> 15 (3 + 5 + 7)
                               bind(17);                      // ==> 25 (3 + 5 + 17)
                              };
                              
                              
                              // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Example:_Partial_Functions
                              /* частичные функции */
                              function _Partial_Functions() {
                               function Point(a, b) {this.x = a;this.y = b;}
                               Point.prototype.toString = function() {return this.x + "," + this.y;};
                               var bind = Point.bind(null, 0), boun = new bind(5);
                               console.log(boun.toString());  // ==> 0,5
                              };
                              
                              // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Example:_Creating_a_bound_function
                              /* Пример: частичные функции */
                              function _Creating_a_bound_function() {
                               this.x = 9;
                               var module = {x: 81, getX: function() { return this.x; }};
                                $A = module.x                 // ==> 81 Значение свойства 'x' module
                                $B = module.getX();           // ==> 81 Значение метода 'getX' module
                                getX = module.getX;           // ссылка на функцию module.getX
                                $C = getX();                  // ==>  9 this === глобальный объект
                                boundGetX = module.getX.bind(module); // bind функции this === module
                               console.log($A, $B, $C, boundGetX());  // ==> 81 81 9 81 
                              }
                              _Creating_a_bound_function()
                              _Partial_Functions()
                              
                               


                                0
                                Function("_", "return new _(" + arg + ")")(self)


                                Здесь же ж аргумент в строку кастится. То есть такое будет падать:
                                Point.bind(null, { my: 'object' })


                                А такое просто работать неправильно:
                                
                                function Point(depend, a, b) {
                                 this.depend = depend;
                                 this.x = a;
                                 this.y = b;
                                }
                                Point.prototype.toString = function() {return this.x + "," + this.y;};
                                var bind = Point.bind(null, [6, 5, 4]);
                                var boun = new bind(1, 2);
                                console.log(boun.toString()); // какой результат? (5,4)
                                
                                0
                                Спасибо за внимание, я не заметил.

                                Об объектах, как об аргументах раньше и не думал.

                                Строку:
                                Function("_", "return new _(" + arg + ")")(self)
                                На:
                                Function('$,_', 'return new $(' + arg.map(function (a, b) { return '_[' + b + ']'}) + ')')(self, arg)
                                Попробуйте так.
                                Function.prototype.bind = function(thisArg) {
                                 function boun() {
                                  var arg = part.concat(Array.prototype.slice.call(arguments));
                                  return this instanceof boun
                                   ? Function('$,_', 'return new $(' + arg.map(function (a, b) { return '_[' + b + ']'}) + ')')(self, arg)
                                   : self.apply(thisArg, arg);
                                 }
                                 var part = Array.prototype.slice.call(arguments, 1), self = this;
                                 return boun;
                                };


                                  0
                                  Я часто пользуюсь ими как-то так:
                                  for (var i = 0; i < this.buttons.length; i++) {
                                   this.buttons[i].onclick = this.clickHandler.bind(this, this.buttons[i]);
                                  }
                                  


                                  Скажите, зачем усложнять и делать через `eval`, если на mdn есть полифил без него?
                                    +1
                                    Доброго времени суток
                                    @TheShock 5 июня 2016 в 00:11
                                    Скажите, зачем усложнять и делать через `eval`, если на mdn есть полифил без него?
                                    @XGuest 14 июня 2016 в 02:56
                                    Использовал такой вариант, писал давно, сейчас не нужен

                                    Писал давно, зачем и когда не помню может, MDM тогда и не было ;)

                                    == > если на mdn есть полифил
                                    Частичная реализация

                                    Почему код с evel, раньше не понимал — «Конструктор у функции».
                                    Нашел в старых файлах, copy ==> past и все, переписывать для комментария было лень, но для понимания полных стандартных возможностей хватало.

                                    Не использую потому, что bind встроен, если нет, быстрей и проще написать 'apply' к имени-телу функции или конструктора.

                                    Переписал полифил без evel.
                                    Function.prototype.bind = function(thisArg) {
                                     function boun() {
                                      Array.prototype.unshift.apply(arguments, part);    // unshift arguments
                                      return self.prototype.constructor                  // constructor для {} и ()
                                       .apply(thisArg || this, arguments);               // в __proto__
                                     }
                                     Array.prototype.shift.apply(arguments);             // shift arguments
                                     var part = arguments, self = this;
                                     boun.prototype = self.prototype;                    // Для {} 
                                     return boun;                                        // Если () то лишняя команда
                                    };
                                    Но как минимум лишние:
                                    — два вызова функции — полифила и обработчика.
                                    — две обработки аргументов.
                                    — одно условие.

                                    P.S. Читаю ваши статьи, спасибо.

                                    С наилучшими пожеланиями
                                    XGuest
                                      0
                                      ==>Писал давно, зачем и когда не помню может, MDM MDN тогда и не было ;)
                                  0

                                  Извините, забыл об основных конструкторах, исправил.


                                  Function.prototype.bind = function(thisArg) {
                                   function boun() {
                                    Array.prototype.unshift.apply(arguments, part);
                                    return self.apply(thisArg || this, arguments);
                                   }
                                   Array.prototype.shift.apply(arguments);
                                   var part = arguments, 
                                    self = this.prototype
                                     ? this.prototype.constructor
                                     : thisArg;
                                   boun.prototype = self.prototype;
                                   return boun;
                                  };

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое