Про this в JavaScript на пальцах™
Denys MikhalenkoПро this у новичков возникает много вопросов, потому что работает он несколько неочевидным образом, поэтому я решил написать это объяснение на пальцах™. Статья не претендует на техническую точность, а скорее пытается простым и понятным языком объяснить саму идею this в JavaScript.
Ну, погнали!
Про this
Итак, что такое this? Проще всего воспринимать его просто как еще один аргумент функции, который передается каждой функции автоматически. Эдакий волшебный, скрытый аргумент.
Условно говоря, когда вы объявляете функцию как foo(a, b, c), на самом деле получается что-то вроде foo(this, a, b, c) — тут 4 аргумента с именами this, a, b и c, вот только this - особенный, его передает за вас движок js, поэтому его на самом деле нет в списке аргументов когда вы эту функцию задаете или вызываете.
Тогда встает вопрос, если вы можете передать любые аргументы, вызывая функцию, а this передает движок js, то что в нём находится в этом аргументе и можно ли его поменять?
Раньше в нем находился некий глобальный объект, вроде window в браузерах и global в Node.js, ну и еще там были варианты у воркеров например, но это вело к трудноуловимым багам у неопытных программистов и умные люди решили это дело отключить.
Как раз тогда ввели так называемый strict mode — это такой режим, где все опасные для неопытных программистов возможности js отключены, вот заодно и отключили передачу глобального контекста. Теперь, когда вы просто вызываете функцию, у нее вthisбудетundefined.
По умолчанию этот режим включен только в модулях, а в простых JavaScript файлах его нужно включать вручную, директивой'use strict'.
Но, спросите вы, как же получается, что когда ты вызываешь функцию как метод объекта, в this не undefined, а этот самый объект? А вот так! Спецификация языка так велит! Поэтому, когда функция вызывается как метод, т.е. obj.func(), js слушается спецификацию и засовывает в this не undefined а этот самый obj, который идет перед точкой.
Про call и apply
Теперь вопрос: вот есть у тебя функция, вызываешь её — а там в this лежит undefined, а ты хочешь, чтоб не undefined, как?
Есть специальные служебные методы у прототипа Function - call и apply. Эти специальные методы доступны у любой функции, т.к. она найдет эти методы в своем прототипе и вызовет. Вот с помощью этих служебных методов и можно задать значение this для вызываемой функции.
Когда вы вызываете функцию вот так: foo(1, 2, 3), js задаст значение this автоматически, в соответствии с правилами, указанными в спецификации языка. Это может быть глобальный контекст вроде window или undefined, если речь идет о strict mode.
А вот с помощью служебного метода call можно задать значение this самому. Вот так: foo.call('hello', 1, 2, 3) — в этом случае js примет первый аргумент за this и если вы в функции foo сделаете console.log(this), то увидите строку 'hello' - которую мы и передали в call первым аргументом.
С apply там все аналогично, разница лишь в форме передачи остальных аргументов - они передаются не через запятую, а одним массивом. И всё.
Про bind
Ну и последний момент: что делать, если вы хотите этот this назначить, а вызывать функцию пока не нужно? Например, вам нужно передать её как обработчик какого-то события, которое пока еще не случилось. Ну, тут на выручку приходит bind — это еще один служебный метод прототипа Function и всё, что он делает, так это берёт вашу функцию и всё те же this и прочие аргументы, которые вы ему передали, а потом создаёт и возвращает новую функцию, которая вызовет изначальную через call или apply и передаст ей все эти аргументы.
То есть когда вы делаете:
const bar = foo.bind('hello', 1, 2, 3)
что делает этот bind? А вот что:
return function() {
foo.call('hello', 1, 2, 3)
}
Ну, естественно, всё это через переменные, а не хардкодом — это я для облегчения понимания написал.
Там есть ещё нюанс с тем, что он также добавит еще и аргументы вызванной функции, но я это нарочно написал упрощённо, чтобы акцентировать внимание именно на работе сthis.
По факту там будет что-то типа:
return function(...args) {
foo.call('hello', 1, 2, 3, ...args)
}
И вот когда вы вызываетеbar(4, 5), то на самом деле произойдетfoo.call('hello', 1, 2, 3, 4, 5)
Отсюда, кстати, можно легко понять, почему нельзя сделать bind еще раз (вернее можно, но эффекта не будет) — потому что в итоге все равно вызовется самый первый foo.call() и this будет передано то, что вы передали в самом первом bind в качестве аргумента, а все остальные значения, заданные в последующих bind не будут использоваться и просто пропадут зря.
Допустим у вас есть функция:
function foo() {
console.log(this)
}
если вы сделали const bound = foo.bind('hello'), то вы получите новую функцию, которая делает примерно следующее:
function bound() {
foo.call('hello')
}
и если вы вызовете теперь эту новую функцию bound, которую bind вернул, то она вызовет foo и сунет ей в this строку 'hello', та выведет свой this с помощью console.log(this) и вы увидите 'hello' на экране.
Но что будет, если вызвать bind еще раз и передать другой контекст, вот так: const bound2 = bound.bind('bye') ?
Мы получим примерно следующее:
function bound2() {
bound.call('bye')
}
Отлично, она вызовет функцию bound и сунет ей строку 'bye' в this.
А теперь посмотрим, что делает функция bound:
function bound() {
// в `this` будет 'bye',
// да вот только this здесь нигде не используется
foo.call('hello')
}
Ваша строка 'bye' как, впрочем, и все последующие, просто не будут использоваться, поэтому никакого эффекта не дадут.
Про стрелочные функции
И вот ещё насчет так называемых стрелочных функций (arrow functions).
Можете представлять себе, что если вы пишете:
() => { ... }
то на самом деле это все равно, что вы написали бы:
(function() { ... }).bind(this)
то есть в момент создания стрелочной функции возьмётся значение this в том месте, где вы её создаете и с помощью bind навсегда прибьется гвоздями к нашей новенькой стрелочной функции.
А как мы знаем, последующими вызовами bind уже не получится изменить контекст, поэтому принято считать, что у стрелочных функций bind не работает. На самом деле он работает, конечно, просто уже поздно — функция уже навсегда привязана к контексту.
Вот такие дела.