- Контекст выполнения, неявное и явное связывание, стрелочные функции и, конечно, this.
- Уровни 1-4: Местные герои
- Прежде чем мы поднимем уровень…
- Уровни 5-10: Герои королевства
- Прежде чем мы поднимемся на уровень выше…
- Уровни 11-16: Владыки царства
- Прежде чем мы поднимем уровень…
- Уровни 17-20: Властелины мира
- Размышления на прощание
- Ресурсы
Контекст выполнения, неявное и явное связывание, стрелочные функции и, конечно, this.
Для меня грамотное понимание ключевого слова this
в JavaScript кажется примерно таким же трудным, как освоить последнего босса в кампании D&D.
Я не готов.
Поэтому инстинкт подсказывает мне, что я должен написать статью на эту тему, чтобы попытаться перевести this
из моего пассивного словаря в активный.
Присоединяйтесь ко мне в этом стремлении, и давайте повышать уровень вместе.
Уровни 1-4: Местные герои
Определение контекста выполнения, this
и new
.
Прежде чем мы рассмотрим this
, будет полезно определить контекст выполнения. В MDN контекст выполнения описывается следующим образом:
Когда выполняется фрагмент кода JavaScript, он выполняется внутри контекста выполнения. Существует три типа кода, которые создают новый контекст выполнения:
Глобальный контекст — это контекст выполнения, созданный для выполнения основной части вашего кода; то есть, любого кода, который существует вне функции JavaScript.
Каждая функция выполняется в своем собственном контексте выполнения. Его часто называют «локальным контекстом».
Использование злополучной функцииeval()
также создает новый контекст выполнения.Каждый контекст, по сути, является уровнем области видимости в вашем коде. Когда один из этих сегментов кода начинает выполняться, создается новый контекст, в котором он будет выполняться; этот контекст затем уничтожается при выходе из кода.
Проще говоря, контекст выполнения — это созданное пространство, которое непосредственно окружает блок выполняемого кода.
Глобальный контекст создается в начале программы. Каждый раз, когда вызывается функция, создается локальный контекст выполнения, который добавляется в стек контекста выполнения (стек вызовов).
В других статьях контекст выполнения называют «средой», окружающей блок выполняемого кода, что имеет смысл, поскольку это больше, чем просто местоположение!
Контекст выполнения также имеет пользовательские свойства.
Эти свойства немного отличаются между глобальным и локальным контекстом, а также между различными типами локального контекста (функции против стрелочных функций против классов и т.д.).
Однако у каждого контекста выполнения есть свойство, известное как this
.
Так что же такое this
?
Барабанная дробь…
this
— это ссылка на объект.
Но на какой объект? Ну, это зависит от ситуации. Пора разобрать несколько примеров, чтобы начать выяснять это.
В нашем первом простом примере мы регистрируем значение this
в глобальном контексте выполнения, вне какой-либо функции:
console.log(this); // window
В браузере мы видим, что this
равно объекту window
.
MDN подтверждает поведение в этом определении:
В глобальном контексте выполнения (вне любой функции),
this
ссылается на глобальный объект, независимо от того, в строгом режиме или нет.
SIDE NOTE: В браузере глобальным объектом является window
. В Node.js глобальным объектом на самом деле является объект global
. Однако есть некоторые небольшие различия в поведении. В Node.js в приведенном выше примере на самом деле регистрируется текущий объект module.exports
, а не объект global
. 🤷 Давайте пока обойдемся без этой кроличьей норы.
В следующем примере мы регистрируем this
внутри функции:
function logThisInAFunction() {
console.log(this);
};
logThisInAFunction(); // window
Сначала вы можете подумать, что this
будет оцениваться как нечто иное, чем window
, поскольку мы создали новый локальный контекст выполнения при вызове функции, в котором выполняется код внутри нашей функции.
Но по умолчанию (когда не используется строгий режим, еще одна кроличья нора 🤷), если не задано явно (к чему мы еще вернемся), this
по-прежнему ссылается на window
.
MDN подтверждает это поведение по умолчанию:
Внутри функции значение
this
зависит от того, как вызывается функция.Поскольку следующий код не находится в строгом режиме, и поскольку значение
this
не задано вызовом,this
по умолчанию будет соответствовать глобальному объекту, которым в браузере являетсяwindow
.
ОБРАТИТЕ ВНИМАНИЕ: В этом втором примере есть странная разница в поведении между браузером и Node.js. В браузере this
равен window
, но в Node.js this
равен global
на этот раз вместо объекта module.exports
. Это связано с разницей в том, как Node.js обрабатывает значение по умолчанию this
между глобальным и локальным контекстами.
Следующий пример раскрывает возможность того, что this
имеет другое значение в зависимости от того, как вызывается функция:
function logThisInAFunction() {
console.log(this);
};
const myImportantObject = {
logThisInMyObject: logThisInAFunction
};
logThisInAFunction(); // window
myImportantObject.logThisInMyObject(); // myImportantObject
Когда мы вызываем logThisInAFunction()
и myImportantObject.logThisInMyObject()
, в конечном итоге вызывается одна и та же функция. Однако значение this
не одинаково для двух вызовов. MDN снова красноречиво описывает:
Когда функция вызывается как метод объекта, ее
this
устанавливается на объект, на котором вызывается метод.
Проще говоря, если вы вызываете функцию как метод объекта, то этот объект станет значением для this
вместо глобального объекта. Таким образом, мы неявно привязываем значение this
к выбранному нами объекту.
В следующем примере рассматривается поведение this
для классов:
class MyAwesomeClass {
constructor() {
console.log(this);
};
};
const awesomeClassInstance = new MyAwesomeClass(); // MyAwesomeClass
При создании экземпляра класса с помощью ключевого слова new
создается новый пустой объект, который устанавливается в качестве значения this
. Любые свойства, добавленные к this
в конструкторе, будут свойствами этого объекта.
Как сказано в MDN:
Поведение
this
в классах и функциях похоже, поскольку классы — это функции под капотом. Но есть некоторые различия и предостережения.В конструкторе класса
this
— это обычный объект. Все нестатические методы в классе добавляются к прототипуthis
.
Относительно просто.
Прежде чем мы поднимем уровень…
Надеюсь, this
начинает проникать в суть. Уже поговорив о неявном связывании, давайте изучим следующий уровень: явное связывание.
Уровни 5-10: Герои королевства
Определение call
, apply
и bind
.
Ранее мы видели, как можно взять функцию, добавить ее в качестве метода к объекту, и когда мы вызываем этот метод, объект неявно становится нашим новым значением для this
.
Теперь мы хотим узнать, как явно установить значение this
при вызове функции. Мы можем сделать это с помощью нескольких различных методов: call
, apply
и bind
.
Сначала пример с использованием call
:
const wizard = {
class: 'Wizard',
favoriteSpell: 'fireball'
};
const warlock = {
class: 'Warlock',
favoriteSpell: 'eldrich blast'
};
function useFavoriteSpell(name) {
console.log(`${name} the ${this.class} used ${this.favoriteSpell}!`);
};
useFavoriteSpell('Bobby'); // Bobby the undefined used undefined!
useFavoriteSpell.call(wizard, 'Bradston'); // Bradston the Wizard used fireball!
useFavoriteSpell.call(warlock, 'Matt'); // Matt the Warlock used eldrich blast!
При первом вызове useFavoriteSpell
мы имеем неопределенные значения. this
по умолчанию ссылается на объект window
, а свойства class
и favoriteSpell
не существуют для окна.
В следующие два раза, когда мы вызываем useFavoriteSpell
, мы используем call
для присвоения значения this
выбранному нами объекту, а также вызываем функцию.
Именно это и делает call
! Первым аргументом call
является объект, которому вы хотите приравнять this
, а последующие аргументы, разделенные запятыми, являются аргументами функции.
Подробнее о call
вы можете прочитать здесь.
Следующий метод, который мы рассмотрим, это apply
. Для его понимания, на мой взгляд, не требуется дополнительного примера кода. Основное различие между call
и apply
заключается в следующем:
-
call
принимает аргументы функции один за другим в виде списка, разделенного запятыми. -
apply
вместо этого принимает все аргументы функции как один массив.
За исключением некоторых других небольших различий, они делают одно и то же. Подробнее о apply
вы можете прочитать здесь.
Наконец, у нас есть bind
:
const wizard = {
class: 'Wizard',
favoriteSpell: 'fireball'
};
const warlock = {
class: 'Warlock',
favoriteSpell: 'eldrich blast'
};
function useFavoriteSpell(name) {
console.log(`${name} the ${this.class} used ${this.favoriteSpell}!`);
};
useFavoriteSpell('Bobby'); // Bobby the undefined used undefined!
const useFavoriteWizardSpell = useFavoriteSpell.bind(wizard);
useFavoriteWizardSpell('Bradston'); // Bradston the Wizard used fireball!
const useFavoriteWarlockSpell = useFavoriteSpell.bind(warlock);
useFavoriteWarlockSpell('Matt'); // Matt the Warlock used eldrich blast!
Вместо установки значения this
и вызова функции, вызов bind
на нашей функции просто возвращает новую функцию, значение this
которой теперь равно объекту, который мы передали в качестве аргумента bind
.
Затем мы можем вызывать нашу новую функцию, когда захотим, а значение this
уже установлено.
Подробнее о bind
читайте здесь.
Прежде чем мы поднимемся на уровень выше…
Итак, теперь мы знаем о контексте выполнения, this
, и как установить значение для this
как неявно, так и явно с помощью call
, bind
и apply
.
Что еще есть? Честно говоря, очень много. Но давайте сосредоточимся на чем-то одном, и перейдем к отношениям между this
и стрелочными функциями.
Уровни 11-16: Владыки царства
Как стрелочные функции влияют на this
.
Ранее я говорил, что каждый контекст выполнения имеет свойство, известное как this
. Однако поведение стрелочных функций немного отличается.
Хотя локальный контекст выполнения, созданный при вызове стрелочной функции, все еще имеет значение this
, он не определяет его для себя. Вместо этого он сохранит значение this
, которое было установлено следующим внешним контекстом выполнения, откуда была вызвана функция.
Например:
const logThisInAnArrowFunction = () => {
console.log(this);
};
const myImportantObject = {
logThisInMyObject: logThisInAnArrowFunction
};
logThisInAnArrowFunction(); // window
myImportantObject.logThisInMyObject(); // window
При вызове myImportantObject.logThisInMyObject()
мы видим, что, несмотря на то, что был создан новый локальный контекст выполнения, this
получит свое значение из следующего внешнего контекста выполнения. В данном случае это глобальный контекст выполнения. Поэтому this
остается ссылкой на объект window
.
Вот еще один пример, который, надеюсь, донесет эту мысль до читателя:
const myImportantObject = {
exampleOne: function() {
const logThis = function() {
console.log(this);
};
logThis();
},
exampleTwo: function() {
const logThis = () => {
console.log(this);
};
logThis();
}
};
myImportantObject.exampleOne() // window
myImportantObject.exampleTwo() // myImportantObject
В этом примере у нас есть два метода, которые мы вызываем из myImportantObject
: exampleOne()
и exampleTwo()
.
Когда мы вызываем myImportantObject.exampleOne()
, мы вызываем эту функцию как метод объекта, и поэтому this
равен myImportantObject
в этом локальном контексте.
Однако внутри этой функции мы определяем другую функцию и затем выполняем ее.
Как уже говорилось ранее, если функция не задана явно и если она не вызывается как метод объекта, this
ссылается на глобальный объект window
. Поэтому мы видим window
зарегистрированным.
Однако, когда мы вызываем myImportantObject.exampleTwo()
, происходит нечто иное. Первая часть та же: this
равняется myImportantObject
внутри exampleTwo
. Далее мы определяем стрелочную функцию, а затем выполняем ее. Разница в том, что функция стрелки не определяет собственное значение для this
! Вместо этого она сохраняет значение this
из следующего внешнего контекста выполнения, который в данном примере был локальным контекстом, созданным myImportantObject.exampleTwo()
, где это значение равнялось myImportantObject
. Вот что мы видим в журнале.
Прежде чем мы поднимем уровень…
Функции Arrows отличаются от обычных функций множеством других способов. Если вы хотите прочитать больше, загляните на страницу MDN, посвященную этой теме.
Если вы дошли до этого места в статье, это, надеюсь, означает, что все начинает проясняться. Найдите время, чтобы насладиться своим достижением.
Остался только один раздел. Давайте погрузимся в него.
Уровни 17-20: Властелины мира
Обратные вызовы.
Прежде чем начать писать эту статью, я думал, что последний раздел будет самым сложным. Раньше примеры того, как это
получало свое значение в функциях обратного вызова, ставили меня в тупик.
С тем контекстом, который у меня есть сейчас, мне больше не кажется, что функции обратного вызова добавляют дополнительную сложность к тому, как работает this
. Нам просто нужно учитывать то, что мы уже знаем о контексте выполнения, и то, как мы вызываем функцию.
Думаю, последний босс начинает выглядеть более побеждаемым 😁.
Вот пример использования this
в функциях обратного вызова. Попробуйте угадать, какие значения будут записаны в журнал для каждой из них:
function higherOrderFunction(callback) {
const myImportantObject = {
callMyCallback: callback
};
myImportantObject.callMyCallback();
};
function callbackFunction() {
console.log(this);
};
const callbackArrowFunction = () => {
console.log(this);
};
higherOrderFunction(callbackFunction);
higherOrderFunction(callbackArrowFunction);
Начнем с первого вызова функции (где аргументом является callbackFunction
):
-
higherOrderFunction
вызывается и создает локальный контекст выполнения. Поскольку он не задан ни неявно, ни явно,this
по умолчанию принимает значениеwindow
. -
Наша функция обратного вызова вызывается внутри
higherOrderFunction
черезmyImportantObject.callMyCallback()
. Поскольку наш обратный вызов не является стрелочной функцией, созданный локальный контекст выполнения определяет свой собственныйthis
. И, поскольку эта функция вызывается как методmyImportantObject
,this
равенmyImportantObject
.
Далее рассмотрим второй вызов функции (где нашим аргументом является callbackArrowFunction
):
-
higherOrderFunction
вызывается так же, как и раньше. Результат тот же:this
равноwindow
в этом локальном контексте. -
Наш обратный вызов снова вызывается внутри
higherOrderFunction
. На этот раз, поскольку наш обратный вызов является стрелочной функцией, он не определяет свой собственныйthis
. Поэтому, хотя она вызывается как методmyImportantObject
,this
сохраняет значение следующего внешнего контекста выполнения, которым былоwindow
.
Мораль этой истории такова: функции обратного вызова добавляют сложности только в том смысле, что вы должны учитывать, как (а также где для стрелочных функций) вызывается обратный вызов.
Но мы уже рассматривали это!
Разница в том, что функции обратного вызова передаются в качестве аргументов другим функциям, и поэтому то, где и как они вызываются, немного отличается.
Размышления на прощание
Надеюсь, я не потерял вас в какой-то момент 😅.
Как разработчик, я всегда стараюсь продолжать свое образование — и я понимаю, что я еще многого не знаю.
Это
— большая тема, и я уверен, что есть аспекты концепции, которые я упустил, или примеры и объяснения, которые можно улучшить. Давайте поможем друг другу, попытавшись выяснить, что это были за аспекты! Не стесняйтесь оставлять комментарии и дайте мне знать, помогла ли вам эта статья, и/или что можно было бы улучшить.
Спасибо, что читаете! 😄 Посмотрите некоторые из моих предыдущих статей на quickwinswithcode.com.
Ресурсы
-
В глубину: Микрозадачи и среда выполнения JavaScript
-
this
-
Строгий режим
-
Function.prototype.call()
-
Function.prototype.apply()
-
Function.prototype.bind()
-
Выражения стрелочных функций