В JavaScript this
- это неотъемлемая концепция, о которой легко споткнуться. this
относится к контексту выполнения функции, который устанавливается во время выполнения, а не во время определения.
Важно понимать разницу, потому что в JavaScript есть первоклассные функции - это означает, что функции обрабатываются как обычные объекты в том смысле, что они могут храниться в переменных и передаваться другим функциям.
Функции, которые принимают другие функции в качестве аргументов, возвращают функцию или и то, и другое, известны как функции высшего порядка.
Итак, почему нас волнует, что this
устанавливается во время выполнения? Потому что, если мы возьмем функцию и передадим ее, не проявляя осторожности, мы столкнемся с потерей контекста, потому что значение this
изменится в зависимости от того, где эта функция в конечном итоге вызывается. Ясно, как грязь, правда? Давайте посмотрим на пример:
В строке 3 мы назначаем анонимную функцию свойству getBar
объекта foo
. Когда мы вызываем getBar
в этом контексте в строке 8, getBar
является методом объекта foo
, поэтому неявное значение this
в строке 4 является родительским объектом foo
, и код дает неудивительный результат: регистрируется значение foo.bar
, которое является baz
.
Теперь о хитрости: строка 10 присваивает значение foo.bar
переменной qux
в глобальном контексте, и делает это без вызова анонимной функции, хранящейся в foo.bar
. Когда мы действительно вызываем функцию, хранящуюся в qux
в строке 11, контекст выполнения теперь отличается от того, что было раньше, поэтому неявный контекст выполнения здесь является глобальным объектом. Другими словами, this
теперь относится к global
или window
(в зависимости от того, выполняется ли этот JavaScript в узле или в браузере - здесь я буду просто говорить global
).
Так почему же строка 11 записывает undefined
? Потому что мы испытали потерю контекста! Мы попытались записать значение this.bar
, за исключением того, что в этом контексте выполнения this.bar
преобразуется в global.bar
. Поскольку в настоящее время у объекта global
нет свойства bar
, возвращаемое значение - undefined
.
Итак, как это исправить?
Вместо того, чтобы полагаться на контекст неявного выполнения, мы можем решить для себя, каким должен быть контекст выполнения (т. Е. Значение this
в вызываемой функции). Мы называем это явным исполнением контекстом. Есть несколько способов добиться этого.
Здесь, во время вызова, мы используем Function.prototype
методы call
и apply
, чтобы явно установить this
объекту foo
при выполнении функции. Это банальный пример, но стоит отметить, что здесь можно передать любой контекст, а не только foo
. Хотя call
и apply
могут выглядеть как псевдонимы друг для друга, это не так. Оба метода принимают явный контекст, который должен быть установлен для их первого аргумента. Разница в том, что apply
ожидает, что его следующий аргумент будет массивом, содержащим все остальные аргументы для передачи функции, в то время как call
ожидает, что каждый аргумент будет передан индивидуально.
Давайте изучим эти различия и их ограничения, прежде чем искать более постоянное решение.
Здесь мы видим, что apply
ожидает массив аргументов, которые он передаст функции, для которой задан контекст, в то время как call
требует, чтобы каждый аргумент передавался отдельно. В строке 12 записано NaN
(или «Не число»), потому что 5 * undefined === NaN
. Стоит отметить, что когда мы используем и call
, и apply
, мы устанавливаем контекст this
только для выполнения этой конкретной функции. Для более постоянного решения нам нужно использовать bind
.
При постоянной привязке контекста qux
в строке 10 каждый вызов qux
будет иметь контекст, привязанный к foo
, и больше не будет происходить потеря контекста. Теперь взгляните на строку 6. Передав второй аргумент в forEach
, мы можем использовать необязательный аргумент thisArg
, чтобы аналогичным образом установить контекст функции обратного вызова, переданной в forEach
. Это неуклюжая техника, но я скоро покажу способ лучше. В приведенном выше примере, поскольку функция обратного вызова, переданная forEach
в строке 4, не является методом объекта foo
, она будет неявно выполняться как функция, а не как метод. При выполнении как функции его неявное значение this
будет преобразовано в объект global
, за исключением того, что мы исправили это, передав thisArg
.
Из-за такого беспорядка я предпочитаю использовать стрелочные функции ES6 для достижения того же самого с гораздо более чистым синтаксисом. Стрелочные функции ES6 не изменяют контекст this
. Также обратите внимание, что в качестве однострочника скобки можно не устанавливать. Скобку также можно опустить, потому что есть только один параметр функции.
Помните, что при использовании bind
он будет работать только с выражением функции (т.е. функцией, хранящейся в переменной). Это не будет работать с объявлением функции. Есть и другие способы передачи контекста this
функциям, вложенным в методы, например, сохранение значения this
в локальной переменной, называемой чем-то вроде self
, но этот подход запутан и не имеет значения из-за стрелочных функций ES6.
Резюме:
Обязательно помнить, что контекст выполнения функции, также известный как this
, неявно определяется во время вызова, если он не устанавливается явно заранее. Контекст выполнения функции (this
) может быть явно установлен для конкретного вызова с помощью call
или apply
или навсегда привязан с помощью функции bind
.
Для дальнейшего чтения ознакомьтесь с документацией MDN: call, apply и bind.