- this в JavaScript — что такое контекст
- Теоретические основы
- Что такое контекст?
- Методы и функции
- Область видимости (scope) и Контекст
- this по умолчанию
- this в методах
- this в функции-конструктора
- Усложним: вызов функции внутри конструктора
- Итого про this
- Область видимости переменной в Javascript (ликбез)
- Объявление переменных
- Наследование области видимости
- this
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 = <> (новый объект)
Область видимости переменной в Javascript (ликбез)
Переменные в Javascript бывают глобальными и локальными. Глобальная переменная доступна везде, локальная — только в текущей области видимости.
Технически, глобальные переменные — всего лишь свойства объекта window , поскольку весь код выполняется в его контексте.
Из этого следует, что глобальные переменные могут затирать свойства window (я уже молчу о том, что они зло, нарушают инкапсуляцию и все такое).
Объявление переменных
При присвоении значения неопределенной локальной переменной используется или создается глобальная переменная.
function foo() < a = 2; b = 3; return a+b; >alert(a); // undefined a = 'очень важное значение'; alert(a); // очень важное значение foo(); alert(a); // 2
Таким образом можно легко затереть лишнего. По-моему такое поведение абсолютно нелогично, но, что ж, это не самое странное место яваскрипта. В любом случае неявного определения переменных стоит избегать.
Явно объявлять переменные можно и нужно ключевым словом var .
Такая строка всегда создает новую локальную переменную. Если объявление происходит вне функций, то она будет глобальной, что вполне логично.
function foo() < var a = 2; var b = 3; return a+b; >alert(a); // undefined var a = 'очень важное значение'; alert(a); // очень важное значение foo(); alert(a); // очень важное значение
Как объявить глобальную переменную из функции? Как обратиться к глобальной переменной, если есть локальная с таким же именем? Очень просто — нужно обратиться к ней как к свойству window :
function foo() < var location = 'location'; alert(location); // вернет 'location' alert(window.location); // вернет window.location window.a = 'переменная из функции'; >alert(a); // undefined foo(); alert(a); // переменная из функции
Наследование области видимости
Меня всегда смущало то, что в Javascript можно определять функции внутри функций, а использовать их потом где угодно. Ну да, если посмотреть, точно то же самое можно делать в Ruby, и, наверное, во многих других языках тоже.
Переменные при этом передаются очень просто: если на момент определения функции переменная существовала, то она будет существовать и внутри функции. Откуда бы ее не вызывали.
function alertOnTimeout(message, timeout) < return setTimeout(function() < // message будет доступен в безымянной функции, переданной таймауту alert(message); >, timeout); >
Передача кода по старинке — строкой, которая прогоняется через eval() — не попадает под это правило, код исполняется в той области видимости, где и определен.
Поскольку объекты в Javascript — это тоже типа функции, то свойство объекта определяется точно так же, как и переменная.
А еще в Javascript область видимости переменной ограничивается только функциями, а не блоками типа if (привет, Паскаль). Потому удобнее всего объявлять переменные в начале функции.
this
А что this ? А то, что эта переменная автоматически появляется в методах объектов и затирает значение this из предыдущей области видимости. Решение простое — переприсваивать ее значение другой переменной.
$('div.with-links').click(function() < var theDiv = this; //сохраняем значение this $(this).find('a').click(function() < alert($(this).attr('href')); // this - это ссылка theDiv.remove(); // а theDiv - это все еще дивак >); >);
Отдельно замечу, что при оборачивании какой-то поведенческой логики в объект надо помнить, что в создаваемых DOM-событиях значение this самого объекта теряется.