Тонкости ECMA-262-3. Часть 2. Объект переменных.
Read this article in: English, Chinese (version1, version2).
Введение
Постоянно в программах мы описываем функции и переменные, которые затем благополучно используем (вызываем, обращаемся) при построении наших систем. Однако, как и где интерпретатор находит наши данные (функции, переменные)? Что происходит, когда мы обращаемся к нужным нам объектам?
Многие знают, что переменные тесно связаны со своим контекстом исполнения:
var a = 10; // переменная глобального контекста
(function () {
var b = 20; // локальная переменная контекста функции
})();
alert(a); // 10
alert(b); // b is not defined
Также, многим известно, что обособленную область видимости в текущей спецификации, создают лишь контексты исполнения типа “Функция”. Т.е., в отличии от C/C++, к примеру, цикл for в ECMAScript не создаёт локальный контекст:
for (var k in {a: 1, b: 2}) {
alert(k);
}
alert(k); // переменная "k" осталась жить и после цикла
Посмотрим более детально, что происходит, когда мы объявляем наши данные.
Объявление данных
Итак, коль скоро, переменные связаны с контекстом, последний должен знать, где хранятся его данные и как их получить. Данный механизм называется Объектом переменных.
Объект переменных (Variable object, сокращённо VO) – это связанный с контекстом исполнения объект, служащий хранилищем для:
- переменных (var);
- деклараций функций (FunctionDeclaration, сокращённо FD);
- и формальных параметров функции,
объявленных в данном контексте.
Схематично и для примеров, объект переменных можно представить в виде обычного JS-объекта:
VO = {};
И, как мы отметили, VO является свойством контекста исполнения:
activeExecutionContext = {
VO: {
// данные контекста (var, FD, параметры функций)
}
};
Косвенное обращение к переменным (по именам свойств VO) позволяет лишь VO глобального контекста (где сам глобальный объект и является объектом переменных, но об этом чуть позже). Для остальных контекстов напрямую обратиться к VO невозможно; это чисто механизм реализации.
Когда мы объявляем переменную или декларируем функцию, происходит ничто иное, как создание свойства с именем и значением нашей переменной в VO контекста.
Пример:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
Имеем:
// Объект переменных глобального контекста
VO(globalContext) = {
a: 10,
test: <ссылка на функцию>
};
// Объект переменных контекста функции test
VO(test functionContext) = {
x: 30,
b: 20
};
Но, и на уровне реализации (да и спецификации) объект переменных является абстрактной сущностью. Физически же, в конкретных контекстах исполнения, VO называется по-разному и имеет разную инициализирующую структуру.
Конкретизация объекта переменных
Некоторое базовые операции (например, создание переменных — variable instantiation) и поведение объекта переменных являются общими для всех контекстов исполнения. Контекст функции может также определять дополнительные сущности, связанные с VO.
AbstractVO (общее поведение процесса создания переменных)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, добавлены: объект <arguments> и <формальные параметры>)
Рассмотрим эти случаи подробней.
Объект переменных в глобальном контексте
Здесь, для начала, нужно дать определение Глобального объекта.
Глобальный объект (Global object) – объект, который создаётся до входа в любой из контекстов исполнения; данный объект существует в единственном экземпляре, свойства его доступны из любого места программы, жизненный цикл объекта завершается с завершением программы.
При создании, глобальный объект инициализируется такими свойствами, как Math, String, Date, parseInt и т.д., а также, дополнительными объектами, среди которых может быть и ссылка на сам глобальный объект – например, в DOM, свойство window глобального объекта ссылается на сам глобальный объект (однако, не во всех реализациях):
global = {
Math: <...>,
String: <...>
...
...
window: global
};
При обращении к свойствам глобального объекта префикс обычно не используется, поскольку сам глобальный объект не доступен напрямую по имени. Однако, получить доступ к нему можно посредством значения this в глобальном контексте, а также через свойства-ссылки на самого себя, например window в DOM, поэтому пишется просто:
String(10); // подразумевается global.String(10); // с префиксами window.a = 10; // === global.window.a = 10 === global.a = 10; this.b = 20; // global.b = 20;
Так вот, возвращаясь к объекту переменных глобального контекста. В глобальном контексте, объектом переменных является сам глобальный объект:
VO(globalContext) === global;
Необходимо чётко осознавать этот факт, т.к. именно поэтому, объявляя переменную в глобальном контексте, мы имеем возможность косвенно обратиться к ней посредством свойства глобального объекта (например, когда имя переменной заранее неизвестно):
var a = new String('test');
alert(a); // напрямую, будет найдено в VO(globalContext): "test"
alert(window['a']); // косвенно через global === VO(globalContext): "test"
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // косвенно, имя свойства сформировано налету: "test"
Объект переменных в контексте функции
Касательно же контекста исполнения типа “Функция”, – здесь VO недоступен напрямую, и его функцию исполняет, так называемый, Объект активации (Activation object, сокращённо AO).
VO(functionContext) === AO;
Объект активации создаётся при входе в контекст функции и инициализируется свойством arguments, значением которого является Объект аргументов (Arguments object, обозначим ArgO, чтобы не путать с AO):
AO = {
arguments: <ArgO>
};
Объект аргументов (Arguments object, сокращённо ArgO) – объект, находящийся в объекте активации контекста функции и содержащий следующие свойства:
- callee – ссылка на выполняемую функцию;
- length – количество реально переданных параметров;
- свойства-индексы (числовые, приведённые к строке), значения которых – есть формальные параметры функции (слева направо в списке параметров). Количество этих свойств-индексов == arguments.length. Значения свойств-индексов объекта arguments и присутствующие формальные параметры – взаимозаменяемы:
Пример:
function foo(x, y, z) {
alert(arguments.length); // 2 - количество реально переданных параметров
alert(arguments.callee === foo); // true
alert(x === arguments[0]); // true
alert(x); // 10
arguments[0] = 20;
alert(x); // 20
x = 30;
alert(arguments[0]); // 30
// однако, для не переданного параметра z,
// соответствующее свойство-индекс объекта
// arguments - не взаимозаменяемое
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
Относительно последнего случая, в текущей версии Google Chrome есть баг — там параметр z и arguments[2] — так же, взаимозаменяемы.
Детализация обработки кода контекста
Сейчас мы подошли к ключевому моменту, касающемуся данной заметки. Обработка кода контекста исполнения делится на два основных этапа:
- Вход в контекст исполнения;
- Непосредственно, интерпретация кода.
С этими двумя этапами тесно связана модификация объекта переменных.
Обратите внимание, что обработка этих двух стадий является общей для всех контекстов.
Вход в контекст исполнения
При входе в контекст исполнения (но до построчного выполнения его кода), VO наполняется следующими свойствами (они уже были описаны в начале статьи):
- для каждого формального параметра функции (если мы находимся в контексте исполнения функции)
- для каждой декларации функции (FunctionDeclaration, FD)
- для каждой переменной (var)
– создаётся свойство VO с именем и значением формального параметра; для непереданных параметров – создаётся свойство VO с именем формального параметра и значением undefined;
– создаётся свойство VO, с именем функции и значением, являющимся ссылкой на объект-функцию; если в VO уже присутствовало свойство с таким именем, оно его значение и атрибуты заменяются значением функции;
– создаётся свойство VO с именем переменной, и значением undefined; если в VO уже присутствовало свойство с таким именем, оно остаётся нетронутым.
Разберём на примере:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // вызов
При входе в контекст функции “test” с переданным параметром 10, AO будет иметь следующий вид:
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <ссылка на FunctionDeclaration "d">
e: undefined
};
Обратите внимание, что в AO не попала функция “x”. Это потому, что “x” является не декларацией функции, а функцией-выражением (FunctionExpression, сокращённо FE), которые не воздействуют на VO. Однако, функция “_e” также является функцией-выражением, но, как мы увидим ниже, за счёт присваивания ссылки на неё переменной “e”, она становится доступна посредством “e”. О разнице FunctionDeclaration от FunctionExpression можно почитать в соответствующей заметке.
А далее наступает вторая фаза обработки кода – построчное выполнение.
Интерпретация кода
К этому моменту, AO/VO уже наполнен свойствами (хотя, и не все из них ещё имеют истинные значения, описанные нами в коде, пока, большинство из них инициализированно значением undefined).
Рассматривая всё тот же пример, AO/VO, по мере интерпретации кода, модифицируется следующим образом:
AO['c'] = 10; AO['e'] = <ссылка на FunctionExpression "_e">;
Ещё раз отмечу, что FunctionExpression “_e” осталась в памяти лишь за счёт переменной “e”. FunctionExpression “x” — не попала в AO/VO: т.е., если в коде попытаться вызвать функцию “x” до или после объявления – будет ошибка “x is not defined”. Несохранённую FunctionExpression, можно вызвать лишь вместе с объявлением, либо рекурсивно.
Ещё (классический) пример:
alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20
Почему в первом выводе “x” – функция, да ещё и доступна до объявления? Почему не 10 и не 20? Потому что, согласно правилу – VO наполняется декларациями функций ещё при входе в контекст, там же, при входе, встречается объявление переменной “x”, но переменные в VO имеют более низкий приоритет, нежели декларации функций, поэтому, при входе, заполнение VO произойдёт следующим образом:
VO = {};
VO['x'] = <ссылка на FunctionDeclaration "x">
// найдена var x = 10;
// если бы до этого не была объявлена функция
// с таким же именем, x стало бы undefined, в нашем же
// же случае переменная не затирает значение функции
VO['x'] = <значение осталось прежним - функция>
А вот уже при выполнении кода, VO модифицируется так:
VO['x'] = 10; VO['x'] = 20;
о чём явно свидетельствуют второй и третий выводы.
В примере ниже, мы снова видим, что переменные попадают в VO ещё при входе в контекст (так, блок else никогда не выполнится, но, тем не менее, переменная “b” существует в VO):
if (true) {
var a = 1;
} else {
var b = 2;
}
alert(a); // 1
alert(b); // undefined, но не "b is not defined"
О переменных
Часто, в различных статьях о JavaScript, можно видеть утверждения вроде: “глобальные переменные можно объявлять и с var (в глобальном контексте) и без var (в любом месте)”. Это не так. Запомните:
переменные объявляются только с ключевым словом var.
Присвоение же вроде:
a = 10;
лишь создаёт очередное свойство (но не переменную) в глобальном объекте. “Не переменную” не в том смысле, что её нельзя изменить, а “не переменную” в понятии переменных в ECMAScript (которые затем также станут свойствами глобально объекта посредством VO(globalContext) === global, помним, да?).
А разница следующая (покажем на примере):
alert(a); // undefined alert(b); // "b" is not defined b = 10; var a = 20;
Всё, опять же, вытекает из VO и стадий его модификации (вход в контекст, исполнение контекста):
Вход:
VO = {
a: undefined
};
Видим, что никакого “b” ещё нет, т.к. это не переменная, “b” появится лишь при исполнении кода (правда, в нашем случае не появится, т.к. будет ошибка). Изменим код:
alert(a); // undefined, понятно почему b = 10; alert(b); // 10, создалось при исполнении var a = 20; alert(a); // 20, модифицировалось при исполнении
Ещё один важный момент относительно переменных. Переменные, в отличии от простых свойств, получают атрибут {DontDelete}, означающий невозможность удалить свойство посредством оператора delete:
a = 10; alert(window.a); // 10 alert(delete a); // true alert(window.a); // undefined var b = 20; alert(window.b); // 20 alert(delete b); // false alert(window.b); // по-прежнему, 20
Но, есть один контекст исполнения, на который это правило не действует, это – контекст eval: здесь {DontDelete} var-ам не выставляется:
eval('var a = 10;');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
Напоминание для тех, кто тестирует эти примеры в консоли отладчика (например, в Firebug): учитывайте, что Firebug также использует eval для запуска вашего кода. И поэтому там var-ы тоже не имеют {DontDelete}.
Особенность реализаций: свойство __parent__
Как уже отмечалось, по стандарту, получить прямой доступ к объекту активации – невозможно. Однако, в некоторых реализациях, а именно в SpiderMonkey и Rhino, функциям доступно свойство __parent__, являющееся объектом активации (либо, объектом переменных), в котором данные функции были порождены.
Пример (SpiderMonkey, Rhino):
var global = this;
var a = 10;
function foo() {}
alert(foo.__parent__); // global
var VO = foo.__parent__;
alert(VO.a); // 10
alert(VO === global); // true
В примере выше видно, что функция foo порождена в глобальном контексте, и, соответственно, её свойство __parent__ указывает на объект переменных глобального контекста, т.е. на сам глобальный объект.
Однако, получить объект активации в SpiderMonkey таким образом уже не удастся: в зависимости от версии, __parent__ для внутренней функции будет возвращать либо null, либо глобальный объект.
В Rhino же, доступ к объекту активации открыт и осуществляется аналогичным способом:
Пример (Rhino):
var global = this;
var x = 10;
(function foo() {
var y = 20;
// объект активации foo
var AO = (function () {}).__parent__;
alert(AO.y); // 20
// __parent__ текущего объекта
// активации - уже глобальный объект,
// т.е. образуется цепь объектов переменных
alert(AO.__parent__ === global); // true
alert(AO.__parent__.x); // 10
})();
Заключение
В этой статье мы ещё дальше продвинулись в изучении объектов, связанными с контекстами исполнения. Надеюсь, материал будет полезен, и прояснит некоторые аспекты и неясности, которые у вас, возможно, были. Далее по плану: цепь областей видимости (Scope chain), разрешение имён идентификаторов (Identifier resolution) и, как следствие, замыкания (Closures).
Если у вас возникнут вопросы, я, с удовольствием, отвечу на них в комментариях.
Дополнительная литература
- 10.1.3 – Инстанциация переменных;
- 10.1.5 – Глобальный объект;
- 10.1.6 – Объект активации;
- 10.1.8 – Объект аргументов.
Автор: Dmitry A. Soshnikov
Дата публикации: 27.06.2009
Tags: ECMA-262-3, ECMAScript, Variable object, Объект переменных
