Тонкости ECMA-262-3. Часть 5. Функции.
Read this article in: English, Chinese.
Введение
В этой заметке мы подробней поговорим об одном из основных видов объектов ECMAScript – о функциях. В частности, рассмотрим различные виды функций, определим, как тот или иной вид влияет на объект переменных контекста и, какое содержание имеет цепь областей видимости контекста, связанного с определённым видом функции. Ответим на часто задаваемые вопросы на форумах, вроде: “есть ли отличия (и, если есть, то в чём?), функций созданных следующим образом:
var foo = function () {
...
};
от функций, определённых в “привычном” виде”?:
function foo() {
...
}
Или, “почему при следующем вызове, функцию обязательно нужно оборачивать в скобки?”:
(function () {
...
})();
Поскольку, статьи данного цикла является зависимыми от более ранних статей, для полного понимания данной заметки (если, конечно, есть необходимость) желательно прочесть часть 2 (объект переменных) и часть 4 (цепь областей видимости), т.к. мы будем активно пользоваться терминологией из этих статей.
Но, давайте по порядку. Начнём мы с рассмотрения видов функций.
Виды функций
Всего в ECMAScript существует три вида функций, и каждый из них обладает своими особенностями.
Декларация функции (Function Declaration)
Декларация функции (Function Declaration, сокращённо FD) – это функция,
- обязательно имеющая имя;
- находящаяся в коде непосредственно: либо на уровне Программа (Program), либо внутри другой функции (FunctionBody);
- создаваемая на этапе входа в контекст;
- воздействующая на объект переменных;
- и объявленная в виде:
function exampleFunc() {
...
}
Основная особенность функций данного типа заключается в том, что только они воздействуют на объект переменных (т.е. попадают в VO контекста). Данное свойство определяет их вторую особенность (вытекающую из определения VO) – к моменту этапа интерпретации кода контекста, они уже доступны (т.к. попадают в VO ещё в при входе в контекст).
Пример (функция вызывается раньше, чем объявлена):
foo();
function foo() {
alert('foo');
}
Также важным моментом является второй пункт из определения – местоположение декларации функции в коде:
// декларация функции
// находится непосредственно:
// либо в глобальной области
// на уровне Программа
function globalFD() {
// либо внутри другой функции
function innerFD() {}
}
Ни в какой другой позиции в коде, декларация функции появиться не может – т.е. нельзя её объявить, например, в зоне выражения, блоке кода и т.д.
Альтернативой (и, даже можно сказать, противоположностью) декларациям функций, являются функции-выражения.
Функция-выражение (Function Expression)
Функция-выражение (Function Expression, сокращённо FE) – это функция,
- всегда находящаяся в зоне выражения;
- имеющая опциональное имя;
- не воздействующая на объект переменных;
- и создаваемая на этапе интерпретации кода контекста.
Зоной выражения обозначим любую часть программы, распознаваемую как выражение ECMAScript. Простейшим примером выражения, является выражение присваивания:
var foo = function () {
...
};
В данном случае представлена анонимная FE, которая присваивается переменной “foo”. Далее, функция доступна для вызова посредством переменной “foo” – foo().
Так же, как было отмечено в пунктах определения, FE может иметь и опциональное имя:
var foo = function _foo() {
...
};
Стоит отметить, что в данном частном случае, снаружи FE доступна всё так же посредством переменной “foo” – foo(), в то время как внутри функции (например, при рекурсивном вызове), возможно использование имени функции “_foo”.
При наличии имени, FE может быть сложно отличима от FD, однако, зная определение, это отличие становится явным и простым: FE всегда находится в зоне выражения. Ниже представлены различные выражения ECMAScript, где связанные с ними функции являются FE:
(function foo() {}); // в скобках (оператор группировки) может быть только выражение;
[function bar() {}]; // в инициализаторе массива - всегда выражение;
1, function alsoFE() {}; // "запятая" также оперирует выражениями;
Также в определении сказано, что FE создаются на этапе исполнения кода контекста и не попадают в объект переменных. Покажем данное поведение на примере:
// FE не доступна ни до объявления
// (т.к. создаётся на этапе выполнения кода контекста),
alert(foo); // "foo" is not defined
(function foo() {});
// ни после, т.к. её вообще нет в VO
alert(foo); // "foo" is not defined
Спрашивается, зачем они тогда вообще нужны? Ответ очевиден — чтобы использовать их в выражениях и не “загрязнять” объект переменных. Самый простой пример — передача функции в качестве параметра другой функции:
function foo(callback) {
callback();
}
foo(function bar() {
alert('foo.bar');
});
foo(function otherFE() {
alert('foo.otherFE');
});
В том случае, когда FE сохраняется ссылкой в переменную, она остаётся в памяти и доступна посредством этой переменной, поскольку переменные, как мы знаем, воздействуют на VO:
var foo = function () {
alert('foo');
};
foo();
Другим примером может является создание обособленной области видимости для скрытия от внешнего контекста вспомогательных данных (в примере ниже используется FE, которая запускается сразу же после создания):
var foo = {};
(function initialize() {
var x = 10;
foo.bar = function () {
alert(x);
};
})();
foo.bar(); // 10;
alert(x); // "x" is not defined
Видим, что функция foo.bar (благодаря свойству [[Scope]]) имеет доступ к внутренней переменной x функции initialize. И одновременно с этим, x недоступна снаружи. Данная стратегия применяется во многих библиотеках для создания “приватных” данных и сокрытия вспомогательных сущностей. Часто в данной конструкции, имя инициализирующей FE опускается:
(function () {
// инициализирующее пространство
})();
Ещё примеры FE, создаваемые по условию и не “загрязняющие” VO:
var foo = 10;
var bar = (foo % 2 == 0
? function () { alert(0); }
: function () { alert(1); }
);
bar(); // 0
Вопрос “о скобках”
Итак, ответим на вопрос, упоминавшийся в начале статьи – “зачем при вызове функции сразу же после её создания, нужно оборачивать её в скобки?”. Ответ на этот вопрос вытекает из ограничений на инструкцию-выражение.
Согласно стандарту, инструкция-выражение не может начинаться с открывающей фигурной скобки — {, т.к. тогда оно было бы неотличимо от блока, и, также, инструкция-выражение не может начинаться с ключевого слова function, т.к. тогда оно было бы неотличимо от декларации функции. Т.е., если мы опишем вызов функции сразу же после её создания следующим образом:
function foo() {
...
}();
парсер выдаст ошибку, т.к. не может определить, что перед ним находится — декларация функции, которую нужно создать ещё при входе в контекст, или же выражение, которое должно быть обработано при выполнении кода контекста, в “ран-тайме”? Соответственно, парсер честно “падает”, выдавая сообщение об ошибке.
Самым простым способом исправить эту ситуацию, является явное преобразование функции к FE, например, используя оператор группировки, внутри которого всегда будет находится выражение. Таким образом, парсер распознает код, как функцию-выражение (FE) и неоднозначности не возникнет.
Обратите внимание, в примере ниже, при вызове функции после её создания, обрамляющие скобки не требуются, т.к. функция уже находится в позиции выражения и парсеру сразу известно, что перед ним FE, которая будет создана в при интерпретации кода контекста:
var foo = {
bar: function (choose) {
return choose % 2 != 0 ? 'yes' : 'no';
}(1)
};
alert(foo.bar); // 'yes'
Как видим, foo.bar является строкой, а не функцией, как может показаться при беглом или невнимательном просмотре кода. Функция здесь используется лишь для инициализации этого свойства в зависимости от параметра — она создаётся и тут же вызывается.
Поэтому, окончательным ответом на вопрос “о скобках”, будет являться следующий: скобки нужны, когда функция находится не в позиции выражения, и мы хотим её вызовать сразу же после создания (в таком случае, мы принудительно преобразуем функцию к FE). В случае же, когда парсеру известно, что перед ним FE, скобки не обязательны.
Помимо обрамляющих скобок можно использовать любой другой способ преобразования функции к FE, например:
1, function () {
alert('anonymous function is called');
}();
но скобки являются в данном случае наиболее элегантным способом.
Кстати сказать, оператор группировки может обрамлять как описание функции без скобок вызова, так и, включая скобки, т.е. оба выражения, описанные ниже, являются правильными FE:
(function () {})();
(function () {}());
Расширение реализаций: Функция-инструкция (Function Statement)
Ниже представлен пример, который ни одна из реализаций (на текущий момент) не обрабатывает согласно стандарту:
if (true) {
function foo() {
alert(0);
}
} else {
function foo() {
alert(1);
}
}
foo(); // 1 или 0 ? потестируйте в разных реализациях
Здесь стоит сказать, что, согласно стандарту, данная синтаксическая конструкция вообще ошибочна, поскольку, как мы помним, декларация функции (FD) не может находится в блоке кода (здесь if и else содержат блоки кода). Как было сказано, FD может находится только в двух местах: на уровне Программа и в теле другой функции.
Ошибочна потому, что блок кода может содержать только инструкции (statements). И единственное место, в котором функция может появится в блоке — это одна из таких инструкций — уже рассмотренная выше инструкция-выражение (expression statement). Но она, по определению и, как опять же мы уже отмечали выше, не может начинаться с открывающей фигурной скобки (т.к. неотличимо от блока) и ключевого слова “function” (т.к. неотличимо от FD).
Однако в разделе обработки ошибок стандарт, одним из пунктов, определяет для реализаций возможность расширения синтаксиса программы. И именно одно из таких расширений мы видим в случае объявления функций внутри блоков. Все существующие на сегодня движки не выбрасывают исключение в этом случае, а обрабатывают данную конструкцию. Но, каждый по-своему.
Наличие ветвей if-else подразумевает динамику, выбор, т.е. напрашивается функция-выражение (FE), которая будет создана динамически. Однако большинство реализаций просто создают здесь декларацию функции (FD) ещё на этапе входа в контекст, причём берут последнюю объявленную функцию. Т.е. функция “foo” будет выводить 1, даже несмотря на то, что ветвь else никогда не выполнится.
Однако, всё же, реализация SpiderMonkey (и TraceMonkey) в этой ситуации поступает двояко: с одной стороны она не считает такие функции декларациями (т.е. функция будет создана по условию, в ран-тайме), но и с другой стороны — это не полноценные функции-выражения, т.к. их нельзя вызвать без обрамляющих скобок (снова ошибка парсера – “неотличимо от FD”) и они попадают в объект переменных.
Моё мнение, что SpiderMonkey поступает в данном случае правильно, выделяя негласный средний вид функции — (FE + FD). Данные функции правильно будут созданы в нужное время и исходя из условий, а также, в отличие от FE — будут доступны для вызова снаружи. Данное синтаксическое расширение SpiderMonkey называет инструкцией функции (Function Statement, сокращённо FS); эта терминология упоминается на MDC. Создатель JavaScript Brendan Eich также отмечал данный вид функций, присутствующий в реализации SpiderMonkey.
Особенность именованной Function Expression (NFE)
В том случае, когда FE имеет имя (named function expression, сокращённо NFE), в силу вступает одна важная особенность. Как нам известно из определения, (и, как мы видели из вышеописанных примеров), функции-выражения не воздействуют на объект переменных контекста (что означает невозможность обратиться к ним по имени ни до, ни после объявления). Однако, FE имеет возможность обращаться к себе по имени при рекурсивном вызове:
(function foo(bar) {
if (bar) {
return;
}
foo(true); // имя "foo" доступно
})();
// а снаружи, правильно, недоступно
foo(); // "foo" is not defined
Где же хранится имя “foo”? В объекте переменных самой foo? Нет, т.к. никто не определял никакого имени “foo” внутри foo. В родительском объекте переменных контекста, породившего foo? Тоже нет, т.к. это следует из определения — FE не воздействуют на VO — что мы и видим при вызове за пределами foo. Где же тогда?
А дело вот в чём. Когда интерпретатор, на стадии исполнения кода контекста, встречает именованную FE, он, перед созданием самой FE, создаёт вспомогательный спецобъект и добавляет его спереди к текущей цепи областей видимости. Далее создаётся сама FE, и в этот момент (как нам известно из статьи о Scope chain) в неё свойством записывается [[Scope]] — цепь областей видимости контекста, породившего функцию (т.е. в [[Scope]] присутствует этот спецобъект). Далее, в спецобъект добавляется одно единственное свойство — имя FE; значением свойства является ссылка на эту FE. И последним действием, спецобъект удаляется из Scope chain порождающего функцию контекста. Отобразить псевдокодом можно так:
__specialObject = {};
Scope = __specialObject + Scope;
foo = FunctionExpression;
foo.[[Scope]] = Scope;
__specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // удалить __specialObject из начала Scope
Таким образом, за пределами функции данное имя недоступно (т.к. его нет в Scope), но спецобъект успел записаться в [[Scope]] функции, и там это имя определено.
Стоит однако отметить, что не всё так гладко в этом моменте с реализациями. Некоторые из них, например, Rhino, записывают это опциональное имя не в спецобъект, а в сам объект активации FE. Реализация же от Microsoft — JScript, полностью нарушая правила FE, вообще сохраняет это имя во внешнем объекте переменных, и оно становится доступно снаружи.
NFE и SpiderMonkey
Касательно реализаций, всё в той же SpiderMonkey, есть одна особенность, связанная с этим спецобъектом, которую, в принципе, можно считать багом (хотя реализовано всё согласно стандарту, так что это — больше недочёт спецификации). Связана она с механизмом разрешения имён идентификаторов: последовательный опрос Scope chain является двумерным и при разрешении имени идентификатора, учитывает также и прототипы объектов цепи.
Наблюдать данный механизм в действии можно, если определить свойство в Object.prototype и обратиться к “несуществующей” переменной из функции, определённой в глобальном контексте. Таким образом, при разрешении имени “x” в примере ниже, мы дойдём до глобального объекта, но и там имя “x” не будет найдено. Однако в SpiderMonkey глобальный объект наследует от Object.prototype и, соответственно, имя “x” будет найдено в нём:
Object.prototype.x = 10;
(function () {
alert(x); // 10
})();
Объекты активации прототипов не имеют. При тех же начальных условиях, данное поведение можно видеть на примере вложенной функции. Если определить локальную переменную “x” и объявить вложенную функцию (FD или анонимную FE), то при обращении из вложенной функции к внешней переменной “x”, значение её будет найдено из функции выше (т.е. там, где она и находится), а не из Object.prototype:
Object.prototype.x = 10;
function foo() {
var x = 20;
// декларация функции
function bar() {
alert(x);
}
bar(); // 20, из AO(foo)
// аналогично при анонимной FE
(function () {
alert(x); // 20, также из AO(foo)
})();
}
foo();
Некоторых реализации, являясь исключением, всё же задают прототип объектам активации. Так, например, в реализации Blackberry значение “x” из примера выше будет определено как 10. Т.е. до объекта активации foo мы не дойдём, т.к. значение будет найдено в Object.prototype:
AO(bar или анонимной функции) -> нет -> AO(bar или анонимной функции).[[Prototype]] -> да - 10
И абсолютно аналогичную ситуацию можно наблюдать в SpiderMonkey со спецобъектом именованной FE. Данный спецобъект является (по стандарту) обычным объектом — “как если бы new Object()”, и соответственно, должен наследоваться от Object.prototype, что мы и видим в реализации SpiderMonkey. Остальные реализации (включая новый TraceMonkey) не задают прототипа спецобъекту:
function foo() {
var x = 10;
(function bar() {
alert(x); // 20, а не 10, т.к. до AO(foo) не доходит
// "x" найдено по цепи:
// AO(bar) - нет -> __specialObject(bar) -> нет
// __specialObject(bar).[[Prototype]] - да: 20
})();
}
Object.prototype.x = 20;
foo();
NFE и JScript
Реализация ECMAScript от Microsoft – JScript, встроенная в Internet Explorer, на текущий момент (вплоть до версии JScript 5.8 – IE8) имеет ряд багов, связанных с именованными функциями-выражениями (NFE). Каждый из этих багов полностью расходится со стандартом ECMA-262-3; некоторые из них чреваты серьёзными ошибками.
Во-первых, JScript в этом моменте нарушает главную особенность FE – то, что они не должны попадать в объект переменных по имени функции. Опциональное имя FE, которое должно записываться в спецобъект, доступный лишь при активации функции (и нигде более), здесь записывается прямо во внешний объект переменных. Более того, именованная FE трактуется в JScript, как декларация функции (FD), т.е. создаётся при входе в контекст и доступна до объявления:
// FE доступна в объекте переменных
// по опциональному имени и
// до объявления, - как FD
testNFE();
(function testNFE() {
alert('testNFE');
});
// а также - после
// объявления, как FD; опциональное
// имя осталось висеть в объекте переменных
testNFE();
В общем, полное нарушение правил.
Во-вторых, в случае сохранения ссылки на именованную FE в переменную сразу при описании переменной и операции присваивания, JScript создаёт два разных объекта-функции. Данное поведение сложно назвать логичным (особенно, учитывая то, что за пределами самой NFE, её имя вообще не должно быть доступно):
var foo = function bar() {
alert('foo');
};
alert(typeof bar); // "function", NFE вновь осталась в VO - уже ошибка
// но, дальше - больше
alert(foo === bar); // false!
foo.x = 10;
alert(bar.x); // undefined
// однако, два объекта выполняют
// одинаковые действия
foo(); // "foo"
bar(); // "foo"
Вновь – полный беспорядок.
Однако стоит отметить, что, если описать NFE отдельно от присваивания переменой (например, при помощи оператора группировки), и только после этого присвоить ссылку на неё в переменную, то равенство объекта по имени переменной и объекта по имени функции будет давать положительный результат, как будто использован один объект:
(function bar() {});
var foo = bar;
alert(foo === bar); // true
foo.x = 10;
alert(bar.x); // 10
Этот момент объясним. На самом деле, создаётся так же два объекта, но в дальнейшем остаётся, действительно, только один. Если опять учесть, что NFE здесь трактуется, как декларация функции (FD), то ещё на этапе входа в контекст создастся FD bar. Далее уже на этапе интерпретации кода создаётся второй объект — функция-выражение (FE) bar, которая нигде не сохраняется. Соответственно, поскольку ссылок на bar нет, она тут же удаляется. Таким образом остаётся только один объект — FD bar, ссылка на который и присваивается переменной foo.
В-третьих, касаемо косвенной ссылки функции на саму себя — посредством arguments.callee, опять же, в случае описания NFE с сохранением ссылки на неё в переменную по месту (т.е. использованием двух объектов), arguments.callee будет указывать на тот объект, чьим именем была активирована функция (а точнее — функции, т.к. их две):
var foo = function bar() {
alert([
arguments.callee === foo,
arguments.callee === bar
]);
};
foo(); // [true, false]
bar(); // [false, true]
В-четвёртых, поскольку JScript трактует NFE как обычную FD, то на неё не действуют правила условных операторов (т.е. как и FD, NFE будет создана ещё при входе в контекст, причём будет взято последнее описание в коде):
var foo = function bar() {
alert(1);
};
if (false) {
foo = function bar() {
alert(2);
};
}
bar(); // 2
foo(); // 1
Данное поведение также можно “логично” разобрать. При входе в контекст была создана последняя встретившаяся “FD” для имени bar, т.е. функция с alert(2);. Далее, при интерпретации кода, создаётся уже новая функция — FE bar, ссылка на которую и присваивается переменной foo. Таким образом (поскольку дальше по коду if-блок с условием false — недостижим), активация foo выводит alert(1);. Логика ясна, но с учётом багов IE, я взял слово “логично” в кавычки, т.к. подобная реализация явно сбитая и завязана на баги JScript.
Ну и пятый баг NFE в JScript связан с инициализацией свойств глобального объекта посредством обычного присвоения имени значения. Т.к. NFE трактуется здесь как FD и, соответственно, попадает в объект переменных, присвоение ссылки свойству без var (т.е. не переменной, а обычному свойству глобального объекта), при условии, что имя самой функции совпадает с именем свойства, данное свойство не становится глобальным.
(function () {
// без var должна быть не переменная в локальном
// контексте, а свойство глобального объекта
foo = function foo() {};
})();
// однако, за пределами контекста
// анонимной функции, имя foo
// не существует
alert(typeof foo); // undefined
Опять же, “логика” понятна: декларация функции foo попадает в объект активации локального контекста анонимной функции ещё при входе в контекст. И к моменту интерпретации кода контекста, имя foo уже содержится в AO, т.е. считается локальным. Соответственно, при операции присваивания просто обновляется, уже существующее в AO свойство “foo”, но не создаётся новое свойство глобального объекта, как должно быть по логике вещей ECMA-262-3.
Функции, созданные конструктором Function
Данный тип объектов-функций обособлен от FD и FE, т.к. также имеет свои особенности. Основная особенность – это то, что данные функции в качестве [[Scope]] имеют лишь глобальный объект:
var x = 10;
function foo() {
var x = 20;
var y = 30;
var bar = new Function('alert(x); alert(y);');
bar(); // 10, "y" is not defined
}
Т.е. видим, что в [[Scope]] функции “bar” отсутствует AO контекста функции “foo” (не доступна переменная “y”, а переменная “x” берётся из глобального контекста). Кстати, обратите внимание, конструктор Function можно вызывать и с new и без, в данном случае эти записи будут эквивалентны.
Следующая особенность данных функций связана с тождественными правилами грамматики (Equated Grammar Productions) и объединёнными объектами (Joined Objects). Данный механизм введён спецификацией, как предложение по оптимизации (однако, реализации вправе не осуществлять данную оптимизацию). К примеру, если у нас есть массив из ста элементов, который заполняется в цикле функциями, реализация вправе задействовать механизм объединённых объектов. В итоге, может быть создана лишь одна функция для всех элементов массива:
var a = [];
for (var k = 0; k < 100; k++) {
a[k] = function () {}; // возможно, объединённые объекты
}
Но, функции, порождённые конструктором Function, никогда не объединяются:
var a = [];
for (var k = 0; k < 100; k++) {
a[k] = Function(''); // всегда 100 разных функций
}
Ещё пример, касающийся объединения:
function foo() {
function bar(z) {
return z * z;
}
return bar;
}
var x = foo();
var y = foo();
Здесь также, реализация вправе объединить объекты x и y (и использовать один объект), поскольку функции физически (включая их внутреннее свойство [[Scope]]) не различимы. Поэтому, функции, порождённые с помощью конструктора Function всегда более затратны по ресурсам.
Алгоритм создания функций
Ниже представлен псевдокод алгоритма создания объектов-функций (исключая пункты с объединёнными объектами), который поможет более наглядно представить, что из себя представляют функции в ECMAScript. Данный алгоритм справедлив для всех видов функций.
F = new NativeObject();
// свойство [[Class]] всегда "Function"
F.[[Class]] = "Function"
// прототип функции
F.[[Prototype]] = Function.prototype
// объект вызова, именно он активируется
// и создаёт контекст посредством выражения вызова F()
F.[[Call]] = callObject
// встроенный конструктор всех
// объектов функций, далее, этот
// встроенный конструктор вызывает F.[[Call]],
// инициализируя созданный объект
F.[[Construct]] = internalConstructor
// цепь областей видимости
// порождающего контекста
F.[[Scope]] = activeContext.Scope
// если функция была создана
// через new Function(...), то
F.[[Scope]] = globalContext.Scope
// количество описанных формальных параметров
F.length = countParameters
// прототип порождаемых от F объектов
__objectPrototype = new Object();
__objectPrototype.constructor = F // {DontEnum}, не выводится в цикле
F.prototype = __objectPrototype
return F
Обратите внимание, F.[[Prototype]] – это прототип функции (конструктора), а F.prototype – это прототип порождаемых от функции объектов (просто часто бывает путаница в терминологии, и F.prototype называют “прототипом конструктора”, что неверно).
Заключение
Данная статья получилась немаленькой, однако, мы ещё затронем функции, когда будем разбирать их работу в качестве конструкторов в одной из следующих заметок об объектах и прототипах. Как всегда, я готов, и буду рад, ответить на ваши вопросы в комментариях.
Дополнительная литература
Спецификация ECMAScript:
- 13 – Определение функции;
- 15.3 – Объекты типа Function.
Другие статьи:
Автор: Dmitry A. Soshnikov.
Дата публикации: 08.07.2009
Tags: ECMA-262-3, ECMAScript, Function Declaration, Function Expression, Декларация функции, Функция-выражение

24. March 2010 at 00:36
Очень интересная статья, только я хотел бы уточнить, в разделе “NFE и SpiderMonkey” в самом первом примере в прототип Object добавляем св-во x, после этого создаем не именованную FE, исходя из текста alert не должен показать 10, т.к. спец. объект для FE не будет создан, потому что как я уже писал FE не именованная. Это так?
24. March 2010 at 11:19
@Anton
Да, но не забывай, что в *Mokey реализациях и глобальный объект тоже наследует (в одном из звеньев цепи прототипов) от Object.prototype.
Поэтому первый пример относился к глобальному объекту, а не спец. объекту именованной FE — чтобы показать, что анализ scope chain – двумерный: (1) по звеньям scope chain, (2) и на каждом из звеньев scope chain — вглубь по звеньям prototype chain. И аналогичная ситуация дальше разбирается на примере со спец. объектом NFE. Изменил немного описание, чтобы неоднозначности не возникало.