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