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 вызов этой функции происходит иначе:

  1. Создается пустой объект, название которого берется из названия функции.
  2. Созданный объект вызывает функцию как конструктор. Так как функцию вызывает новый объект, то this внутри этой функции ссылается на этот новый объект.
  3. Функция-конструктор отрабатывает и 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 = {} (новый объект)