Тонкости ECMA-262-3. Часть 3. This.
Read this article in: English, Chinese (version1, version2, version 3).
Введение
В данной небольшой заметке мы рассмотрим ещё одну сущность, связанную с контекстами исполнения. Речь пойдёт о ключевом слове 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;
Tags: ECMA-262-3, ECMAScript, this

14. September 2011 at 01:25
Большое спасибо за статью, очень актуальный вопрос.
30. January 2012 at 06:27
Дмитрий спасибо за замечательную статью!
Объясните пожалуйста чем отличается
(foo.bar = foo.bar)()от
foo.bar = foo.bar; foo.bar();И ещё вопрос о том же, я знаю что чтобы вызвать
eval(да и в принципе любую функцию) в глобальном контексте можно написать что то вроде(eval=eval)("/*global code*/");или(1,eval)("");Такая техника называется indirect call.
Могли бы вы пояснить логику выполнения этого выражения?
Спасибо.
30. January 2012 at 09:04
@ 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 и с безопасностью.13. February 2012 at 03:24
Дмитрий, у Вас отличные и главное – интересные статьи! Спасибо за это! Объясните, пожалуйста, подробнее, какое же все-таки значение будет принимать ключевое слово this в тот момент, когда мы только входим в контекст исполнения. Предположим, у нас есть только глобальный контекст исполнения программы. У него определяется свойство this. Какое значение оно будет принимать до интерпретации программы? В Вашем примере использовалось thisValue. Что оно представляет из себя? Заранее огромное спасибо!
13. February 2012 at 23:11
@Aleksey
Значение
thisвсегда определяется до интерпретации программы (т.е., когда мы видим какой-то код и начинаем его исполнять — уже точно известно, что содержитthis, и это значение неизменно на протяжении исполнения кода контекста).Да, верно, и это значение — сам глобальный объект, как было отмечено выше.
Это схематического обозначение значения
thisв определенном контексте исполнения.Структура контекста следующая (всего три основных свойства):
executionContext = { VO: { хранилище_переменных }, // variable object ScopeChain: VO + все родительские VO, this: значение this // thisValue };Если есть необходимость, я рекомендую начать с описания самого контекста исполнения, и дальше уже его компонентов: VO, ScopeChain и this. Или же прочитать обзорную лекцию JavaScript. Ядро, где все эти моменты так же затрагиваются.
16. February 2012 at 00:24
Дмитрий, спасибо за Ваши пояснения! Продолжаю дальше читать Ваши статьи.
10. May 2012 at 16:28
в примере “Вызов функции и НЕ тип Reference” команда
(false || foo.bar)();возрашаетtrue13. May 2012 at 00:41
@max
Если вы говорите про этот пример:
var foo = { bar: function () { alert(this); } }; (false || foo.bar)();То это выражение возвращает
undefined. Тут основной смысл заключается в том какое значение присвоено ключевому словуthis, и в данном, конкретном, случаеthisссылается на глобальный объект.Надеюсь я правильно понял ваш вопрос, и смог на него ответить.