Когда определяется 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.
До встречи в следующей части.