Тонкости 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] — так же, взаимозаменяемы.

Сейчас мы подошли к ключевому моменту, касающемуся данной заметки. Обработка кода контекста исполнения делится на два основных этапа:

  1. Вход в контекст исполнения;
  2. Непосредственно, интерпретация кода.

С этими двумя этапами тесно связана модификация объекта переменных.

Обратите внимание, что обработка этих двух стадий является общей для всех контекстов.

При входе в контекст исполнения (но до построчного выполнения его кода), VO наполняется следующими свойствами (они уже были описаны в начале статьи):

  • для каждого формального параметра функции (если мы находимся в контексте исполнения функции)
  • – создаётся свойство VO с именем и значением формального параметра; для непереданных параметров – создаётся свойство VO с именем формального параметра и значением undefined;

  • для каждой декларации функции (FunctionDeclaration, FD)
  • – создаётся свойство VO, с именем функции и значением, являющимся ссылкой на объект-функцию; если в VO уже присутствовало свойство с таким именем, оно его значение и атрибуты заменяются значением функции;

  • для каждой переменной (var)
  • – создаётся свойство 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}.

Как уже отмечалось, по стандарту, получить прямой доступ к объекту активации – невозможно. Однако, в некоторых реализациях, а именно в 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).

Если у вас возникнут вопросы, я, с удовольствием, отвечу на них в комментариях.

Автор: Dmitry A. Soshnikov
Дата публикации: 27.06.2009


Tags: , , ,

 
 
 

Leave a Reply

Code: For code you can use tags [js], [text], [ruby] and other.

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>