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, которое является ссылкой на объект, «владеющий» текущим исполняемым кодом.
this по умолчанию
Чаще всего this используется внутри функции/метода. Однако this также работает в глобальной области.
Когда мы пишем простую функцию и затем её используем, она все равно вызывается каким-то объектом и this ссылается на этот вызывающий объект. Для браузера — это объект window.
Запустим такой код в консоли браузера:
function get_this() < return this >get_this() // ▸ Window this // ▸ Window
Как мы видим в обоих случаях this будет объектом Window. Это равносильно такому коду:
window.get_this = function() < return this >window.get_this() // ▸ Window window.this // ▸ Window
Однако если функция выполняется в строгом режиме, то в this будет записано undefined , так как в этом режиме запрещены привязки по умолчанию:
function get_this() < 'use strict' return this >get_this() // ▸ undefined window.get_this() // ▸ Window
Т.е. в строгом режиме нужно прямо указывать контекст из которого вызывается метод:
this в методах
Метод вызывается наглядно: сначала идет название объекта ourObject , затем точка . , затем название метода ourMethod .
this очень легко отследить, когда метод вызывается в сочетании с объектом object.method() — что находится с левой стороны от точки, то и будет в this вызываемого метода (в данном случае this = object):
let object = < method: function()< console.log( this ) >> object.method() // ▸
Это самый простой вариант, чтобы понять что будет находится в this. Тут метод вызван из того же объекта в котором он определен. Поэтому this ссылается на экземпляр object потому что method вызывается этим объектом. this тут это вызывающий объект.
this в функции-конструктора
Когда для создания новых объектов используется функция, её нужно вызывать с помощью ключевого слова new. При таком вызове функция будет вызвана как конструктор нового объекта и новый объект будет возвращен.
Название функции в этом случае принято писать с заглавной буквы. Это название станет названием экземпляра объекта.
Когда вызывается конструктор, this указывает на вновь созданный объект, а не на объект который запустил построение.
function Doge( param ) < // this = <>— новый пустой объект, который вызвал эту функцию благодаря new this.saying = param // return this // это конструктор, он не может ничего возвращать > new Doge( ‘Привет’ ) // ▸ Doge new Doge( ‘Браузер’ ) // ▸ Doge
Заглянем под капот, чтобы лучше понять почему 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 // ▸ Doge
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 // ▸ Doge
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 // ▸ Window
Как мы видим функция была вызвана объектом window и она добавила в вызываемый объект window новое свойство saying и метод get_this() . Убедимся в этом, посмотрим значение свойства saying и вызовем метод get_this().
window.saying // «Кто здесь?» window.get_this() // Window
Итого про this
Значение this устанавливается в зависимости от того, как вызвана функция:
При вызове функции в качестве метода, контекстом будет вызываемый объект:
obj.func(. ) // this = obj obj["func"](. ) // this = obj
При обычном вызове, контекстом будет глобальный объект она будет вызвана без контекста:
func(. ) // this = window (ES3) / undefined (ES5 - 'use strict')
При использовании ключевого слова new, создается новый чистый контекст.
new func() // this = <> (новый объект)