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 мы обязаны передавать все аргументы в массиве.
Вот и все. Надеюсь, ты поборол свой страх и понял, что все, что связано с контекстом и, конкретно, данными методами – совсем не страшно.