Что такое this в JavaScript

Во второй части серии Mat Marquis объясняет, что такое this на самом деле, и помогает разобраться, чему он соответствует, исходя из различных контекстов.

Не стану утверждать, что определение объекта, связанного с this, — это лёгкое чтиво, но есть и небольшое утешение: теперь, когда мы понимаем, когда происходит это определение, мы можем понять, что именно — и это, к счастью, сводится лишь к нескольким отдельным случаям использования, каждый из которых имеет одну большую оговорку в виде стрелочных функций.

Глобальная привязка (ранее привязка по умолчанию)

Любой код за пределами определённой функции находится в глобальном контексте выполнения. В рамках глобального контекста выполнения this ссылается на globalThis — свойство, ссылающееся на глобальный объект среды JavaScript. В документе браузера глобальным объектом является объект window:

this;
// результат: Window {...}

this === window;
// результат: true

globalThis;
// результат: Window {...}

this === globalThis;
// результат: true

Достаточно просто, хотя и не особенно полезно вне контекста функции. В контексте объявления функции вы увидите похожий результат:

function theFunction() {
return this;
}

theFunction();
// результат: Window {...}

Теперь вспомните, что значение this — это ссылка на объект, связанный с вызовом функции. Эта ссылка на глобальный объект — window, для целей веб-браузера — имеет смысл с точки зрения того, как JavaScript исторически обрабатывает глобальную область: когда создаётся функция с помощью конструктора function, эта функция создаётся в глобальной области и становится доступной как метод объекта window:

function theFunction() { }

theFunction;
// результат: theFunction()

window.theFunction;
// результат: function theFunction()

Поэтому имеет определённый смысл — во всяком случае, в JavaScript — что значение this в контексте выполнения, созданном для myFunction, будет ссылкой на объект window: при отсутствии более конкретного объекта для ссылки, ну, объект window более или менее владеет всем, поэтому он используется как привязка по умолчанию — отсюда и название для этого случая: привязка по умолчанию.

function theFunction() {
return this;
}

theFunction() === window
// результат: true

Знаю, немного странно. Как бы то ни было, вы вряд ли будете сталкиваться с такой ситуацией часто. Ещё менее вероятно, что у вас будет повод использовать её — нам не нужна выделенная ссылка на window, когда можно обратиться к нему напрямую. Хотя привязка по умолчанию может иметь смысл с точки зрения анатомии JavaScript, для такого опасного случая использования, как воздействие на объект, связанный с этой функцией, даже если это означает вмешательство в window, она не слишком нужна. По этой причине в более поздних версиях JavaScript это поведение было усовершенствовано, но изменение правил JavaScript как есть потенциально привело бы к поломке несметного количества веб-сайтов, поэтому изменения были внесены в Строгий режим.

В контексте строгого режима такое поведение гораздо более предсказуемо: при вызове функции по идентификатору в глобальной области видимости this получает значение undefined:

function theFunction() {
"use strict";
return this;
}

theFunction();
// результат: undefined

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

Естественно, эта функция по-прежнему доступна как метод window — такова природа JavaScript и всё такое. Что же произойдёт, если мы вызовем её как метод? В этом случае мы сделаем window объектом, неявно связанным с этой функцией:

function theFunction() {
"use strict";
return this;
}

theFunction();
// результат: undefined

window.theFunction();
// результат: Window { … }

Мы вызываем функцию как метод window, поэтому windowэто объект, связанный с нашей функцией в момент её выполнения, то есть это ссылка на window. Через неё мы получаем доступ ко всем родительским свойствам и методам объекта, на который она ссылается:

function theFunction() {
"use strict";
if ( this !== undefined ) {
console.log( this.getComputedStyle );
} else {
return this;
}
}

theFunction();
// результат: undefined

window.theFunction();
// результат: function getComputedStyle()

Полезно ли это, как показано здесь? Ну, всё ещё нет, не особенно; не нужно на этом останавливаться, когда у нас уже есть window. На самом деле, я бы предостерёг от намеренного использования this для ссылки на globalThis — лучше использовать window, self или даже сам globalThis, которые дадут именно то, что нужно, независимо от окружающего контекста.

Для наших целей этот фрагмент кода иллюстративен, хотя и не пригоден для copy-paste: это прекрасный пример неявной привязки, и это то, что вы обязательно будете использовать.

Неявная привязка

Как вы только что видели, когда функция вызывается как метод объекта, значение this в контексте выполнения этой функции является ссылкой на объект, содержащий метод. Это даёт доступ ко всем методам и свойствам, находящимся рядом с методом.

const theObject = {
theString: "This is a string!",
theMethod() {
console.log( this.theString );
}
};

theObject.theMethod();
// результат This is the string!

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

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

this в стрелочных функциях

У стрелочных функций нет собственных привязок this. Вместо этого значение this в стрелочной функции разрешается в привязку в лексически окружающей среде. Именно так: лексическая среда — то есть то, где она находится в написанном нами коде, а не контекст выполнения. Внутри стрелочной функции это работает совсем по-другому: оно ссылается на значение this в ближайшем окружающем контексте этой функции, как написано:

const theObject = {
theMethod() { console.log( this ); },
theArrowFunction: () => console.log( this )
};

theObject.theMethod();
// результат: Object { myMethod: myMethod(), myArrowFunction: myArrowFunction() }

theObject.theArrowFunction();
// результат: Window {...}

Здесь myObject.theMethod() — классическое неявное связывание. Как и в предыдущих примерах: когда вызывается theMethod и для него создаётся контекст выполнения функции, значение this устанавливается ссылкой на объект, содержащий метод.

Однако это не так, когда мы вызываем myObject.theArrowFunction(): эта функция наследует значение this из лексически окружающей среды — значение globalThis. Поскольку мы находимся вне строгого режима, this оказывается ссылкой на window — если бы мы находились в строгом режиме, оно было бы undefined.

Стрелочные функции исключительно полезны во многих отношениях, и отсутствие собственной привязки может быть одним из них, если их использовать очень осторожно. По этой причине (а также по другим причинам) обычно считается хорошей практикой избегать использования стрелочных функций в качестве метода — функции, определяемой как свойство объекта, — если только вы не можете привести веские аргументы в его пользу.

Явная привязка

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

function theFunction() {
"use strict";
console.log( this );
}

const theObject = {
theValue: "This is a string!"
};

theFunction();
// результат: undefined

theFunction.call( theObject );
// результат: Object { theValue: "This is a string!" }

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

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

function theFunction() {
"use strict";
console.log( this );
}

const theObject = {
theValue: "This is a string!"
};

const boundFunction = theFunction.bind( theObject );

theFunction();
// результат: undefined

boundFunction();
// результат: Object { theValue: "This is a string!" }

Когда вызывается функция theFunction, this ссылается на globalThis — потому что мы находимся в строгом режиме, в результате чего this оказывается undefined. Однако когда функция вызывается с помощью call() (или bind() для создания привязанной функции) с theObject в качестве аргумента, то this содержит ссылку на этот объект. Явная привязка переопределяет неявную привязку:

const theObject = {
theValue : "This string sits alongside myMethod.",
theMethod() {
console.log( this.theValue );
}
};

const someOtherObject = {
theValue : "This is a string in another object entirely!",
};

theObject.theMethod();
// результат: This string sits alongside myMethod.

theObject.theMethod.call( someOtherObject );
// результат: This is a string in another object entirely!

Мы немного отклонились от темы, но явная привязка связана с одним интересным осложнением: не существует правила, согласно которому нельзя привязать что-то, отличное от объекта — например, примитив.

function theFunction() {
"use strict";
console.log( this );
}

theFunction.call( "How'd this string get here?" );
// результат: How'd this string get here?

Необходимость использовать явную привязку возникает довольно редко, и я не могу представить себе случай, когда вы бы сделали это с не-объектом специально, но с JavaScript нельзя предугадать, поэтому на всякий случай стоит знать уникальные особенности поведения.

Переданное значение this не привязывается в строгом режиме — this просто принимает это значение:

"use strict";
function theFunction(){
"use strict";
console.log( typeof this, this );
}

theFunction.call( 7 );
// результат: number 7

theFunction.call( "A string." );
// результат: string A string.

theFunction.call( undefined );
// результат: undefined undefined

theFunction.call( null );
// результат: object null
// Хотите верьте, хотите нет, но `typeof null` - это `object`! Посмотрите <https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/typeof#typeof_null>

Как это часто бывает, вне строгого режима всё становится немного страннее. Если функция вызывается таким образом, что ей присваивается значение undefined или null, это значение заменяется на globalThis.

function theFunction() {
console.log( this );
}

theFunction.call( null );
// результат: Window {...}

Если функция вызывается таким образом, чтобы предоставить ей примитивное значение, то this заменяется объектом-обёрткой примитивного значения вне строгого режима:

function theFunction() {
console.log( this );
}

theFunction.call( 10 );
// результат: Number { 10 }

Возможно, вы спросите: Чем это может быть полезно?. Вы будете правы, если спросите! Признаюсь вам, я так и делаю уже много лет.

Дело в том, что природа веба такова, что мы не можем знать, не было ли у кого-то дико непонятного случая использования (или ошибки), когда JavaScript, запущенный на его сайте, полагался на это непонятное поведение, и поэтому природа стандартов, регулирующих JavaScript, такова, что они не могут быть изменены без согласования. Поэтому вне строгого режима это поведение сохраняется и по сей день. Если вы встречали такое поведение в legacy-коде — поделитесь в комментариях!

В общем, явная привязка — это часть языка, и её нужно использовать без колебаний, там и тогда, где она нужна. Тем не менее, когда я пишу что-то новое, я стараюсь не использовать явную привязку для чего-либо несущего нагрузку; это нарушает контекстуальную полезность this и превращает его в аргумент, который отчаянно хочет быть слишком умным для своего же блага.

Привязка new

Когда класс используется в качестве конструктора с помощью ключевого слова new, this ссылается на вновь созданный объект:

class TheClass {
theString;
constructor() {
this.theString = "A string.";
}
logThis() {
console.log( this );
}
}

const thisClass = new TheClass();

thisClass.logThis();
// результат: Object { theString: "A string.", ... }

Но помните, что значение this устанавливается в момент выполнения метода logThis, а это значит, что значение может быть переопределено способом вызова метода внутри класса:

class TheClass {
successMsg = "You clicked the button!";
constructor( theElement ) {
theElement.addEventListener('click', this.logThis );
// результат: <button>
}
logThis() {
console.log( this );
};
}

const theButton = document.querySelector( "button" )
const theConstructedObject = new TheClass( theButton );

Представьте, что необходимо что-то сделать со свойством экземпляра, содержащим строку You clicked the button! в ответ на нажатие кнопки. Спойлеры будут чуть позже, но this принимает другое значение в контексте слушателя событий — из-за области выполнения метода logThis this не имеет нужного нам значения.

Можно привести аргумент, что это как раз тот случай, когда стрелочные функции наследуют своё значение this от лексической области видимости, а не от области выполнения. Если написать метод logThis как стрелочную функцию, то область выполнения обработчика событий уже не имеет значения, а лексической областью для метода станет экземпляр класса:

class TheClass {
successMsg = "You clicked the button!";

constructor( theElement ) {
theElement.addEventListener('click', this.logThis );
// результат: Object { successMsg: "You clicked the button!", logThis: logThis() }
}

logThis = () => {
console.log( this );
};
}

const theButton = document.querySelector( "button" )
const theConstructedObject = new TheClass( theButton );

Как и в случае с классами, когда функция вызывается с помощью new, this внутри этой функции является экземпляром этой функции-конструктора:

function TheConstructorFunction() {
this.theString = "A string.";

this.logThis = function() {
console.log( this );
}

}
const theConstructedObject = new TheConstructorFunction();

theConstructedObject.logThis();
// результат: Object { theString: "A string.", ... }

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

function TheConstructorFunction() {
this.logThis = function() {
console.log( this.constructor.name );
}
}

const theConstructedObject = new TheConstructorFunction();

theConstructedObject.logThis();
// результат: TheConstructorFunction

Если бы эта функция-конструктор была написана с использованием return — не лучшая практика, но я видел, как это делается, — объект, который явно возвращается, мог бы не быть экземпляром функции-конструктора:

function TheConstructorFunction() {
return {
logThis: function() {
console.log( this.constructor.name );
}
};
}
const theConstructedObject = new TheConstructorFunction();

theConstructedObject.logThis();
// результат: Object

Мы снова немного отклонились от темы, но это не значит, что нельзя использовать this в приведённом выше примере, используя нашу лаконичную, но удивительно сложную подругу — стрелочную функцию.

function TheConstructorFunction() {
return {
logThis: function() {
console.log( this.constructor.name );
},
logThat: () => console.log( this.constructor.name ),
};
}

const thing = new TheConstructorFunction();

thing.logThis();
// результат: Object

thing.logThat();
// результат: TheConstructorFunction

В этом примере logThat наследует значение this из лексической области TheConstructorFunction.

Для тех, кто ведёт счёт: мы используем стрелочную функцию в качестве метода, чего обычно следует избегать, для решения проблемы с функцией-конструктором, возвращающей объект, чего определённо следует избегать — мы на два уровня углубились в территорию «сомнительной практики».

Тем не менее, я лучше других знаю, что мы не всегда можем полностью контролировать кодовые базы, которые наследуем; стоит знать крайние случаи, на случай, если вы однажды столкнётесь с ними.

Привязка обработчика событий

Ах, никогда не забудешь свой первый this. Старый фаворит. Классика. Внутри функции обратного вызова обработчика события this ссылается на элемент, связанный с обработчиком. Вот и всё!

document.querySelector( "button" ).addEventListener( "click", function( event ) {
console.log( this );
// результат: <button class="btn">
});

Когда пользователь вызывает это событие click на элементе button, результирующее значение this является ссылкой на объект элемента <button>.

Технически, это не совсем отдельный случай; за кулисами JavaScript эффективно использует call() для установки значения this. То же самое можно сделать с помощью bind(), чтобы создать привязанную функцию для использования в качестве обратного вызова, и тогда this будет явно ссылаться на указанный объект:

const button = document.querySelector( "button" );

const theObject = {
"theValue" : true
};

function handleClick() {
console.log( this );
}

button.addEventListener( "click", handleClick.bind( theObject ) );
// результат: Object { theValue: true }

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

let button = document.querySelector( "button" );

button.addEventListener( "click", ( event ) => { console.log( this ); } );
// результат: Window { … }

Но это не значит, что нельзя использовать стрелочную функцию в качестве функции обратного вызова обработчика событий, однако обращаться к ней следует только в тех случаях, когда вы явно хотите полагаться на лексическую область для определения значения this — например, обработчик событий внутри метода, прикреплённого к объекту, на который необходимо сослаться из функции обратного вызова обработчика событий. Это может показаться довольно специфическим случаем использования, написанным таким образом, но это то, с чем я сталкивался в своей повседневной работе, и, скорее всего, рано или поздно вы тоже столкнётесь.

Вот что такое this

Такая сильная зависимость от контекста делает this особенно коварным — разобраться в нём методом тыка почти невозможно. Если использовать его неправильно, вместо явной ошибки вы получите совершенно неожиданный объект, а поиск почему this вдруг стал window скорее приведёт вас к горячим философским дискуссиям, чем к ответам на Stack Overflow. Даже самые опытные разработчики иногда попадают в ловушку — и ваш покорный слуга не исключение.

Если вы дочитали досюда — все лавры по праву ваши: вы пробирались через одну из самых печально известных головоломок JavaScript. Если же вы перескочили сюда, чтобы узнать, чем всё закончится, — в этом тоже нет ничего зазорного. Никто не обязан запоминать все тонкости this после первого, второго или даже десятого прочтения пары статей — каким бы гениальным и обаятельным ни был их автор. Но this встречается в разработке так же часто, как и остаётся загадочным, так что, скорее всего, вам предстоит провести с ним немало качественного времени.

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

Комментарии


Дополнительные материалы

Предыдущая Статья

Error.isError(): Лучший способ проверки типов ошибки в JavaScript

Следующая Статья

Мемоизация кэша в Laravel