Узнайте, как значение ключевого слова this определяется в JavaScript.

Темы

  • Глобальный объект
  • Контекст выполнения (он же значение this)
  • Неявные и явные контексты выполнения
  • Потеря контекста

Понимание того, как определяется значение ключевого слова this, очень важно, иначе вы можете столкнуться с неожиданными результатами при выполнении ваших программ. Однако, чтобы понять, как JavaScript определяет значение this, мы должны сделать шаг назад и перед тем, как начать, взглянуть на еще одну важную концепцию — глобальный объект.

Глобальный объект

Когда программа JavaScript запускается, она создает объект, доступный во всей вашей программе, называемый глобальным объектом. Глобальный объект в Node.js — это объект global, а если говорить о JavaScript в браузере, то это объект window.

Примечание. В оставшейся части этой статьи я продолжу использовать Node.js в качестве среды программирования.

Глобальный объект имеет следующие характеристики:

  • Он содержит значения, методы и объекты, глобально доступные в вашей программе, например, parseInt, Math, String, Number.
  • Переменные, объявленные без const, let или var, и объявления функций добавляются как свойства к глобальному объекту.
  • Он используется как неявный контекст выполнения для вызовов функций.

Контексты выполнения (неявные и явные)

Контекст выполнения (или просто контекст) — это среда, в которой выполняется функция, которая в JavaScript также известна как значение ключевого слова this. Существует два типа контекстов выполнения: неявный и явный.

Неявный контекст выполнения

Неявные контексты выполнения автоматически устанавливаются JavaScript.

1) Само по себе значение this относится к объекту global

Как видите, сравнение значения this и объекта global с использованием оператора строгого сравнения дает true, потому что это один и тот же объект. Кроме того, вы можете изучить свойства, доступные объекту global, используя метод Object.prototype.getOwnPropertyNames.

2) Внутри объекта значение this относится к объекту global

Когда приведенный выше код будет выполнен, он выведет NaN на консоль.

Удивлен?

Поскольку внутри объекта значение this относится к объекту global, когда мы используем this.firstName и this.lastName для доступа к их значениям, мы имеем в виду не объект user, а объект global, а в объекте global свойства firstName и lastName не существуют, поэтому им присваивается значение undefined, затем выражение становится undefined + undefined, и, поскольку вы не можете выполнять математические операции над нечисловыми значениями, возвращается NaN.

Итак, как мы можем решить эту проблему?

Очень важно помнить следующее:

JavaScript определяет значение this keyword for вызовов функции или метода в зависимости от того, КАК вызывается функция или метод. Где или когда определена функция или метод не имеет значения.

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

Теперь, когда мы знаем это новое правило, мы знаем, что когда мы обращаемся к методу username, значение this будет относиться к объекту user.

Итак, с помощью этого нового правила у нас есть способ исправить проблему в предыдущем примере кода, мы можем переместить свойства this.firstName и this.lastName внутрь функционального выражения и вернуть вычисленное значение.

Также обратите внимание, что метод user.username был вызван как метод. Вот почему значение this относится к объекту user. Как вы думаете, что произойдет, если мы присвоим метод username переменной и вызовем его как функцию?

4) При вызове функции значение this относится к глобальному объекту

Чтобы проиллюстрировать это, мы просто внесем небольшое изменение. Мы назначим метод username переменной, а затем вызовем его, чтобы посмотреть, что произойдет. Вкратце, мы можем сделать это (назначить метод переменной), потому что функции являются первоклассными значениями в JavaScript, они такие же, как и любые другие значения, поэтому их можно назначать и передавать.

Поскольку сейчас мы ссылаемся на объект global, мы получаем ту же проблему, что и раньше.

5) При вызове функции в строгом режиме значение this равно undefined.

Поскольку this не ссылается на объект global (он ссылается на undefined), попытка вызвать функцию getUsername вызовет TypeError: Cannot read property 'firstName' of undefined, когда мы попытаемся получить доступ к свойству firstName из undefined.

Явный контекст выполнения

В JavaScript явные контексты выполнения явно устанавливаются разработчиком. Это относится к привязке значения ключевого слова this с использованием методов call, apply и bind. Давайте рассмотрим каждый из упомянутых методов, чтобы узнать, как он работает.

1) Явная установка контекста выполнения функции с помощью метода call

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

Вот шаблон для метода call:

вызов функции с явным контекстом выполнения с помощью метода call:

Вы можете использовать метод call для повторного использования кода между конструкторами:

Вы можете использовать метод call для вызова функции без указания контекста выполнения. Когда вы используете call без указания контекста, контекстом будет объект global.

2) Явная установка контекста выполнения с помощью метода apply

Метод применения работает точно так же, как call, с той лишь разницей, что необязательные аргументы принимаются как объект, подобный массиву.

Вот шаблон для метода apply:

Использование метода apply для явной установки контекста выполнения функции

3) Использование ключевого слова bind для жесткой привязки контекста выполнения функции.

Способ работы метода bind отличается от методов call и apply. Метод bind возвращает новую функцию, которая постоянно привязана к объекту, переданному в качестве первого аргумента bind. Следовательно, исходная функция или метод не затрагиваются. После того, как вы «жестко привязали» функцию к объекту, вы не можете «отвязать» ее, даже если вы снова вызываете bind для этой функции или используете методы call или apply. В отличие от call и apply, метод bind не выполняется сразу.

Пример 1. Привязка функции и ее назначение переменной

Обратите внимание, что с bind теперь метод username всегда будет ссылаться на объект user, и поскольку он возвращает новую функцию, мы можем назначить ее другой переменной и вызвать эту переменную позже в нашем коде.

Пример 2. Контекст выполнения постоянно связанной функции нельзя изменить

Несмотря на то, что мы снова вызываем fetchData с методом call, используя объект smallData, контекст выполнения функции getData не меняется, потому что он был привязан навсегда. Итак, мы получим две копии schittsCreekData.data, напечатанные на консоли.

Потеря контекста

Иногда функции могут терять свой контекст выполнения (также известный как значение this) во время определенных сценариев, и когда это происходит, это называется потеря контекста или называется функцией, потеряющей свой контекст. Однако важно знать, что у функции всегда есть контекст, и когда мы говорим, что она потеряна, это просто означает, что контекст не такой, как мы ожидаем.

Я расскажу о некоторых наиболее распространенных сценариях потери контекста:

  1. Когда метод извлекается из объекта и выполняется как функция
  2. Когда внутренняя функция не использует окружающий контекст
  3. Когда функции передается аргумент, и она теряет окружающий контекст

1) Функция может потерять свой контекст, когда метод копируется (извлекается) и вызывается как функция.

Когда мы присваиваем метод sergiosTasks.getTasks переменной printTodaysTasks, контекст выполнения метода getTasks теряется, и значение this теперь ссылается на объект global. Почему?

JavaScript определяет значение this keyword for вызовов функции или метода на основе того, КАК вызывается функция или метод. Где и когда определена функция или метод, не имеет значения.

Когда метод getTasks присваивается переменной и вызывается как функция, его контекстом выполнения является глобальный объект, а поскольку объект global не имеет свойства tasks, он возвращает undefined. Итак, когда мы пытаемся получить доступ к методу forEach из undefined, выдается TypeError: Cannot read property 'forEach' of undefined.

Решение

Чтобы сохранить контекст метода getTasks, мы можем использовать метод bind и передать объект sergiosTasks в качестве контекста выполнения. Это навсегда привяжет метод getTasks к объекту sergiosTasks, и даже если мы будем передавать его ссылку, его контекст не будет потерян.

2) Функция может потерять свой контекст, если внутренняя функция не использует окружающий контекст.

Почему теряется контекст для функции calculateMonthlyWages?

JavaScript определяет значение this keyword for вызовов функции или метода на основе того, КАК вызывается функция или метод. Где и когда определена функция или метод, не имеет значения.

Надеюсь, вы начинаете замечать здесь закономерность. К счастью для нас, есть несколько решений этой распространенной проблемы, и мы можем сохранить контекст выполнения следующими способами:

  1. Сохраняйте контекст, используя переменную во внешней области
  2. Вызовите внутреннюю функцию с явным контекстом, используя методы call или apply.
  3. Навсегда привязать контекст выполнения с помощью метода bind

Решение 1. Сохраните контекст выполнения, используя переменную во внешней области.

Мы можем сохранить контекст выполнения, сохранив его значение в переменной. Поскольку JavaScript использует лексическую область видимости, код внутри функции calculateMonthlyWages будет иметь к ней доступ, и функция сможет вернуть нужную нам информацию.

Решение 2. Вызовите внутреннюю функцию с явным контекстом (используя call или apply)

Другим решением может быть использование методов call или apply для явного вызова функции calculateMonthlyWages с правильным контекстом.

Решение 3. Навсегда привязать контекст выполнения с помощью метода bind

Мы можем навсегда привязать контекст выполнения функции calculateMonthlyWages, используя функциональное выражение и метод bind.

Решение 4. Использование функции стрелки

Наконец, это может быть наилучшим сценарием для стрелочной функции. Стрелочные функции наследуют свой контекст выполнения из окружающего контекста. Следовательно, функция calculateMonthlyWages сможет сохранить свой контекст, получив его из внешнего метода getSalaryInfo.

3) Функция может потерять окружающий контекст, когда функция передается в качестве аргумента другой функции.

Другой способ потерять контекст выполнения функции — передать ее в качестве аргумента другой функции.

Решение

Мы могли решить эту проблему по-разному. Мы могли бы добавить еще один параметр в функцию logTasks и передать ссылку на объект sergiosTasks, затем внутри функции logTasks мы могли бы использовать либо call, либо apply для вызова функции callback, используя ссылку на объект sergiosTasks в качестве контекста выполнения.

Однако эту проблему можно решить более простым способом, используя метод bind для постоянной привязки метода getTasks к объекту sergiosTasks.

Я знаю, это было сложно переварить, но знаете что? Есть еще кое-что. Это не исчерпывающий список сценариев, но я надеюсь, что теперь вы лучше понимаете, как JavaScript определяет контексты выполнения и как вы можете манипулировать контекстами с помощью методов call, apply и bind.

Ключевые выводы

  • Глобальный объект: глобальный объект автоматически создается JavaScript при запуске программы. Он используется как неявный контекст выполнения вызовов функций.
  • Контекст выполнения. Контекст выполнения относится к значению ключевого слова this в любой заданный момент времени.
  • Неявный контекст выполнения: автоматически устанавливается JavaScript.
  • Явный контекст выполнения: задается разработчиком с помощью методов call, apply и bind.
  • Как JavaScript определяет значение this: JavaScript определяет значение ключевого слова this для вызовов функций или методов в зависимости от того, КАК вызывается функция или метод. Где и когда определена функция или метод, не имеет значения.

Ресурсы

  • Объектно-ориентированное программирование с помощью JavaScript — JS120 (Launch School)
  • документация по функциям вызова, применения и привязки (Mozilla Developer Network)
  • Это ключевое слово (школы W3)