|
|||||||
Привязка контекста (this) к функции в javascript и частичное применение функций
Время создания: 08.03.2019 11:44
Текстовые метки: javascript function this bind context
Раздел: Javascript
Запись: Velonski/mytetra-database/master/base/1552027471f2yntdp3qg/text.html на raw.githubusercontent.com
|
|||||||
|
|||||||
В предыдущем посте я описал, что 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. Совместимость с браузерами Эмуляция Function.prototype.bind из Mozilla Developer Network 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'); Ответы Прежде чем разобрать — я перечислю основные особенности 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, при этом я старался оставить только важные детали (с моей точки зрения). Если вы заметили ошибки/неточности или хотите что-то уточнить/добавить — напишите в ЛС, поправлю |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|