this в JavaScript — что такое контекст
Одна из причин, по которой ключевое слово this
такое не простое для многих программистов JavaScript (даже опытных) - заключается в том, что, значение this
зависит от контекста, иногда довольно сложным образом. Чтобы разобраться с this
, важно не только понять, как «this» ведет себя в различных контекстах, но и понять сами контексты - и как их определить в живом коде.
Теоретические основы
this
— это ключевое слово в JavaScript которое содержит в себе объект (контекст) выполняемого кода.
Мне кажется, что проще всего представить, что this
— это уникальная переменная, которая хранит в себе контекст исполняемого кода. И наоборот — контекст — это значение ключевого слова this.
this
имеет различные значения в зависимости от того, где используется:
- Сама по себе - this относится к глобальному объекту (window).
- В методе - this относится к родительскому объекту.
- В функции - this относится к глобальному объекту.
- В функции в 'strict mode' - this = undefined.
- В стрелочной функции - this относится к контексту где функция была создана.
- В событии - this ссылается на элемент запустивший событие.
Что такое контекст?
Любое определение this, так или иначе крутится вокруг слова контекст. Давайте разберемся что же это такое.
JavaScript является одно-поточным языком, то есть одновременно может выполняться только одна задача. Интерпретатор JavaScript всегда начинает выполнять код с глобального контекста (в браузере это объект window). С этого момента у самой первой вызванной функции контекстом будет глобальный объект (window). Наша функция может создать новый объект, создать в нем методы и запустить один из этих методов, теперь контекст вызова изменился и стал указывать на новый объект, который был создан из глобального контекста.
Таким образом, контекстом всегда является какой-то объект из под которого был вызван метод (функция).
По ходу обработки кода внутри глобального контекста window создаются другие объекты, они представляют собой новые контексты в которых выполняется код. Таким образом, в одном коде может быть замешено много контекстов. Одна функция может вызывать методы разных объектов и у каждого из них будет свой контекст.
Контекст
— это всегда значение ключевого слова this, которое является ссылкой на объект, который запустил метод (функцию). Контекст — это объект "владеющий" исполняемым кодом. А this
всегда ссылаться на объект (контекст) запустивший функцию.
Методы и функции
В теле каждого метода или функции, которая по сути всегда является методом какого-то объекта мы можем использовать this
, чтобы обратится к родительскому объекту который вызвал функцию, или в котором находится метод. Упрощенно можно сказать — this
— это родительский объект который вызвал метод (функцию).
Например, когда this
используется внутри функции my_function(), можно сказать что this
это универсальная переменная в значении которой лежит объект, который вызвал функцию my_function().
Это удобно, потому что:
- Неудобно каждый раз писать в коде название родительского объекта, чтобы вызвать его метод (функцию), а можно написать
this.my_function()
вместоwindow.my_object.my_function()
. - Иногда мы вообще можем заранее не знать объект, который вызвал функцию. Потому что программист написал код так, что объект (контекст) нужно указать при вызове функции (этот подход позволяет использовать одну и ту же функцию/метод, для разных объектов).
Представить что this
это переменная ссылающаяся на объект, который вызвал метод (функцию), сильно упрощает использование this в вашем коде.
Важно понять, что this не зависит от того, где была объявлена функция, а зависит от того, как (кем) функция была вызвана. То где функция была объявлена имеет отношение к области её видимости (scope), а не к контексту.
Область видимости (scope) и Контекст
Кроме контекста нужно еще понимать что такое область видимости. Эти понятия часто путают. Называя одно другим. Однако контекст и область видимости это разные вещи.
Каждый вызов функции имеет как область видимости, так и контекст, связанный с ней. Область видимости основана на том, где функция вызывается (какие переменные ей доступны), а контекст основан на том, кем функция вызывается (каким объектом). Другими словами, область видимости относится к доступу функции к переменным при ее вызове и является уникальной для каждого вызова. Контекст - это всегда значение ключевого слова this, которое является ссылкой на объект, "владеющий" текущим исполняемым кодом.
Подробнее Scope and Context in JavaScript.
this по умолчанию
Чаще всего this используется внутри функции/метода. Однако this также работает в глобальной области.
Когда мы пишем простую функцию и затем её используем, она все равно вызывается каким-то объектом и this ссылается на этот вызывающий объект. Для браузера - это объект window.
Запустим такой код в консоли браузера:
function get_this(){ return this } get_this() // ▸ Window {0: Window, window: Window, self: Window, …} this // ▸ Window {0: Window, window: Window, self: Window, …}
Как мы видим в обоих случаях this будет объектом Window. Это равносильно такому коду:
window.get_this = function(){ return this } window.get_this() // ▸ Window {0: Window, window: Window, self: Window, …} window.this // ▸ Window {0: Window, window: Window, self: Window, …}
Однако если функция выполняется в строгом режиме, то в this будет записано undefined, так как в этом режиме запрещены привязки по умолчанию:
function get_this(){ 'use strict' return this } get_this() // ▸ undefined window.get_this() // ▸ Window {0: Window, window: Window, self: Window, …}
Т.е. в строгом режиме нужно прямо указывать контекст из которого вызывается метод:
this в методах
Метод вызывается наглядно: сначала идет название объекта ourObject
, затем точка .
, затем название метода ourMethod
.
this очень легко отследить, когда метод вызывается в сочетании с объектом object.method()
— что находится с левой стороны от точки, то и будет в this вызываемого метода (в данном случае this = object):
let object = { method: function(){ console.log( this ) } } object.method() // ▸ {method: ƒ}
Это самый простой вариант, чтобы понять что будет находится в this. Тут метод вызван из того же объекта в котором он определен. Поэтому this ссылается на экземпляр object потому что method вызывается этим объектом. this тут это вызывающий объект.
this в функции-конструктора
Когда для создания новых объектов используется функция, её нужно вызывать с помощью ключевого слова new. При таком вызове функция будет вызвана как конструктор нового объекта и новый объект будет возвращен.
Название функции в этом случае принято писать с заглавной буквы. Это название станет названием экземпляра объекта.
Когда вызывается конструктор, this указывает на вновь созданный объект, а не на объект который запустил построение.
function Doge( param ) { // this = {} - новый пустой объект, который вызвал эту функцию благодаря new this.saying = param // return this // это конструктор, он не может ничего возвращать } new Doge( 'Привет' ) // ▸ Doge {saying: "Привет"} new Doge( 'Браузер' ) // ▸ Doge {saying: "Браузер"}
Заглянем под капот, чтобы лучше понять почему this в функции-конструктора работает именно так. Когда функция вызывается через new
вызов этой функции происходит иначе:
- Создается пустой объект, название которого берется из названия функции.
- Созданный объект вызывает функцию как конструктор. Так как функцию вызывает новый объект, то this внутри этой функции ссылается на этот новый объект.
- Функция-конструктор отрабатывает и
new
возвращает вновь созданный объект.
В такой функции не нужно использовать return - в этом нет смысла, потому что она является конструктором объекта. В задачи функции-конструктора входит установка свойств и методов нового объекта и она не может ничего возвращать.
Усложним: вызов функции внутри конструктора
Что будет если мы поместим обычную функцию внутрь функции-конструктора и вызовем её там?
У нас есть глобальная функция getThis(), функция-конструктора и новый объект, созданный функцией-конструктора. В этом примере глобальная getthis() вызываются изнутри функции-конструктора:
function get_this(){ console.log( this ) } function Doge( saying ){ this.saying = saying get_this() } new Doge( 'Не Window!' ) // увидим в консоли: // ▸ Window {0: Window, window: Window, self: Window, …} // ▸ Doge {saying: "Не Window!"}
get_this() все еще указывает на Window, потому что, несмотря на то что get_this() вызывается внутри функции конструктора Doge(), она фактически вызывается из глобальной области — window.get_this()
.
А что, если мы создадим метод внутри конструктора и вызовем его в конструкторе при создании нового объекта?
function Doge( saying ){ this.saying = saying this.get_this = function(){ console.log( this ) } this.get_this() } new Doge( 'не Window' ) // создадим экземпляр Doge // ▸ Doge {saying: "не Window", getThis: ƒ} // ▸ Doge {saying: "не Window", getThis: ƒ}
get_this() по-прежнему указывает на вновь созданный объект. Потому что именно он его вызвал.
А что если, мы вызовем Doge без ключевого слова new?
function Doge( saying ){ this.saying = saying this.get_this = function(){ console.log( this ) } this.get_this() console.log( this ) } Doge( 'Кто здесь?' ) // запускаем Doge как простую функцию // Получим в консоли: // ▸ Window {0: Window, window: Window, self: Window, …} // ▸ Window {0: Window, window: Window, self: Window, …}
Как мы видим функция была вызвана объектом window и она добавила в вызываемый объект window новое свойство saying
и метод get_this()
. Убедимся в этом, посмотрим значение свойства saying и вызовем метод get_this().
window.saying // "Кто здесь?" window.get_this() // Window {0: Window, window: Window, self: Window, …}
Итого про this
Значение this устанавливается в зависимости от того, как (кем) вызвана функция:
При вызове функции в качестве метода, контекстом будет вызываемый объект:
obj.func(...) // this = obj obj["func"](...) // this = obj
При обычном вызове, контекстом будет глобальный объект она будет вызвана без контекста:
func(...) // this = window (ES3) / undefined (ES5 - 'use strict')
При использовании ключевого слова new, создается новый чистый контекст.
new func() // this = {} (новый объект)