Если вы читаете эту статью, то наверняка уже пробовали разобраться с тем, для чего в JavaScript существуют методы bind()
, call()
и apply()
. Проблема понимания этих методов заключается в том, что их часто преподносят таким образом, что они кажутся более сложными, чем они хотелось бы, и вы снова откладываете возможность разобраться с bind()
, call()
и apply()
до лучших времен.
Что же такое особенное есть в bind (), call () и apply()? Как они работают?
Чтобы ответить на этот вопрос, сначала важно вспомнить, что в JavaScript все функции являются объектами. Это означает, что они могут иметь свойства и методы, как и любой другой объект. Функции - это особый тип объектов, поскольку они имеют множество встроенных свойств и методов, три из которых - call()
, apply()
и bind()
.
Для того чтобы разобраться со всеми этими методами, мы создадим объект person с двумя свойствами и методом. Метод будет задан функцией showFullName()
, использующей ключевое слово this.
1 2 3 4 5 6 7 |
const person = { firstname: 'James', lastname: 'Murray', showFullName: function(){ return this.firstname + ' ' + this.lastname; } } |
Кроме того, нам понадобится функция getSkills()
, которая у нас будет существовать отдельно от объекта person:
1 2 3 4 5 |
function getSkills(s1, s2){ console.log(this.showFullName() +' has skills: '+s1+', '+s2); } getSkills('HTML/CSS', 'JavaScript'); person.getSkills('HTML/CSS', 'JavaScript'); |
Эта функция выводит некоторую конформацию о навыках нашего объекта, используя его собственную функцию showFullName()
и передаваемые в нее параметры. Мы будем использовать методы bind()
, call()
и apply()
для того, чтобы иметь возможность вызвать функцию getSkills()
для нашего объекта person.
Если мы вызовем getSkills()
прямо сейчас, мы получим ошибку (4 и 5-строки кода). Это связано с тем, что функция getSkills()
определена в глобальной области видимости, поэтому this
указывает на глобальный объект window, у которого нет метода getFullName()
, а у объекта person отсутствует метод getSkills()
.
this
фактически указывает на объект, в котором оно содержится, а НЕ на функцию (это странная особенность JavaScript может привести к ошибкам, но не с теми методами, которые мы здесь рассматриваем).Метод bind()
Из документации на MDN узнаем, что
Метод
bind()
создаёт новую функцию, которая при вызове устанавливает в качестве контекста выполненияthis
предоставленное значение. В метод также передаётся набор аргументов, которые будут установлены перед переданными в привязанную функцию аргументами при её вызове.
С английского bind переводится, как связывать. То есть этот метод позволяет связать наш объект person с ключевым словом this
во внешней функции через вспомогательную функцию.
Вспомогательной функцию назовем personSkills
, и запишем в нее вызов глобальной функции getSkills
, передав через метод bind() наш объект person
. Теперь ключевое слово this указывает на объект person
, который мы передали в качестве аргумента в bind()
. Код теперь выглядит так:
1 2 3 4 5 |
let personSkills = getSkills.bind(person); personSkills('HTML/CSS', 'JavaScript'); //или 2-м способом, указав параметры вызова функции сразу let personSkills = getSkills.bind(person, 'HTML/CSS', 'JavaScript'); personSkills(); |
В результате таких действий в консоли браузера будет выведено:
1 |
James Murray has skills: HTML/CSS, JavaScript |
Обратите внимание на то, что метод bind() всегда возвращает новую функцию. Он создает копию getSkills() и сообщает механизму JavaScript: «Каждый раз, когда вызывается эта копия getSkills()
, установите ключевое слово this
для ссылки на person
во время ее выполнения». Это отличает bind()
от методов call()
и apply()
, которые ждут нас впереди.
Последнее небольшое замечание о bind()
: мы можем вызвать bind()
только один раз для определенного объекта. Попытка использовать возвращаемую им фунцию для другого объекта ничего не даст. Проиллюстрируем пример с функцией, выводящей некоторые сообщения в консоль от разных объектов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function printPhrase() { return console.log(this.phrase); }; const holidayNY = { phrase: "С Новым Годом!", }; const holiday8March = { phrase: "С 8 Марта!", }; // связываем printPhrase() и объект holidayNY const printNY = printPhrase.bind(holidayNY); printNY(); //выведет "С Новым Годом!" //пытаемся связать второй объект через функцию printNY const print8March = printNY.bind(holiday8March); print8March(); //выведет "С Новым Годом!" //меняем функцию связывания на printPhrase() const print8March_2 = printPhrase.bind(holiday8March); print8March_2(); // выведет "С 8 Марта!" |
Метод call()
Отличием метода call() от bind() является то, что нет необходимости создавать вспомогательную функцию для того, чтобы передать в качестве this нужный объект. В документации на MDN читаем:
Метод
call()
вызывает функцию с указанным значениемthis
и индивидуально предоставленными аргументами.
Несколько переписываем наш код:
1 |
getSkills.call(person, 'PHP', 'Python'); //James Murray has skills: PHP, Python |
Очень просто по сравнению с bind()
, не так ли?
Вместо аргументов "HTML/CSS" и "JavaScript" при вызове глобальной функции getSkills()
были использованы 'PHP' и 'Python', чтобы посмотреть на работоспособность вызова функции с call()
- и все сработало! В отличие от bind()
, call() не копирует функцию. Он позволяет передавать объект в качестве this
и любые аргументы, а затем немедленно вызывает функцию.
Метод apply()
Методы apply()
и call()
практически идентичны при работе с выставлением значения this
, за исключением того, что вы передаёте параметры функции в apply()
как массив, в то время, как в call()
, параметры передаются в индивидуальном порядке. Давайте опять обратимся к документации на MDN:
Метод
apply()
вызывает функцию с указанным значениемthis
и аргументами, предоставленными в виде массива (либо массивоподобного объекта).
Это значит, что при вызове apply()
для нашей функции getSkills()
параметры нужно передать в квадратных скобках, как массив - и все. Результат:
1 |
getSkills.apply(person, ['C++', 'C#']); //James Murray has skills: C++, C# |
Вот, собственно, и все, что касается применения методов bind()
, call()
и apply()
для связывания this
объекта и внешней функции. Однако практическое использование этих методов не исчерпывается только лишь этой ситуацией. Например, эти методы с успехом можно применять для различных действий с массивами или псевдомассивами (аргументами функций или коллекциями HTML-элементов).
Практическое применение методов call(), apply(), bind()
Метод call()
Метод call()
периодически используется (или, скорей, использовался ранее) для преобразование массивоподобных объектов в массивы. Так, например, свойство arguments
, которое существует внутри каждой функции - это не совсем массив, хотя каждый элемент в нем имеет свой числовой индекс. Это можно проверить строкой ниже, которая выведет в консоль false
:
1 |
console.log(arguments instanceof Array); //false |
Для того чтобы преобразовать arguments
в массив, используем call()
при вызове метода массива Array.prototype.forEach
. Будем рассматривать функцию подсчета суммы различного количества чисел:
1 2 3 4 5 6 7 8 9 |
function summa() { console.log(arguments instanceof Array); //false var sum = 0; Array.prototype.forEach.call(arguments, (value) => sum += value); console.log(sum); } summa(1, 2, 3, 4, 5, 6); //21 summa(10, 20, 30, 40, 50, 60, 100, 200); //510 summa(30, 5, -15, -20); //0 |
Раньше для преобразования коллекций в массивы приходилось использовать подобные конструкции:
1 2 3 4 5 6 7 8 |
var links = document.getElementsByTagName('a'); var linksArr = Array.slice.call(links); // Или более короткий вариант var linksArr = [].slice.call(document.links); Array.isArray(links); // false Array.isArray(linksArr); // true |
Сейчас можно воспользоваться методом Array.from(псевдомассив)
для получения такого же результата:
1 2 3 |
let allLinks = document.links; // псевдомассив allLinks = Array.from(allLinks); // массив |
Пример использования метода call()
совместно с методом массивов slice()
для преобразования коллекции html-элементов (псевдомасива) в обычный массив:
See the Pen Array.slice.call() for HTMLCollection by Elen (@ambassador) on CodePen.
Практический пример использования метода call()
c codepen.io от автора Kyle Edwards при работе с массивами объектов.
See the Pen Meaningful Transitions by Kyle Edwards (@kyledws) on CodePen.
Метод apply()
Вы можете вывести элементы массива в консоль с помощью метода apply()
либо воспользоваться для этого появившимся в стандарте ES6 оператором spread ...
:
1 2 3 |
console.log.apply(null, [1, 2, 3]); // 1 2 3 // Равнозначно console.log(...[1, 2, 3]); // 1 2 3 |
Для определения минимального и максимального элемента в массиве есть вариант использования метода apply()
для Math.min()
и Math.max()
от создателя jQuery Джона Ресига:
1 2 3 4 5 6 7 8 9 10 |
//обычная функция Array.max = function( array ){ return Math.max.apply( Math, array ); }; //стрелочная функция Array.min = array => Math.min.apply( Math, array ); const arr1 = [4, -7, 99, 120, 0, -3, 18]; console.log(Array.max(arr1)); //120 console.log(Array.min(arr1)); //-7 |
Альтернатива применению методу apply()
- использование spread-оператора для передачи массива в методы объекта Math.
1 2 3 |
let min = Math.min( ...arr1 ), max = Math.max( ...arr1 ); console.log(min, max); // -7 120 |
Метод bind()
В примере ниже метод bind() поможет нам создать выборку, подобную jQuery селекторам, а затем применить метод addEventListener для создания обработчиков событий, подобных jQuery-методу on():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var $ = document.querySelectorAll.bind(document); Element.prototype.on = Element.prototype.addEventListener; console.log($('#bindtest')[0]); //ссылка на второй абзац примера // обработка события клика по среднему абзацу $('#bindtest')[0].on('click', function(){ let fontSize = parseInt(this.style.fontSize || window.getComputedStyle(this).getPropertyValue('font-size')); this.style.fontSize = fontSize+2+'px'; }); console.log($('.example')); // ссылка на 3 абзаца с классом example NodeList(3) [p.example, p#bindtest.example, p.example] function randomColor () { let r = parseInt(Math.random()*256); let g = parseInt(Math.random()*256); let b = parseInt(Math.random()*256); return `rgb(${r}, ${g}, ${b})`; } // обработка события наведения на каждый из абзацев $('.example').forEach(item => item.on('mouseover', function(){ this.style.backgroundColor = randomColor(); })) |
Посмотрим, как работает пример в действии. Наведите на любой из абзацев - и увидите изменение его фона. Клик на среднем абзаце будет увеличивать его размер, отталкиваясь либо от текущего значения font-size в атрибуте style этого элемента, или от того, который берется из таблицы стилей с помощью метода window.getComputedStyle().
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Debitis, inventore reprehenderit alias, rem deserunt repudiandae id nihil vitae corporis! Odit veniam unde, tempora voluptatem iure adipisci impedit velit quidem labore?
Porro nisi provident nostrum aspernatur, voluptates ex consequatur quaerat modi, quis iste, sapiente atque error vero temporibus velit maxime dolore. Accusantium quibusdam est enim doloribus. Quod sint quaerat quisquam nemo!
Blanditiis doloremque sed reprehenderit, magni neque aut a itaque aperiam consequatur consectetur inventore, earum, dolorum aspernatur odio repellat incidunt aliquid non. Ab aperiam accusantium ducimus ratione, ea odit delectus repudiandae.
Аналогичным способом можно добавить обработку событий на тач-экранах:
1 2 3 4 5 |
$('#mylink')[0].on('touchstart', myTouchHandle); function myTouchHandle(){ // ваш код } |
Каррирование (или карринг - currying) – это термин функционального программирования, который означает создание новой функции путём фиксирования аргументов существующей.
При каррировании функции создается копия функции с некоторыми предустановленными параметрами. Чем может быть полезен для этого bind()
, если он тоже занимается созданием копии функции?
1 2 3 4 5 6 7 |
let multiply = (a, b) => a * b //стандартный вызов функции console.log(multiply(4,6)); // 24 //Используем bind() var multiplyByTwo = multiply.bind(this, 2); //вызов вспомогательной функции с одним аргументом console.log(multiplyByTwo(24)); //48 |
Пример использования объекта для открытия/закрытия модального окна с помощью метода bind()
В этом примере метод bind()
понадобится в 2-х случаях:
- При вызове определенного метода объекта во время срабатывания события клика, когда
this
переходит к тому объекту, на котором был сделан клик - При вызове определенных методов объекта с задержкой с помощью метода
setTimeout()
.
HTML-разметка примера:
1 2 3 4 5 6 7 8 |
<div class="my-popup"> <div class="modal"> <span class="mypopup-close">×</span> <h2 class="mypopup-header">Вы открыли модальное окно</h2> <p class="modal-text">Здесь размещается текст</p> </div> </div> <a href="javascript: void(0)" id="addPopup">Открыть модальное окно</a> |
Открываем модальное окно по клику на ссылке с id="addPopup"
.
CSS-стили примера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
body.popup-open { overflow-y: hidden; } .my-popup.open { background-color: rgba(0, 0, 0, 0.6); position: fixed; z-index: 30; left: 0; right: 0; top: 0; bottom: 0; display: flex; align-items: center; justify-content: center; } .my-popup { display: none; } .my-popup .modal { width: 80%; min-height: 80px; max-width: 500px; padding: 20px; background-color: #fff; border-radius: 10px; text-align: center; position: relative; } .mypopup-header { margin: -20px; margin-bottom: 15px; padding: 10px; background-color: #378dd8; color: #fff; } .mypopup-close { position: absolute; font-size: 2rem; font-weight: bold; right: 10px; top: 5px; z-index: 2; cursor: pointer; } |
Изначально модальное окно .my-popup
скрыто с помощью свойства display: none
и будет открыто при добавлении к нему класса .open
.
JavaScript-код примера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
var popupViewer = { container: document.body, popup: document.querySelector('.my-popup'), close: document.querySelector('.mypopup-close'), activateModal: function(state) { setTimeout(function() { this.addClass(this.container, 'popup-open'); this.addClass(this.popup, 'open'); }.bind(this), 300); }, deactivateModal: function(state) { setTimeout(()=> { this.removeClass(this.popup, 'open'); this.removeClass(this.container, 'popup-open'); }, 500); }, addClass: function(element, name) { element.classList.add(name); }, removeClass: function(element, name) { element.className = element.className.replace(name, ''); } }; addPopup.addEventListener('click', popupViewer.activateModal.bind(popupViewer)); popupViewer.close.addEventListener('click', popupViewer.deactivateModal.bind(popupViewer)); |
Тестируем пример:
Вы открыли модальное окно
Здесь размещается текст
В коде мы используем метод bind(), чтобы связать this
с объектом popupViewer
, который имеет те методы, которые мы вызываем при клике. Однако, при использовании метода addEventListener
ключевое слово this
указывает либо на ссылку, либо на вложенный объект. Поэтому с помощью метода bind()
мы возвращаем this
к объекту popupViewer
(строки 26, 27).
Когда в коде используются методы setTimeout()
или setInterval()
, которые принадлежат объекту window
, this
также перестает указывать на объект, внутри которого они вызываются, и относится к window.
Поэтому, необходимо "забайндить" this
, указав для setTimeout()
в строке 10, что мы будем использовать внутри функции ссылку на текущий объект. Вторым способом "вернуть" ссылку на this
является использование стрелочной функции (строка 13).
Примеры использования метода bind
Счетчик от Jon Kantner
See the Pen Bouncy Counter by Jon Kantner (@jkantner) on CodePen.
И еще один пример использования функции bind()
- игра Память (Memory) на основе класса MemoryGame.
See the Pen Memory Game by Jonathan Tarnate (@jeytii) on CodePen.
Ссылки по теме: