Когда определяется this
в JavaScript
this
в JavaScript ставит в тупик всех разработчиков — от джунов до сеньоров. В первой из двух частей углубимся в основы, необходимые для лучшего понимания this
и того, как он работает.Первые несколько лет своей карьеры я рассматривал this
как ещё один аспект моего основанного на интуиции подхода к веб-разработке — то есть иногда мне казалось, что this
может быть тем, чего я хочу, но если это так, то по причинам, которые я не мог понять.
Иногда я оказывался прав — к моей весьма скромной чести, — но не потому, что я знал, почему или что-то в этом роде. Теперь, после многих лет, проведённых в раздумьях, методах проб и ошибок, а также заглядывая в Stack Overflow так долго, как только позволяли надвигающиеся сроки, я думаю, что довольно хорошо разобрался в this
. То есть, плюс-минус эмоциональная поддержка console.log
время от времени; я всего лишь человек.
Для меня проблема всегда заключалась в том, что this
— это контекст, но этот контекст не столько важен для нас, разработчиков, сколько важен для JavaScript. Мы привыкли указывать JavaScript, что это за вещи: это идентификатор, который мы ввели, где мы его ввели, с охватом пары скобок, установленных вокруг него, и он имеет значение, которое мы ему присвоили.
Это не относится к this
, который — следите за этим — является ключевым словом, ссылающимся на объект, связанный с функцией, в которой вызывается this
, в тот момент, когда эта функция вызывается.
Это кошмарное предложение, я знаю, но потерпите. Оно сводится к двум важным понятиям: this
ссылается на объект, связанный с функцией, а объект, на который ссылается this
, определяется в момент вызова функции. Как только разберётесь с этими двумя аспектами, this
станет… ну, всё ещё немного странным, но вполне понятным.
На высшем уровне объект, связанный с функцией
имеет смысл, если рассматривать его с точки зрения сущности JavaScript: объекты с методами, ссылающиеся на объекты с методами, всё ниже и ниже. Почти любой код, который выполняется в любом месте сценария, так или иначе связан с объектом, явно или неявно. Не сказал бы, что это сложная тема, но мы к ней ещё вернёмся.
Мы не можем понять что, не разобравшись сначала в когда, и, на мой взгляд, в момент вызова функции
— это то, что делает this
особенно сложным для понимания. Значение this
определяется не записью, а вызовом функции — то есть значение this
внутри функции может быть разным при каждом вызове этой функции. Чтобы понять это, необходимо думать скорее о JavaScript, выполняющем функцию, чем о человеке, набирающем функцию на клавиатуре.
Контексты выполнения (execution context)
Чтобы лучше понять, как JavaScript думает
, необходимо лучше понять модель выполнения JavaScript и природу стека вызовов (call stack) — структуры данных первый вошёл, последний вышел
, используемой интерпретатором JavaScript для выполнения кода.
Опять же, мы привыкли думать о JavaScript, который мы пишем, в терминах JavaScript, который мы пишем. Мы определяем, когда, где и как объявлять переменную и присваивать ей значение, это говорит о её значимости для JavaScript.
function theFunction() {
const theVariable = true;
};
Когда движок JavaScript встречает этот код, он создаёт лексическое окружение для theFunction
— структуры данных, представляющей код в том виде, в котором он был написан.
Весь контекст, необходимый для выполнения этой функции — наша переменная, в данном примере — добавляется в запись окружения для функции. Если вы когда-либо задавались вопросом, как JavaScript может знать о переменных до их объявления (но при этом не даёт вам покоя), то это происходит благодаря записям окружения:
function theFunction() {
console.log( theVariable );
let theVariable;
}
theFunction();
// результат: Uncaught ReferenceError: can't access lexical declaration 'theVariable' before initialization
Запись окружения, созданная для контекста выполнения этой функции, содержит свойство theVariable
, потому что оно находится прямо там, в лексическом окружении функции. JavaScript знает о нашей переменной в тот момент, когда выполняет функцию и сталкивается с этим console.log
, но мы делаем с ней что-то довольно странное, поэтому JavaScript выбрасывает ошибку, чтобы спасти нас от самих себя. Эта осведомлённость
, основанная на записях в окружении, и есть то, как работает поднятие/hoisting переменной, но это уже тема для другого раза.
После определения лексического смысла функции и создания записи окружения движок JavaScript создаёт контекст выполнения (иногда называемый фреймом
) для функции. Этот контекст выполнения включает в себя функцию и всё, необходимое для её выполнения.
Стек вызова (call stack)
Стек вызовов можно представить как некий плейлист, состоящий из контекстов выполнение функций: он определяет, какой код будет выполнен и когда. Это, наверное, само собой разумеется, но в стеке вызовов и вокруг него происходит очень много всего.
На данный момент и для того, чтобы понять this
, вот краткая и синхронная версия того, как работает стек вызова.
Возьмём следующий скрипт:
const theValue = 4;
function checkTheValue( theFunctionVariable ) {
const successMsg = "High enough.";
const failureMsg = "Too low.";
if( theFunctionVariable > 2 ) {
consoleLogger( successMsg );
} else {
consoleLogger( failureMsg );
}
};
function consoleLogger( msg ) {
console.clear();
console.log( msg );
};
checkTheValue( theValue );
Когда скрипт выполняется, интерпретатор JavaScript создаёт глобальный контекст исполнения
и помещает его в стек вызова. Все операторы, находящиеся в этом глобальном контексте, выполняются по очереди, сверху вниз.
Диаграмма. "Ведро", представляющее стек, со стрелкой, направленной от вершины к низу. В стеке есть блок, обозначенный как глобальный контекст выполнения. Внутри блока глобального контекста выполнения каждый блок из приведённого выше примера выделен цветом.
Как только интерпретатор встречает вызов функции checkTheValue
в глобальном контексте выполнения, он создаёт контекст выполнения функции для этого вызова в верхней части стека. Глобальный контекст выполнения приостанавливается, а вновь добавленный контекст выполнения функции немедленно выполняется.
Ссылаясь на предыдущую иллюстрацию, утверждение checkTheValue( theValue )
в глобальном контексте выделено, а стрелка указывает на блок "контекст выполнения функции checkTheValue
", добавленный над блоком глобального контекста выполнения. Он содержит следующие утверждения: const successMsg = 'High enough'
, const failureMsg = 'Too low'
и if ( theFunctionVariable > 2 consoleLogger( successMsg ) else consoleLogger( failureMsg )
Этот контекст выполнения функции содержит вызов consoleLogger
, поэтому контекст выполнения функции для этого вызова добавляется на вершину стека и — вы угадали — немедленно исполняется.
Строка consoleLogger( sucessMsg )
в контексте выполнения функции checkTheValue
выделена, а стрелка указывает на вновь добавленное исполнение функции
.consoleLogger
на вершине стека
После завершения работы consoleLogger
удаляется из стека, а JavaScript продолжает выполнение вызвавшей его функции checkTheValue
.
Блок "Контекст выполнения функции consoleLogger
" был удалён с вершины стека, оставив блок "Контекст выполнения функции checkTheValue
" и блок "Глобальный контекст выполнения".
Функция checkTheValue
завершается, выводится из стека, а глобальный контекст выполнения продолжается.
Блок "контекст выполнения функции checkTheValue
" был удалён с вершины стека, оставив только глобальный контекст выполнения.
Ну, он бы продолжал, если бы ему было чем заняться. Поскольку других операторов для выполнения нет, то и он удаляется по принципу первый вошёл, последний вышел
. К этому моменту в стеке вызовов ничего не осталось, и скрипт завершился.
Когда определяется значение this
Когда интерпретатор JavaScript создаёт контекст выполнения, происходит несколько вещей, одна из которых — установка значения ключевого слова this
в области действия этого контекста выполнения. За исключением одного конкретного случая, this
не зависит от лексического окружения — мы не диктуем значение для this
в области контекста выполнения для функции, в которой мы на него ссылаемся. Значение, на которое ссылается this
в контексте выполнения, зависит от того, как была вызвана эта функция: вызов функции как отдельной функции, метода или конструктора может изменить это значение.
На мой взгляд, именно в этом кроется суть путаницы вокруг this
. Когда мы пишем функцию, содержащую const theVariable = true
, неважно, каким путём движок JavaScript доберётся до этого утверждения: лексическое окружение содержит утверждение, инициализирующее theVariable
, поэтому неизменный факт существования theVariable
отмечен в записи окружения. Если и когда это утверждение будет достигнуто в контексте выполнения, значение переменной theVariable
будет true
, и точка:
const theObject = {
theMethod() {
const theVariable = true;
console.log( theVariable );
}
};
theObject.theMethod();
// результат: true
Вы не сможете ошибиться, если предположите, что this
работает точно так же, несмотря на то, что JavaScript выполняет установку вместо нас (пока не беспокойтесь о специфике этого примера):
const theObject = {
theMethod() {
console.log( this );
}
};
theObject.theMethod();
// результат: Object { theMethod: theMethod() }
Мы ещё поговорим о природе объекта, связанного с функцией
во второй части этой серии статей, но здесь мы видим, что this
выводит в консоль внутри theMethod()
в результате приводит к объекту, содержащему метод (theObject
). Даже несмотря на все мои оговорки, этот фрагмент кода, вероятно, читается так, как будто this
в методе ссылается на объект, потому что, посмотрите, метод находится прямо в этом объекте. Однако если мы вызовем этот же метод немного другим способом:
const theObject = {
theMethod() {
console.log( this );
}
};
const theFunctionIdentifier = theObject.theMethod;
theObject.theMethod();
// результат: Object { theMethod: theMethod() }
theFunctionIdentifier();
// результат: Window {...}
Вот такая контекстная странность. Один и тот же метод и один и тот же объект, но если вызвать метод немного по-другому, значение this
изменится:
const theObject = {
theMethod() {
console.log( this === theObject );
}
};
const theFunctionIdentifier = theObject.theMethod;
theObject.theMethod();
// результат: true
theFunctionIdentifier();
// результат: false
В момент вызова функции.
Не самая интуитивная вещь в мире, конечно, но, по крайней мере, то, когда устанавливается значение this
, имеет некий глубокий смысл в JavaScript-машине.
Не понимая когда
this
, нельзя понять что
— в частности, на какие объекты this
может ссылаться в зависимости от того, как вызывается функция. Во второй части этой серии статей как раз этим и займёмся, и есть хорошие новости: существует всего несколько случаев, определяющих объект, на который ссылается this
.
До встречи в следующей части.