Подробно о методах apply(), call() и bind(), необходимых каждому JavaScript разработчику

Stas Bagretsov
13 min readJan 13, 2019

--

Вообще, эти методы довольно старые, но их понимание не становится от этого менее необходимым при изучении JavaScript. В этой статье вы узнаете о том, как работают apply(), call() и bind(), а также поймете на реальных примерах, что они на самом делают и как смогут вам пригодится в реальной жизни.

Адаптивный и дополненный перевод статьи JavaScript’s Apply, Call, and Bind Methods are Essential for JavaScript Professionals

👉Мой Твиттер — там много из мира фронтенда, да и вообще поговорим🖖. Подписывайтесь, будет интересно: ) ✈️

В JavaScript функции это объекты, как вы должны были бы уже знать. И как объекты, функции имеют свои методы, включая такие действенные, как apply(), call() и bind(). Можно сказать, что Apply() и Call() буквально идентичны друг другу и зачастую используются в JavaScript для того, чтобы заимствовать методы и выставлять значения this. Также мы используем Apply() для функций с большим количеством переменных и аргументов, но об этом вы узнаете дальше в статье.

Bind() же мы используем для выставления значения this в методах и для каррирования функций.

Мы рассмотрим каждый сценарий, в котором будет использоваться каждый из трех методов. Давайте начнем с метода bind().

Метод bind()

В основном, мы используем метод bind(), чтобы вызывать функцию с указанием значения this. А другими словами, bind() позволяет нам легко выставлять какой именно объект будет привязан к this в момент вызова функции или метода.

Это может показаться относительно тривиальным, но зачастую значение this в методах и функциях должно быть выставлено явным способом, когда вам нужно привязать конкретный объект к значению this самой функции.

Обычно нам требуется bind() тогда, когда мы используем в методе this и вызываем сам метод из получающего объекта. В таких случаях, иногда this не привязывается к предполагаемому объекту, что само собой ведет к ошибке в работе кода. Так, сейчас без паники, если вы полностью не поняли предыдущее предложение. Всё скоро станет кристально ясным. За этим вы и читаете эту статью.

Перед тем, как взглянуть на код ниже, вы должны понять то, как работает this в JavaScript. Для этого прочтите статью — Подробно о том, как работает this в JavaScript. Если вы не будете понимать его работы, то вы встретите множество затруднений в понимании концепций, описанных ниже. Так что, лучше не ленитесь и прочтите её, потратьте время, оно вам окупится. А что будет ещё лучше, бонусом прочтите Разбираемся с “поднятием” (hoisting) в JavaScript

Bind() позволяет нам выставить значение this для метода

При клике на кнопку ниже, текстовое поле будет заполнено рандомным именем.

var user = {
data: [
{name:"T. Woods", age:37},
{name:"P. Mickelson", age:43}
],
clickHandler: function (event) {
var randomNum = ((Math.random () * 2 | 0) + 1) - 1; // Случайное число от 0 до 1.

// Эта строка добавляет случайного человека из массива данных в текстовое поле.
$("input").val(this.data[randomNum].name + " " + this.data[randomNum].age);
}

}

// Назначаем eventHandler на событие по клику на кнопку
$ ("button").click (user.clickHandler);

Итак, когда вы кликаете на кнопку, вы получаете ошибку, так как clickHandler() метод привязан к HTML элементу — кнопке, следовательно, он считается объектом на котором выполняется метод.

Вообще, эта проблема довольно распространена в JavaScript и фреймворки, такие как Backbone.js, вместе с библиотеками типа jQuery автоматически делают привязывание за нас, таким образом, что this всегда будет связан с объектом, работу с которым мы подразумеваем в нашем коде.

Чтобы исправить эту проблему в предыдущем примере, мы можем использовать метод bind():

Следовательно, вместо этой строки:

$ ("button").click (user.clickHandler);

Мы просто свяжем clickHandler с объектом user, как тут:

$ ("button").click (user.clickHandler.bind (user));

Давайте продолжим. Значение this, также привязывается к другому объекту, если мы назначаем метод (где this определено) на переменную. В этом примере вы это увидите:

// Переменная data является глобальной
var data = [
{name:"Samantha", age:12},
{name:"Alexis", age:14}
]

var user = {
// а это уже локальная
data: [
{name:"T. Woods", age:37},
{name:"P. Mickelson", age:43}
],
showData: function (event) {
var randomNum = ((Math.random () * 2 | 0) + 1) - 1; // Любое число с 0 до 1

console.log (this.data[randomNum].name + " " + this.data[randomNum].age);
}

}

// Назначаем метод showData от объекта переменной
var showDataVar = user.showData;

showDataVar (); // Samantha 12 (Данные взялись из глобального массива данных, а не из локального в объекте)

Когда мы выполняем функцию showDataVar(), значения, выводимые в консоль идут из глобального data массива, а не из data массива внутри объекта user. Так получается, потому что showDataVar() выполняется как глобальная функция и использование this внутри showDataVar() привязано к глобальному полю видимости, что и является объектом window самого браузера.

И снова, мы можем пофиксить эту проблему, указав точно значения this c помощью метода bind().

//  Привязываем метод showData к объекту user
var showDataVar = user.showData.bind (user);

// Теперь мы получаем значение из объекта user, так как this привязано к объекту
showDataVar (); // P. Mickelson 43

С помощью bind() мы можем заимствовать методы

В JavaScript мы можем передавать функции куда угодно, возвращать их, а ещё заимствовать и всё тому подобное. А с помощью метода bind() это будет просто ну супер как легко сделать и в особенности это касается заимствования методов.

Вот пример использования bind(), чтобы позаимствовать метод:

//  Тут у нас объект с данными о машинах, у которого нет метода для вывода своих данных в консоль
var cars = {
data:[
{name:"Honda Accord", age:14},
{name:"Tesla Model S", age:2}
]

}

// Мы можем взять метод showData() из объекта user, который мы сделали в предыдущем примере
// Ниже мы свяжем метод user.showData с объектом cars
cars.showData = user.showData.bind (cars);
cars.showData (); // Honda Accord 14

Тут проблема заключается в том, что мы добавляем новый метод (showData) объекту cars и возможно нам просто не нужно его заимствовать, потому что сам объект уже может иметь свойство или метод под названием showData. В общем, мы не хотим его случайно переписать. Как будет показано далее, на примерах Apply() и Call(), лучше всего заимствовать методы используя либо Apply() или Call().

С помощью bind() мы можем каррировать функцию

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

Давайте уже применим метод bind() и каррирование вместе. Для начала, у нас есть простенькая функция greet(), которая принимает 3 параметра:

function greet (gender, age, name) {
// Если мужчина, то используем Mr., если нет то Ms..
var salutation = gender === "male" ? "Mr. " : "Ms. ";

if (age > 25) {
return "Hello, " + salutation + name + ".";
}
else {
return "Hey, " + name + ".";
}
}

И мы используем bind() метод, чтобы каррировать нашу функцию greet(). Как мы говорили ранее, первый аргумент метода bind() будет иметь значение this.

//  В общем, мы передаем null, так как мы не используем this в функции
var greetAnAdultMale = greet.bind (null, "male", 45);

greetAnAdultMale ("John Hartlove"); // "Hello, Mr. John Hartlove."

var greetAYoungster = greet.bind (null, "", 16);
greetAYoungster ("Alex"); // "Hey, Alex."
greetAYoungster ("Emma Waterloo"); // "Hey, Emma Waterloo."

Используя метод bind() для каррировния, все наши параметры greet(), кроме последнего аргумента, подставляются автоматически. Таким образом этот аргумент, который мы меняем, используется при вызове новых функций, каррированных с greet().

В общем, с помощью метода bind(), мы можем выставлять значение для вызова методов на объекте, заимствовать и копировать методы, а также назначать методы переменным, которые выполнятся как функции.

Методы Apply и Call

Два этих метода чуть ли не самые используемые в JavaScript и это неспроста: с их помощью мы можем заимствовать функции и выставлять значение this в вызове функции. Более того, функция apply() в буквальном смысле позволяет нам выполнять функцию в массиве параметров, как-будто каждый параметр передаётся функции индивидуально, при её выполнении — отличное решение для вариативных функций; такие функции берут варьирующееся число аргументов, а не заданное количество, как делается в большинстве функций.

Выставляем значение this с помощью Apply или Call

Как и в примере с bind(), мы также можем указывать значение для this, когда вызываем функцию используя методы apply() и call(). Первый параметр в call() и apply() выставляет this значение объекту на котором вызвана функция.

Вот очень быстрый и показательный пример для начинающих, перед тем, как мы окунёмся в использование Apply и Call:

//  Глобальная переменная для демонстрации
var avgScore = "global avgScore";

// Функция
function avg (arrayOfScores) {
// Складываем все показатели
var sumOfScores = arrayOfScores.reduce (function (prev, cur, index, array) {
return prev + cur;
});

// В этом случае this будет привязан к глобальному объекту, пока мы не выставим его с call() или apply()
this.avgScore = sumOfScores / arrayOfScores.length;
}

var gameController = {
scores: [20, 34, 55, 46, 77],
avgScore:null
}

// Если мы выполним функцию avg, то this внутри функции будет привязана к глобальному объекту:
avg (gameController.scores);
// Вот, что получаем:
console.log (window.avgScore); // 46.4
console.log (gameController.avgScore); // null

// Сбрасываем avgScore
avgScore = "global avgScore";

// Чтобы указать, что значение this привязано к gameController,
// Мы вызываем call() метод:
avg.call (gameController, gameController.scores);

console.log (window.avgScore); //global avgScore
console.log (gameController.avgScore); // 46.4

А сейчас очень внимательно! Обратите внимание, что первый аргумент в call() выставляет значение this. В предыдущем примере, оно выставляется на объект gameController. А другие аргументы, после первого, передаются как параметры функции avg().

Методы apply() и call() практически идентичны при работе с выставлением значения this, за исключением того, что вы передаёте параметры функции в apply() как массив, в то время, как в call(), параметры передаются в индивидуальном порядке. Но дальше ещё интереснее. У apply() есть ещё одна фича, которой нет у call() и очень скоро вы о ней узнаете.

Используем call() и apply(), чтобы выставлять this в Callback функциях

//  Создаём объект со свойствами и методами
// Далее мы передадим метод, как колбэк другой функции
var clientData = {
id: 094545,
fullName: "Not Set",
// Метод на объекте clientData
setUserName: function (firstName, lastName) {
// тут мы выставляем fullName свойство в данном объекте
this.fullName = firstName + " " + lastName;
}
}

Сама функция, тут очень внимательно:

function getUserInput (firstName, lastName, callback, callbackObj) {
// Использование метода apply ниже, выставит this для callbackObj
callback.apply (callbackObj, [firstName, lastName]);
}

Метод apply() выставляет значение this для callbackObj. Так мы можем выполнить колбэк с указанным значением this, так что параметры переданные колбэку, будут выставлены на объекте clientData:

// Объект clientData будет использоваться методом Apply, чтобы выставить значение this.
getUserInput ("Barack", "Obama", clientData.setUserName, clientData);
// Получаем в консоль
console.log (clientData.fullName); // Barack Obama

Методы apply(), call() и bind() всегда используются для того, чтобы выставлять значение this, при вызове метода, но каждый слегка по-своему и это отображается на том, как мы можем контролировать и поддерживать наш код. Значение this в JavaScript также важно, как и любая другая часть языка. Помните, что у нас есть 3 вышеупомянутых метода, которые просто необходимы для эффективной и правильной работы с этим самым this. Всегда старайтесь перепроверять то, что предполагается на месте this.

Заимствование функций с помощью Apply и Call (Важно знать)

Вообще Apply() и Call() в JavaScript используют в основном, чтобы заимствовать функции. С этими методами, мы можем выполнять заимствования также, как и с методом bind(), но при этом быть более гибкими. Давайте рассмотрим следующие примеры:

Заимствуем методы массива

У массивов есть несколько полезных методов для перебора и изменения массива, но к сожалению, у объектов нет стольких методов по-дефолту. Однако, мы можем выражать объект как массив (в виде массивоподобного или как иногда говорят — итерируемого объекта), так как все методы работы с массивами стандартны, мы можем смело их заимствовать и использовать на этих самых массивоподобных объектах.

Массивоподобный или итерируемый (array-like) объект — это объект ключи которого определены как неотрицательные целые числа. Они особенно хороши для добавления свойства length объекту. Почему? Потому что этого свойства не существует в объектах, но оно есть у массивов.

Тут нужно подчеркнуть, что в следующих примерах, когда мы вызываем Array.prototype, мы подтягиваем массив объекта и его прототип, в котором все его методы наследуются. И получается, что именно отсюда мы заимствуем методы массивов. Следовательно, использование кода, такого как Array.prototype.slice приведет к вызову метода slice, который определён в прототипе Array.

Давайте создадим массивоподобный объект и позаимствуем некоторые методы у массивов, чтобы применить их на нём же. Помните, что такой объект (array-like) это реальный объект и в целом это не массив:

//  Массивоподобный объект: тут обратите внимание на то, что неотрицательные целые числа используются как ключи
var anArrayLikeObj = {0:"Martin", 1:78, 2:67, 3:["Letta", "Marieta", "Pauline"], length:4 };

Теперь, если вам надо применить какие-либо из распространенных методов массивов на нашем объекте, мы можем сделать следующее:

//  Делаем быструю копию и сохраняем её в реальный объект:
// Первый параметр выставляет значение “this”
var newArray = Array.prototype.slice.call (anArrayLikeObj, 0);

console.log (newArray); // ["Martin", 78, 67, Array[3]]

// Ищем Мартина в нашем массивоподобном объекте
console.log (Array.prototype.indexOf.call (anArrayLikeObj, "Martin") === -1 ? false : true); // true

// А теперь давайте применим indexOf без вызова call() или apply()
console.log (anArrayLikeObj.indexOf ("Martin") === -1 ? false : true); // Error: Object has no method 'indexOf'

// Переворачиваем объект:
console.log (Array.prototype.reverse.call (anArrayLikeObj));
// {0: Array[3], 1: 67, 2: 78, 3: "Martin", length: 4}

// Клёво, мы даже можем вызвать pop():
console.log (Array.prototype.pop.call (anArrayLikeObj));
console.log (anArrayLikeObj); // {0: Array[3], 1: 67, 2: 78, length: 3}

// А что у нас с push()?
console.log (Array.prototype.push.call (anArrayLikeObj, "Jackie"));
console.log (anArrayLikeObj); // {0: Array[3], 1: 67, 2: 78, 3: "Jackie", length: 4}

Тадам! Мы получаем все прелести работы над объектами и при этом нам доступны методы массивов, когда мы делаем наш объект массивоподобным и заимствуем дефолтные возможности работы с опять-таки массивами.

Объект arguments который является свойством всех функций в JavaScript — является массивоподобным объектом и по этой причине, одним из самых популярных применений call() и apply() методов это извлечение параметров переданных функции из этого объекта.

function transitionTo (name) {
// Так как объект arguments это массивоподобный объект
// Мы можем использовать на нём slice ()
// Число один в параметре говорит о том, нужно отдать копию массива от параметра с индексом 1 и до конца. Или простым языком, просто пропустить первый элемент.

var args = Array.prototype.slice.call (arguments, 1);

// Я добавил этот кусочек кода, чтобы мы могли видеть то, что получится в args.

// Я закомментировал последнюю строку, потому что она не в тему этого примера.
//doTransition(this, name, this.updateURL, args);
}

// Так как метод slice() скопировал все элементы начиная от индекса 1 до конечного, первый элемент “contact” не был отдан.
transitionTo ("contact", "Today", "20"); // ["Today", "20"]

Переменная args это реальный массив. В нём содержатся все параметры, переданные функции transitionTo.

С этого примера мы узнали быстрый способ получения всех аргументов, переданных функции. А именно:

//  Мы пока не определяем функцию с какими-либо параметрами, но можем получить все переданные ей аргументы
function doSomething () {
var args = Array.prototype.slice.call (arguments);
console.log (args);
}

doSomething ("Water", "Salt", "Glue"); // ["Water", "Salt", "Glue"]

Мы обсудим то, как использовать метод apply() c arguments массивоподобным объектом немного позже, но уже на примере вариативных функций.

Заимствование строчных методов с помощью Apply() и Call()

Как и в предыдущем примере, мы также можем использовать apply() и call(), чтобы заимствовать строчные методы. Так как строки неизменяемые (иммутабельны), то только неманипулятивные массивы работают с ними, так что вы не сможете использовать reverse, pop и т.п.

Заимствование чужих методов и функций

Так как мы заимствуем, то давайте пойдем ва-банк и позаимствуем наши собственные кастомные методы и функции, а не только из Array (массивов) или из String (строк):

var gameController = {
scores :[20, 34, 55, 46, 77],
avgScore:null,
players: [
{name:"Tommy", playerID:987, age:23},
{name:"Pau", playerID:87, age:33}
]
}

var appController = {
scores: [900, 845, 809, 950],
avgScore:null,
avg :function () {

var sumOfScores = this.scores.reduce (function (prev, cur, index, array) {
return prev + cur;
});

this.avgScore = sumOfScores / this.scores.length;
}
}

// Обратите внимание, что тут мы используем apply(), так что вторым аргументом должен быть массив
appController.avg.apply (gameController);
console.log (gameController.avgScore); // 46.4

// appController.avgScore до сих пор null; он не изменился, только gameController.avgScore
console.log (appController.avgScore); // null

Будьте уверены, это не сколько просто, сколько рекомендуется — заимствовать наши собственные кастомные функции и методы. Объект gameController заимствует метод appController под именем avg(). Значение this, определенное в методе avg() будет выставлено первым параметром — то есть объектом gameController.

Вы возможно хотите знать, что случится если метод, который мы заимствуем изменится. Изменится ли сам заимствованный (скопированный) метод или этот метод останется полной копией оригинала без любой взаимосвязи с ним? Давайте ответим на эти вопросы быстрым и показательным примером:

appController.maxNum = function () {
this.avgScore = Math.max.apply (null, this.scores);
}

appController.maxNum.apply (gameController, gameController.scores);
console.log (gameController.avgScore); // 77

Как и ожидалось, если мы изменим оригинальный метод, то изменения коснуться заимствованных примеров этого метода. Это ожидаемо по причине того, что мы никогда не делаем полную копию метода, мы просто её заимствуем (Ссылаясь на её актуальную реализацию).

Используем apply() для выполнения функций с составными переменными

Чтобы окончательно закончить наш разговор на тему многогранности и полезности методов Apply(), Call() и Bind(), мы обсудим одну очень интересную возможность, которую нам даёт метод Apply(). А именно, выполнение функции с массивом аргументов.

Мы можем передать массив с аргументами функции и воспользовавшись преимуществом метода apply(), функция будет работать с элементами массива так, как если бы мы запустили её таким образом:

createAccount (arrayOfItems[0], arrayOfItems[1], arrayOfItems[2], arrayOfItems[3]);

Эта техника, в частности, используется для создания функций из составных переменных или как ещё известно — вариативных функций.

Это функции, которые принимают любое число аргументов, а не фиксированное. Арность функции указывает число аргументов, которое функции было определено принять.

Math.max() метод это общеизвестный пример вариативной функции в JavaScript:

//  Мы можем передать любое число аргументов Math.max () методу
console.log (Math.max (23, 11, 34, 56)); // 56

Но, что если у нас массив из чисел, которые нам нужно передать к Math.max? Вот так мы сделать не сможем:

var allNumbers = [23, 11, 34, 56];
// Попросту мы не можем передать массив чисел методу
console.log (Math.max (allNumbers)); // NaN

Вот тут, с вариативными функциями, нам и поможет метод apply(). Вместо того, что мы увидели сверху, мы передадим массив чисел используя apply(), следовательно, код будет таким:

var allNumbers = [23, 11, 34, 56];
// Используя метод apply(), мы передаём числа:
console.log (Math.max.apply (null, allNumbers)); // 56

Как мы уже раньше узнали, первый аргумент в apply() выставляет значение this, но this не используется в Math.max() методе, поэтому мы передаём null.

Вот пример вариативной фукнции, чтобы ещё лучше проиллюстрировать концепцию использования метода apply():

var students = ["Peter Alexander", "Michael Woodruff", "Judy Archer", "Malcolm Khan"];

// Не указываем конкретное число параметров, так как любое число параметров допустимо
function welcomeStudents () {
var args = Array.prototype.slice.call (arguments);

var lastItem = args.pop ();
console.log ("Welcome " + args.join (", ") + ", and " + lastItem + ".");
}

welcomeStudents.apply (null, students);
// Welcome Peter Alexander, Michael Woodruff, Judy Archer, and Malcolm Khan.

Заключение

Call(), Apply() и Bind() методы определенно рабочие лошадки и просто обязаны быть частью вашего JavaScript инструментария, особенно в плане выставления значения this у функций, для создания и выполнения вариативных функций и конечно же для заимствования методов и функций. Как JavaScript разработчик, вы с большой долей вероятности будете полагаться и использовать эти функции, снова и снова. Так что убедитесь в том, что вы хорошо их поняли.

--

--

Stas Bagretsov

Надеюсь верую вовеки не придет ко мне позорное благоразумие. webdev/sports/books