May 3, 2020

this, call, apply, bind

Сегодня хотелось бы рассказать о таких надуманно сложных вещах как call, bind, apply, ну и, соответственно, затронуть this. Все эти слова связаны одним словом – контекст. Но, обо всем по-порядку.

Многие боятся даже подходить к этой теме потому что им кажется, что это что-то невероятно сложное и непонятное. Поэтому, надеюсь, что сегодня я развенчаю все ваши страхи и непонятки по этой теме.

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

Статьи о функциях: Функции, Функции. Возврат значения

Ключевое слово this

Создадим обычную функцию и сразу же вызовем ее:

function hi() {
    console.log('Привет', this);
}

hi();

Функция выводит в консоль слово Привет и this. Если заглянуть в консоль, то увидим следующее:

Если со словом Привет все понятно, то вот что это такое вывелось на месте this? Почему вывелся какой-то объект Window?

Когда мы создаем глобальные объекты, то они автоматически начинают принадлежать глобальному объекту window – самому главному объекту в браузере.

На самом деле все методы, которые ты часто вызываешь, например: console.log(), alert, prompt и т.д. находятся внутри объекта window и являются его методами.

Это достаточно просто проверить. Просто попробуй вызвать эти методы так:

  • window.console.log('Привет');
  • window.alert('Привет');
  • window.prompt('Как тебя зовут?');

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

function hi() {
    console.log('Привет', this);
}

hi();

На самом деле, когда мы вызываем функцию, то мы вызываем ее так же из объекта window, т.е. вызов нашей функции можно переписать так:

window.hi();

Из всего выше сказанного можно сделать вывод, что наша функция запускается из объекта window, т.е. контекст, в котором выполняется наша функция так же равен объекту window. А ключевое слово this как раз и содержит внутри себя контекст, в котором вызывается функция/метод.

А теперь создадим объект:

const man = {
   name: 'Вася',
   lastName: 'Пупкин',
   age: 30,
   sayHi: hi
};

Простой объект описывающий абстрактного мужчину. Обрати внимание на метод sayHi. В него мы передали ссылку на функцию, которую создали в самом начале.

Давай вызовем метода sayHi из нашего объекта:

man.sayHi();

А теперь посмотрим в консоль, что же теперь вывелось на месте this. Получаем следующий результат:

В этом случае, значение this (контекста) становится равным самому объекту. Почему так случилось? Все очень просто.

Первый раз мы вызывали функцию hi и значение this, внутри нее было равно объекту Window, потому что мы запускали функцию в контексте объекта Window.

А сейчас, мы создали свой объект, и запустили функцию hi из него. Получается, что место вызова функции изменилось с объекта Window на объект man. Следственно, изменилось и значение this с Window на man.

Теперь коротко: значение this равно тому объекту в контексте которого было вызвано.

С контекстом разобрались, давай разбираться с этими страшными методами: call, apply, bind.

Метод bind

Мы ранее создали объект man, а теперь еще создадим объект woman. Итого, получим:

const man = {
   name: 'Вася',
   lastName: 'Пупкин',
   age: 30,
   sayHi: hi
};

const woman = {
   name: 'Катя',
   lastName: 'Иванова',
   age: 25
};

Итак, мы имеем 2 объекта, один из которых описывает мужчину, а второй женщину.

Для примера, создадим еще один объект (логгер), которому добавим один метод:

const Logger = {
   info: function() {
       console.log('Имя: ', this.name);
       console.log('Фамилия: ', this.lastName);
       console.log('Возраст: ', this.age);
   }
};

Внутри объекта Logger мы создали метод info, который должен выводить информацию о наших объектах. Мы использовали ключевое слово this внутри этого метода:

console.log('Имя: ', this.name);
console.log('Фамилия: ', this.lastName);
console.log('Возраст: ', this.age);

Но что будет, если вызвать этот метод прямо сейчас?

Logger.info();

Результат:

Мы получили undefined во всех случаях и это более чем логично. Т.к. мы обращаемся к ключевому слову this внутри метода info, который принадлежит объекту Logger, то и контекст, т.е. само ключевое this имеет значение, которое равно объекту Logger. А внутри этого объекта у нас имеется только один метод info. Никаких свойств с именами name, lastName, age у нас нет.

Как сделать так, чтобы с помощью объекта Logger и его метода info вывести данные, например, об объекте man?

Как раз тут на помощь и приходит тот самый страшный метод bind:

const loggerMan = Logger.info.bind(man);

У каждой функции в JS существует метод bind, благодаря которому мы можем любой функции задать контекст, внутри которого она должна будет выполняться.

Метод bind возвращает новую функцию, к которой будет привязан тот контекст, который мы указали как аргумент метода bind. В нашем случае контекстом мы указали объект man. Так как bind возвращает новую функцию, то мы ее запишем в константу loggerMan.

Теперь вызовем полученную функцию:

loggerMan();

Результат:

Как видишь, bind успешно привязал в качестве контекста объект man и информация о нем успешно вывелась в консоль.

Теперь мы хотим получить информацию об объекте woman с помощью нашего логгера. Для этого делаем тоже самое:

const loggerWoman = Logger.info.bind(woman);

С помощью bind мы привязываем новый контекст для метода info со значением woman.

Вызовем:

loggerWoman();

Результат:

Вуаля, все так же прекрасно работает. И ведь совсем не сложно и не страшно не так ли?

И последнее, что хотелось бы тут сказать. В функцию инфо мы можем дополнительно передавать какие-либо параметры, если это нужно. Давай немного исправим метод info в объекте Logger. Пускай он будет принимать пол человека:

const Logger = {
   info: function(sex) {
       console.log('Имя: ', this.name);
       console.log('Фамилия: ', this.lastName);
       console.log('Возраст: ', this.age);
       console.log('Пол: ', sex);
   }
};

Все что мы сделали – это добавили в определение функции параметр sex и выводим его под всеми остальными данными. Остается вопрос, как нам туда это все передать, если нам еще и контекст нужно сменить. Все очень просто. У нас уже есть полученные функции для наших объектов:

const loggerMan = Logger.info.bind(man);
const loggerWoman = Logger.info.bind(woman);

Чтобы передать параметр sex, нам достаточно указать его аргументом наших функций при их вызове:

loggerMan('мужской');
loggerWoman('женский');

Смотрим на результат:

Все прекрасно отработало. Но это не единственный способ передачи параметров.

Есть еще один способ передачи параметров – указать их при вызове метода bind.

Получим следующее:

const loggerMan = Logger.info.bind(man, 'мужской');
const loggerWoman = Logger.info.bind(woman, 'женский');

loggerMan();
loggerWoman();

Результат:

Результат никак не поменялся и все прекрасно продолжает работать.

Первый аргумент в методе bind – это сам контекст, который нужно привязать к функции. А вот дальше, через запятую, можно передавать сколько угодно дополнительных параметров(аргументов), которые должны попасть в метод info. В нашем случае нам нужно было передать только один аргумент sex.

На этом с bind все. Не такой и страшный зверь, как мог бы показаться.

Метод call

Что же, продолжим убивать страх внутри тебя. Теперь на очереди метод call.

Ты не поверишь, но он делает то же самое, что и bind. С одной маленькой поправкой: если метод bind привязывает контекст и возвращает новую функцию с этим контекстом, то метод call привязывая контекст, сразу же вызывает указанную функцию, а не возвращает новую.

Продублирую наш код:

const Logger = {
   info: function(sex) {
       console.log('Имя: ', this.name);
       console.log('Фамилия: ', this.lastName);
       console.log('Возраст: ', this.age);
       console.log('Пол: ', sex);
   }
};

const man = {
   name: 'Вася',
   lastName: 'Пупкин',
   age: 30,
   sayHi: hi
};

const woman = {
   name: 'Катя',
   lastName: 'Иванова',
   age: 25
};

const loggerMan = Logger.info.bind(man, 'мужской');
const loggerWoman = Logger.info.bind(woman, 'женский');

loggerMan();
loggerWoman();

Итак, давай привяжем контекст с помощью call, а не bind и посмотрим в чем же разница.

Было:

const loggerMan = Logger.info.bind(man, 'мужской');
const loggerWoman = Logger.info.bind(woman, 'женский');

Стало:

Logger.info.call(man, 'мужской');
Logger.info.call(woman, 'женский');

Так как метод call после привязки контекста сразу же выполняет функцию (info), и наша функция ничего не возвращает (внутри нет ключевого слова return), то не имеет никакого смысла записывать результат данной функции, так как он всегда в нашем случае будет равен undefined, поэтому создание констант loggerMan и loggerWoman делать не нужно.

И вот так вот просто. call – ничуть не страшнее bind. Вся разница:

  • bind привязывает контекст и возвращает новую функцию и мы можем вызвать ее в любом месте;
  • call привязывает контекст и сразу же вызывает функцию

Собственно, больше о call и сказать нечего.

Метод apply

По всему повествованию статьи, ты мог заметить, что о методе bind было сказано очень много. О методе call – уже куда меньше, потому что это одно и тоже с одним исключением. Будешь смеяться, но метод apply – это тоже самое, что и call. И существует только одно отличие.

Если метод call (как, собственно, и bind) первым аргументом принимает контекст, а дальше через запятую принимает аргументы для функции (получается, что аргументов может быть разное количество), то метод apply принимает всего 2 аргумента:

  • контекст (так же как bind и call);
  • массив параметров.

Т.е. если в bind и call мы можем передавать параметры функции через запятую, то в apply мы должны передать их в массиве. И в этом вся разница. Давай перепишем код с call на apply.

Было:

Logger.info.call(man, 'мужской');
Logger.info.call(woman, 'женский');

Стало:

Logger.info.apply(man, ['мужской']);
Logger.info.apply(woman, ['женский']);

Вот и вся разница. В случае с call мы передали аргумент sex для функции info как обычную строку. А в случае с apply мы обязаны передавать все аргументы в массиве.

Вот и все. Надеюсь, ты поборол свой страх и понял, что все, что связано с контекстом и, конкретно, данными методами – совсем не страшно.