Read this article in: English, Chinese (version1, version2, version 3), Korean, French.
Введение
В данной небольшой заметке мы рассмотрим ещё одну сущность, связанную с контекстами исполнения. Речь пойдёт о ключевом слове this
.
Как показывает практика, данная тема является достаточно сложной и часто вызывает затруднения в определении значения this
в том или ином контексте.
Многие привыкли, что ключевое слово this
в языках программирования тесно связано с объектно-ориентированным программированием, а именно, указывает на текущий порождаемый конструктором объект. В ECMAScript данная концепция также реализована, однако, как мы увидим, здесь this
не ограничивается лишь определением порождаемого объекта.
Давайте подробней разберём, что же такое this
в ECMAScript.
Определение
Итак, this
является свойством контекста исполнения:
activeExecutionContext = { VO: {...}, this: thisValue };
где VO — это объект переменных (variable object), который мы подробно разбирали в предыдущей статье.
Значение this
напрямую связанно с типом исполняемого кода контекста. Определяется оно при входе в контекст и на протяжении исполнения кода контекста, является неизменным.
Рассмотрим эти случаи подробней.
This в глобальном коде
Здесь довольно всё просто. В коде глобального контекста, значением this
всегда является сам глобальный объект (global); таким образом, можно косвенно к нему обратиться:
// явное объявление свойства // глобального объекта this.a = 10; // global.a = 10 alert(a); // 10 // косвенное, посредством присваивания // неопределённому до этого идентификатору b = 20; alert(this.b); // 20 // также косвенное, посредством объявления // переменной, поскольку объектом переменных // в глобальном контексте является сам глобальный объект var c = 30; alert(this.c); // 30
This в коде функции
Со значением this
в коде функции — интереснее. Именно в этом моменте возникает большинство затруднений.
Первая (и, возможно, главная) особенность значения this
в этом типе кода заключается в том, что оно здесь не связано статично с функцией.
Как уже было сказано выше, значение this
определяется при входе в контекст, и в случае с кодом функции, каждый раз может быть абсолютно разным.
Однако, на протяжении исполнения кода контекста, значение this
является неизменным, т.е. нельзя присвоить ему новое значение динамически в рантайме, т.к. this
не является переменной (в отличие, скажем, от языка программирования Python, и его явно определяемого объекта self
, значение которого может неоднократно меняться по ходу исполнения кода контекста):
var foo = {x: 10}; var bar = { x: 20, test: function () { alert(this === bar); // true alert(this.x); // 20 this = foo; // ошибка, нельзя менять this alert(this.x); // если бы не было ошибки, было бы 10, а не 20 } }; // при входе в контекст, значение this // определёно как объект "bar"; почему – будет // подробно разобрано ниже bar.test(); // true, 20 foo.test = bar.test; // однако, здесь уже this указывает // на "foo" - при вызове той же функции foo.test(); // false, 10
Итак, от чего же зависит меняющееся значение this
в коде функции? Здесь есть несколько факторов.
Во-первых, при обычном вызове функции, this
определяется вызывающей стороной, которая активирует код контекста функции, — так называемый, caller, т.е. родительский контекст, который вызывает функцию. Определение значения this
происходит по форме выражения вызова (иными словами, как синтаксически вызвана функция).
Это очень важный момент, его нужно понять, запомнить и чётко осознавать, тогда, с определением значения this
в любом контексте, никаких проблем возникать не будет. Именно форма выражения вызова влияет на значение this
вызываемого контекста, и ничто другое.
Так, например, в некоторых статьях и даже книгах по JavaScript, иногда можно видеть неверные утверждения, где описания вроде: “this
зависит от того, как описана функция: если это глобальная функция, то this
будет указывать на глобальный объект, если же функция является методом объекта, то this
всегда будет объектом, которому принадлежит вызываемый метод” — являются ошибочными. Забегая вперёд, покажем, что уже обычную глобальную функцию можно активировать разными формами вызова, которые и влияют на разное значение this
:
function foo() { alert(this); } foo(); // global alert(foo === foo.prototype.constructor); // true // но при иной форме вызова той же // функции, this будет иметь уже другое значение foo.prototype.constructor(); // foo.prototype
Аналогично можно вызвать функцию, описанную как метод объекта, но значением this
не будет являться этот объект:
var foo = { bar: function () { alert(this); alert(this === foo); } }; foo.bar(); // foo, true var exampleFunc = foo.bar; alert(exampleFunc === foo.bar); // true // опять же, при иной форме вызова той же // функции, this уже установлен в другое значение exampleFunc(); // global, false
Каким же образом форма выражения вызова влияет на значение this
? Здесь всё завязано на один из внутренних типов реализации — тип Reference
, который, для полного понимания определения значения this
, нужно рассмотреть подробней.
Тип Reference
Псевдокодом, значение типа Reference
можно представить в виде обычного объекта с двумя свойствами: база (base) (т.е. объект, которому принадлежит свойство) и имя свойства (property name) внутри базы:
var valueOfReferenceType = { base: <base object>, propertyName: <property name> };
Значение типа Reference
может быть получено только в двух случаях:
- когда мы имеем дело с идентификатором (identifier);
- либо же с выражением доступа к свойству (property accessor);
Процесс разрешения имён идентификаторов (identifier resolution) подробно рассматривается в четвёртой части (Цепь областей видимости, Scope chain). Здесь же отметим, что на выходе данного алгоритма всегда будет значение типа Reference
(это важно для значения this
).
Идентификаторами являются имена переменных, функций, формальных параметров функций и неявные свойства глобального объекта. К примеру, для значений по следующим идентификаторам:
var foo = 10; function bar() {}
в промежуточных операциях, будут получены соответствующие значения типа Reference
:
var fooReference = { base: global, propertyName: 'foo' }; var barReference = { base: global propertyName: 'bar' };
Для получения реального значения объекта из значения типа Reference
предусмотрен метод GetValue
, который псевдокодом можно описать следующим образом:
function GetValue(value) { if (Type(value) != Reference) { return value; } var base = GetBase(value); if (base === null) { throw new ReferenceError; } return base.[[Get]](GetPropertyName(value)); }
где внутренний метод [[Get]]
получает значение свойства объекта, учитывая также и наследуемые свойства из цепи прототипов:
GetValue(fooReference); // 10 GetValue(barReference); // function object "bar"
Выражение доступа к свойству (property accessor), так же многим известно. Осуществляется оно либо через точечную нотацию (когда имя свойства является правильным идентификатором и заранее известно), либо же — через скобочную:
foo.bar(); foo['bar']();
На выходе промежуточного вычисления также будет получено значение типа Reference
:
var fooBarReference = { base: foo, propertyName: 'bar' }; GetValue(fooBarReference); // function object "bar"
Итак, как же связано значение типа Reference
со значением this
контекста функции? — Самым главным образом. Данный момент является основным в этой статье. Правило определения this
в контексте функции звучит следующим образом:
Значение this
в контексте функции определяется вызывающей стороной (caller-ом) по форме вызова.
Если слева от скобок вызова ( ... )
, находится выражение типа Reference
, то значением this
будет являться базовый объект этого значения типа Reference
.
Во всех остальных случаях (т.е. при любом другом типе значения, отличном от типа Reference
), значением this
будет всегда являться null
. Но, т.к. null
особого смысла для значения this
не несёт, автоматом подставляется глобальный объект.
Покажем на примерах:
function foo() { return this; } foo(); // global
Видим, что слева от скобок вызова стоит выражение типа Reference
(поскольку foo
— это идентификатор):
var fooReference = { base: global, propertyName: 'foo' };
Соответственно, значение this
определенно, как база этого значения типа Reference
, т.е. глобальный объект. Аналогично с выражением доступа к свойству:
var foo = { bar: function () { return this; } }; foo.bar(); // foo
Имеем, опять же, значение типа Reference
, где базой является объект foo
, который и будет использован в качестве значения this
при активации функции bar
:
var fooBarReference = { base: foo, propertyName: 'bar };
Однако, активируя ту же самую функцию, но с другой формой вызова, мы имеем уже другое значение this
:
var test = foo.bar; test(); // global
поскольку test
, являясь идентификатором, порождает другое значение типа Reference
, то именно база этого нового значения типа Reference
и будет использована в качестве значения this
— т.е. глобальный объект:
var testReference = { base: global, propertyName: 'test' };
Примечание: в строгом режиме ES5 значение this
не преобразуется к глобальному объекту, но установлено вместо этого в значение undefined
.
Теперь мы можем точно сказать, почему одна и та же функция, но активированная разными формами вызова, имеет и разные значения this
— ответ заключён в разных промежуточных значения типа Reference
:
function foo() { alert(this); } foo(); // global, т.к. var fooReference = { base: global, propertyName: 'foo' }; alert(foo === foo.prototype.constructor); // true // другая форма вызова foo.prototype.constructor(); // foo.prototype, т.к. var fooPrototypeConstructorReference = { base: foo.prototype, propertyName: 'constructor' };
Ещё (классический) пример динамического определения this
по форме выражения вызова:
function foo() { alert(this.bar); } var x = {bar: 10}; var y = {bar: 20}; x.test = foo; y.test = foo; x.test(); // 10 y.test(); // 20
Вызов функции и НЕ тип Reference
Итак, как мы уже отметили, в случае, когда слева от скобок вызова находится значение не типа Reference
, а любого другого типа, значение this
будет автоматически определенно как null
, и, как следствие, global.
Рассмотрим примеры таких выражений:
(function () { alert(this); // null => global })();
В данном случае мы имеем объект Function
, но не объект типа Reference
(это не идентификатор и не выражение доступа к свойству), соответственно, значение this
в конечном итоге будет определенно, как глобальный объект.
Примеры сложнее:
var foo = { bar: function () { alert(this); } }; foo.bar(); // Reference, OK => foo (foo.bar)(); // Reference, OK => foo (foo.bar = foo.bar)(); // global? (false || foo.bar)(); // global? (foo.bar, foo.bar)(); // global?
Почему же, используя выражение доступа к свойству (property accessor), промежуточным результатом которого должно являться значение типа Reference
, мы, в определённых вызовах, получаем в качестве this
не базовый объект (т.е. foo
), а global?
Дело в том, что последние три вызова, после применения определённых операций, имеют слева от скобок вызова уже значение не типа Reference
.
С первым случаем всё понятно – там однозначно тип Reference
и, как следствие, this
— это база, т.е. foo
.
Во втором случае применяется оператор группировки, который не вызывает, рассмотренный выше, метод получения реального значения объекта из значения типа Reference
, т.е. GetValue
(см. примечание к 11.1.6). Соответственно, на выходе оператора группировки — всё ещё значение типа Reference
, а потому, значение this
снова определено, как база, т.е. foo
.
В третьем случае, оператор присваивания, в отличие от оператора группировки, вызывает метод GetValue
(см. шаг 3 11.13.1). В итоге на выходе уже будет значения типа Function
, означающее, что в качестве this
будет использован null
и, как следствие, global.
Аналогично с четвертым и пятым случаями — оператор запятая и логическое ИЛИ вызывают GetValue
, соответственно, мы теряем значение типа Reference
и получаем значение типа Function
; вновь, this
определён как global.
Тип Reference и значение this равное null
Существует ситуация, когда выражение вызова определит слева от скобок вызова значение типа Reference
, однако значение this
будет определено как null
и, как следствие, global.
Это относится к случаю, когда базовым объектом значения типа Reference
, является объект активации.
Данную ситуацию можно показать на примере с вложенной функцией, вызванной из родительской. Как нам известно из второй части, локальные переменные, локальные функции и параметры функции хранятся в объекте активации данной функции:
function foo() { function bar() { alert(this); // global } bar(); // равносильно AO.bar() }
Объект активации всегда возвращает в качестве значения this
— null
(т.е. схематичная запись AO.b()
равносильна null.b()
). И здесь мы снова возвращаемся к вышеописанному случаю, и снова в качестве this
подставляется глобальный объект.
Исключение может составить вызов внутренней функции в блоке оператора with
, в случае, если объект with
содержит свойство с именем функции. Оператор with
в цепи областей видимости добавляет свой объект перед объектом активации. Соответственно, имея значения типа Reference
(по идентификатору или выражению доступа к свойству), мы имеем базой не объект активации, а объект with
. Кстати, это касается не только вложенной функции, но и глобальной — объект with
“заслонит” вышестоящий объект (глобальный или объект активации) области видимости:
var x = 10; with ({ foo: function () { alert(this.x); }, x: 20 }) { foo(); // 20 } // поскольку var fooReference = { base: __withObject, propertyName: 'foo' };
Аналогичная ситуация должна быть с активацией функции, являющейся параметром выражения catch
: в данном случае объект catch
также добавляется перед объектом активации, либо глобальным объектом. Однако, данное поведение было определенно как баг ECMA-262-3 и исправлено в новой версии стандарта ECMA-262-5; таким образом, значение this
в данной активации должно быть global, но не объект catch
:
try { throw function () { alert(this); }; } catch (e) { e(); // __catchObject - в ES3, global - исправлено в ES5 } // по идее var eReference = { base: __catchObject, propertyName: 'e' }; // однако, это баг, и base // принудительно определяется как // null => global var eReference = { base: global, propertyName: 'e' };
Та же самая ситуация с рекурсивным вызовом именованной функции-выражения (подробней о функциях смотрите в пятой части). При первой активации функции, базой является родительский объект активации (или глобальный объект), при последующих — базой должен быть специальный объект хранящий имя функции-выражения. Однако, в данном случае в качестве this
также всегда используется global:
(function foo(bar) { alert(this); !bar && foo(1); // "должен" быть спец.объект, но всегда global })(); // global
This при вызове функции в качестве конструктора
Есть ещё одна ситуация, связанная со значением this
в контексте функции — это вызов функции в качестве конструктора:
function A() { alert(this); // вновь сознанный объект, ниже - объект "a" this.x = 10; } var a = new A(); alert(a.x); // 10
В данном случае, оператор new вызовет внутренний метод [[Construct]] функции A
, который, в свою очередь, после создания объекта, вызовет внутренний метод [[Call]], всё той же функции А
, передав в качестве значения this
вновь созданный объект.
Явная установка значения this при вызове функций
Существуют два метода, описанные в Function.prototype
(а, соответственно, они доступны всем функциям), позволяющие явно указать значение this
при вызове функции. Это методы apply
и call
.
Оба они принимают в качестве первого параметра значение this
, которое будет использовано в контексте вызова. Разница между этими методами несущественная: для первого из них вторым параметром обязательно должен быть массив (либо массиво-подобный объект, например, arguments
), в свою очередь, call
может принимать любые параметры. Обязательным параметром для обоих методов является лишь первый — значение this
.
Примеры:
var b = 10; function a(c) { alert(this.b); alert(c); } a(20); // this === Global, this.b == 10, c == 20 a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30 a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40
Заключение
В данной заметке мы разобрали особенности ключевого слова this
в ECMAScript (и они, действительно — особенности, в отличие, скажем, от C++ или Java). Надеюсь, статья помогла более чётко представить, как работает эта сущность в JavaScript. Как всегда, буду рад ответить на ваши вопросы в комментариях.
Дополнительная литература
10.1.7 – This;
11.1.1 – Ключевое слово this;
11.2.2 – Оператор new;
11.2.3 – Вызовы функций.
Автор: Dmitry A. Soshnikov
Дополнения и корректировки: Zeroglif
Дата публикации: 29.06.2009; обновление: 07.03.2010;
Большое спасибо за статью, очень актуальный вопрос.
Дмитрий спасибо за замечательную статью!
Объясните пожалуйста чем отличается
(foo.bar = foo.bar)()
от
foo.bar = foo.bar; foo.bar();
И ещё вопрос о том же, я знаю что чтобы вызвать
eval
(да и в принципе любую функцию) в глобальном контексте можно написать что то вроде(eval=eval)("/*global code*/");
или(1,eval)("");
Такая техника называется indirect call.
Могли бы вы пояснить логику выполнения этого выражения?
Спасибо.
@ Sergey
В первом случае, как было отмечено в этой статье, оператор присваивания вызывает внутренний метод
GetValue
. Данный метод в свою очередь “портит” значение типаReference
, получая истинное значение (саму функциюfoo.bar
).Потеря значения типа
Reference
приводит к глобальномуthis
в ES3 (в ES5 в strict-mode будетundefined
в качествеthis
).Обратите еще раз внимание, что просто оператор группировки (обрамляющие скобки), не вызывает
GetValue
, и таким образом вызов функции получаетthis
как объектfoo
:(foo.bar)();
.Во втором же случае у Вас просто два независимых действия. Первое никак не относится к определению
this
; Вы просто “переприсвоили” функции значение самой себя. А вот дальше уже идет активация функции, и именно в этот момент определяетсяthis
, как объектfoo
, поскольку слева от скобок вызова —Reference
, т.е. аккессор — доступ к свойству через точку.Да, есть такой вызов
eval
‘a. Подробней я его описывал в статье про strict-mode.Суть его опять же сводится к
Reference
типу. Проще — толькоeval
записанный в данной синтаксической форме, является явным (direct):Все остальные синтаксические формы — это уже косвенные (indirect) вызовы и исполняются в глобальном контексте. Причины этого связаны с особенностями реализации
eval
‘a и с безопасностью.Дмитрий, у Вас отличные и главное – интересные статьи! Спасибо за это! Объясните, пожалуйста, подробнее, какое же все-таки значение будет принимать ключевое слово this в тот момент, когда мы только входим в контекст исполнения. Предположим, у нас есть только глобальный контекст исполнения программы. У него определяется свойство this. Какое значение оно будет принимать до интерпретации программы? В Вашем примере использовалось thisValue. Что оно представляет из себя? Заранее огромное спасибо!
@Aleksey
Значение
this
всегда определяется до интерпретации программы (т.е., когда мы видим какой-то код и начинаем его исполнять — уже точно известно, что содержитthis
, и это значение неизменно на протяжении исполнения кода контекста).Да, верно, и это значение — сам глобальный объект, как было отмечено выше.
Это схематического обозначение значения
this
в определенном контексте исполнения.Структура контекста следующая (всего три основных свойства):
Если есть необходимость, я рекомендую начать с описания самого контекста исполнения, и дальше уже его компонентов: VO, ScopeChain и this. Или же прочитать обзорную лекцию JavaScript. Ядро, где все эти моменты так же затрагиваются.
Дмитрий, спасибо за Ваши пояснения! Продолжаю дальше читать Ваши статьи.
в примере “Вызов функции и НЕ тип Reference” команда
(false || foo.bar)();
возрашаетtrue
@max
Если вы говорите про этот пример:
То это выражение возвращает
undefined
. Тут основной смысл заключается в том какое значение присвоено ключевому словуthis
, и в данном, конкретном, случаеthis
ссылается на глобальный объект.Надеюсь я правильно понял ваш вопрос, и смог на него ответить.
Добрый день!
В FireFox (версии 16.0.2) код
пишет в консоль
У вас же в примере указано, что будет
"f"
:@Иван Иван
Я думою, дело в том, что при использовании консоли браузера, код будет выполнен с помощью функции eval, это и приведет к изменению значения this.
А что Вам консоль должна выдать?
Все правильно, Вы получили свой объект f, с методом b.
В случае, если !Reference, ответом был бы глобальный объект, т.к. это браузерная консоль, то Вы бы получили объект Window
Почему в данном коде
this
– global?Дмитрий подскажите пожалуйста, я правильно мыслю?
НО при выполнении
bar()
вернётся ссылка на функцию jFuncj : func...
Соответственно, когда:
мы попадаем под правило
т.к.
и
Всё верно ?
Если объяснять чуть проще, главное то, “как” мы вызываем функцию, в которой используем
this
.То есть не “откуда”, а именно “как”!
outerFunc();
-> Вызывается из AO родительской somePropFE, а доступа к AO в нормальных браузерах (текущий костяк) нет. Соответственноthis === AOsomePropFunctionExpression === global // или undefined в 'use strict';
Можно ещё так сказать:
[ТО ЧТО СТОИТ ЗДЕСЬ, ВЛИЯЕТ НА this ВНУТРИ ->] outerFunc [ЕСЛИ ДАЛЬШЕ ИДЁТ ВЫЗОВ ->] ()
@Alexander
Пример получился несколько запутанный, но в целом, да, все верно 😉
@Alexander
Не очень понял Ваш ответ по поводу моего примера выше.
Вообщем, как я вижу ситуацию:
Ф-ция outerFunc создаётся в глобальном контексте, то есть входит в AO глобального объекта.В теле свойства someProp она вызывается как обычная ф-ия, следовательно её промежуточный Reference объект в качестве базы имеет global, обращение к этой функции идёт через scope chain. Поэтому и получаем global в качестве this…
Надеюсь на Ваш ответ 🙂
Поясните, пожалуйста.
И вот:
Однако ниже говорится, что:
Следовательно немного не сходится пример:
Как base может быть AO(объектом активации), если в определении сказано, что base – родительский контекст исполнения, а VO/AO – является именно СВОЙСТВОМ контекста исполнения. (Взято из второго урока про контесты исполнения).
Понятно, что пример расскладывается так:
Однако следуя логике определий про то, что base является именно родитеським контекстом исполнения (как я понял, не свойство родитеского контекста исполнения VO/AO, а им самим), то должно быть так:
@Кирилл
Вероятно я несколько путано написал первое предложение. Сам контекст исполнения никогда не является значением свойства
base
объекта типаReference
. Соответственно, правильный вариант — первый:Если перефразировать проще первое предложение, то в нем говорится, что значение
this
не зависит от того, как функция была создана, а от того, как функция (и где) будет вызвана, т.е. от caller’a:Видим, что функция создана как метод объекта
foo
, но на определение значенияthis
это никак не повлияло, поскольку caller (глобальный контекст) определил его при вызове — передав глобальный объект в качествеthis
(Reference:{base: global, propertyName: 'bar'}
. В случае вызова из внутренней функции, base будет объект активации, и дальше также global.не подскажите а почему алерт выдает
undefined
, а вот здесь ожидаемоvar name = "";
@Dmitriy
Потому что контекст, в котором создается объект не равен самому объекту. Только внутри функции
this
будет объектом, но не в момент создания объекта. В момент созданияthis
равен глобальному объекту, у которого нет свойстваfirstName
.