Что такое this
в JavaScript
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, для такого опасного случая использования, как воздействие на объект, связанный с этой функцией, даже если это означает вмешательство в
, она не слишком нужна. По этой причине в более поздних версиях JavaScript это поведение было усовершенствовано, но изменение правил JavaScript window
как есть
потенциально привело бы к поломке несметного количества веб-сайтов, поэтому изменения были внесены в Строгий режим.
В контексте строгого режима такое поведение гораздо более предсказуемо: при вызове функции по идентификатору в глобальной области видимости 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
, вы на два шага приблизились к пониманию почему — а значит, и к уверенной работе с ним в повседневных задачах.