Read this article in: English, Chinese (version 1, version 2), French.
Введение
В этой заметке мы подробней поговорим об одном из основных видов объектов ECMAScript — о функциях. В частности, рассмотрим различные виды функций, определим, как тот или иной вид влияет на объект переменных контекста и, какое содержание имеет цепь областей видимости контекста, связанного с определённым видом функции. Ответим на часто задаваемые вопросы на форумах, вроде: “есть ли отличия (и, если есть, то в чём?), функций созданных следующим образом:
var foo = function () { ... };
от функций, определённых в “привычном” виде”?:
function foo() { ... }
Или, “почему при следующем вызове, функцию обязательно нужно оборачивать в скобки?”:
(function () { ... })();
Поскольку статьи данного цикла является зависимыми от более ранних статей, для полного понимания данной заметки желательно прочесть часть 2 (объект переменных) и часть 4 (цепь областей видимости), т.к. мы будем активно пользоваться терминологией из этих статей.
Но, давайте по порядку. Начнём мы с рассмотрения видов функций.
Виды функций
Всего в ECMAScript существует три вида функций, и каждый из них обладает своими особенностями.
Декларация функции (Function Declaration)
- обязательно имеющая имя;
- находящаяся в коде непосредственно: либо на уровне Программа (Program), либо внутри другой функции (FunctionBody);
- создаваемая на этапе входа в контекст;
- воздействующая на объект переменных;
- и объявленная в виде:
function exampleFunc() { ... }
Основная особенность функций данного типа заключается в том, что только они воздействуют на объект переменных (т.е. попадают в VO контекста). Данное свойство определяет их вторую особенность (вытекающую из определения VO) – к моменту этапа интерпретации кода контекста, они уже доступны (т.к. попадают в VO ещё в при входе в контекст).
Пример (функция вызывается раньше, чем объявлена):
foo(); function foo() { alert('foo'); }
Также важным моментом является второй пункт из определения – местоположение декларации функции в коде:
// декларация функции // находится непосредственно: // либо в глобальной области // на уровне Программа function globalFD() { // либо внутри другой функции function innerFD() {} }
Ни в какой другой позиции в коде, декларация функции появиться не может – т.е. нельзя её объявить, например, в зоне выражения, блоке кода и т.д.
Альтернативой (и, даже можно сказать, противоположностью) декларациям функций, являются функции-выражения.
Функция-выражение (Function Expression)
- всегда находящаяся в зоне выражения;
- имеющая опциональное имя;
- не воздействующая на объект переменных;
- и создаваемая на этапе интерпретации кода контекста.
Зоной выражения обозначим любую часть программы, распознаваемую как выражение 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
):
function () { ... }(); // или с именем function foo() { ... }();
то мы имеем дело с декларациями функций, и в обоих случаях парсер выдаст синтаксическую ошибку. Однако причины этих двух синтаксических ошибок различны.
Если мы имеем такое объявление в глобальном коде (т.е. на уровне Программа
), парсер должен распознавать такую функцию как декларацию, т.к. она начинается с ключевого слова function
. Поэтому в первом случае мы получаем SyntaxError
из-за отсутствия имени функции (функция-декларация всегда должна иметь имя).
Во втором случае имя задано (foo
), и по идее, декларация функции должна пройти нормально. Однако, мы всё равно имеем ошибку синтаксиса, но уже, касаемо оператора группировки без выражения внутри. Обратите внимание, в данном случае — это именно оператор группировки, который следует за декларацией функции, а не скобки вызова функции! Т.е., если бы мы имели следующий код:
// "foo" - функция-декларация, // которая создана при входе в контекст alert(foo); // function function foo(x) { alert(x); }(1); // а это оператор группировки, но не вызов! foo(10); // а это уже вызов, 10
то все, как мы видим, было бы в порядке, поскольку мы имеем здесь две синтаксические конструкции: декларация функции и оператор группировки с выражением внутри (1
). Пример выше можно расписать следующим образом:
// декларация функции function foo(x) { alert(x); } // оператор группировки // с выражением (1); // еще один оператор группировки // с другим (функциональным) выражением (function () {}); // также - внутри выражение ("foo"); // etc
В случае, если бы мы имели подобное определение внутри инструкции (statement), то, как мы сказали, была бы неоднозначность с декларацией функции, и снова должна была быть синтаксическая ошибка:
if (true) function foo() {alert(1)}
Конструкция выше по спецификации синтаксически неверна (инструкция-выражение не может начинаться с ключевого слова function
), однако, как мы увидим ниже, ни одна из текущих реализаций не выбрасывает синтаксическую ошибку, но обрабатывает данный случай (хоть и каждая на свой манер).
Имея все вышесказанное, как мы должны сообщить парсеру, что мы хотим именно запустить функцию сразу же после её объявления? Ответ очевиден. Это должна быть функция-выражение, но не функция-декларация. И самым простым способом создать выражение будет использование упомянутого выше оператора группировки, внутри которого всегда будет находиться выражение. Таким образом, парсер распознает код, как функцию-выражение (FE) и неоднозначности не возникнет.
(function foo(x) { alert(x); })(1); // OK, это вызов, не оператор группировки, 1
В примере выше, скобки в конце это уже именно вызов, а не оператор группировки, как было в случае FD.
Обратите внимание, в примере ниже, при вызове функции после её создания, обрамляющие скобки не требуются, т.к. функция уже находится в позиции выражения и парсеру сразу известно, что перед ним 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'); }(); // или даже так !function () { alert('ECMAScript'); }(); // и любой другой вид // трансформации FD в FE ...
Однако скобки являются в данном случае наиболее распространенным и элегантным способом.
Кстати сказать, оператор группировки может обрамлять как описание функции без скобок вызова, так и, включая скобки, т.е. оба выражения, описанные ниже, являются правильными 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 = new 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
Очень интересная статья, только я хотел бы уточнить, в разделе “NFE и SpiderMonkey” в самом первом примере в прототип Object добавляем св-во x, после этого создаем не именованную FE, исходя из текста alert не должен показать 10, т.к. спец. объект для FE не будет создан, потому что как я уже писал FE не именованная. Это так?
@Anton
Да, но не забывай, что в *Mokey реализациях и глобальный объект тоже наследует (в одном из звеньев цепи прототипов) от Object.prototype.
Поэтому первый пример относился к глобальному объекту, а не спец. объекту именованной FE — чтобы показать, что анализ scope chain – двумерный: (1) по звеньям scope chain, (2) и на каждом из звеньев scope chain — вглубь по звеньям prototype chain. И аналогичная ситуация дальше разбирается на примере со спец. объектом NFE. Изменил немного описание, чтобы неоднозначности не возникало.
А к какому типу функций можно отнести фабрики объектов?:
@Фелекс
Фабрики, или точнее в данном случае конструкторы, являются обычными функциями. Подробно работа конструкторов (и ООП в JS в целом) разбирается в седьмой части.
В данном случае у Вас функциональное-выражение (FE). Т.е. функция создается по ходу выполнения программы и сразу же применяется к оператору
new
, который в свою очередь создает новый объект и выполняет функцию в контексте вновь созданного объекта (поэтомуthis
указывает на этот новый объект).Технически Ваш вариант не отличается от вызова функции без new (кроме разницы в установке прототипа, см. ниже):
Подобные одноразовые фабрики (конструкторы) используется, чтобы создать одиночный объект и использовать внутреннее состояние (переменная
foo
в примере выше).Кстати, возвращать
this
в Вашем примере не обязательно, он и так вернется по умолчанию.Существенная разница между Вашим примером и просто вызовом функции с возвратом объекта в том, что при
new
прототипом нового объекта будет значение свойстваprototype
функции-конструктора (фабрики). В моем примере выше, прототипом являетсяObject.prototype
. Однако, в данном конкретном случае Вы не исопльзуете свойствоprototype
, поэтому прототипом Вашего объекта будет пустая прослойка, которая также наследует отObject.prototype
.Равно так же можно декларировать функцию:
Теперь это FD, но используется в том же ключе.
Огромное спасибо Дмитрий, за столь скорый развернутый ответ!
Привычка (однако не пока знаю вредная ли):))
Получaется, что если в фабрике FE не используется свойство prototype, и ее можно представить в нотации FD, то лучше тогда задействовать prototype?
Или все-таки у фабрик есть какие-то преимущества?
Но все-таки у меня отстралось небольшое недопонимание почему эта нотация не описывается в спецификации (по смыслу – FE, но это нигде не описывается)?
И еще хотел бы задать один вопрос, на который я не смог найти ответ в спецификации (уже давно он меня мучает, просто не знал у кого спросить).
Почему в вашем первом примере нельзя сразу вернуть выражение (не создавая локальной переменной)?
@Фелекс
Возвращают обычно для построения цепочек, но, как правило, из других методов, потому что, повторю — из конструктора
this
возвращается по умолчанию.“Не используется” — в смысле, не задействовано по прямому назначению (Вы там ничего не храните). А так — свойство
prototype
есть у любой функции.Не важно — в нотации FD или FE — если Вам нужно задействовать конструктор несколько раз, то дайте ему имя и используйте столько раз с операцией
new
, сколько нужно:В случае Вашей фабрики — создание одиночного (т.е. когда конструктор больше не нужен) объекта со скрытым состоянием. Но, повторю, равно так же можно использовать обычную функцию с возвратом объекта, наследуемого от
Object.prototype
(см. мой пример в предыдущем ответе).“Фабрика” — это абстрактное определение функции-генератора (т.е. функции, которая может создавать объекты). В JavaScript такие функции называются конструкторами. Еще раз — обычная функция, возвращающая объект — это “фабрика”. Но это понятие не имеет никакого отношения к спецификации ECMAScript.
В смысле,
var foo = 1;
? Можно конечно без нее; это просто для наглядности скрытого состояния.В 3-ей главе вы говорите о .apply и .call, а здесь нет. Хотя они к теме функций относятся.
@Ruzzz
Да, возможно имеет смысл их упомянуть в плане применения функций к изменяемому (и неизвестному заранее) количеству аргументов.
А почему именно круглые скобки интерпретируются оператором группировки, а не оператором вызова?
Может, это менее “затратный” способ, а элегантность тут не при чем?
Нет, не правильно. Доп. возможности и “баловство” с семантикой языка – не одно и то же. Короче, признано существенным противоречием.
@AKS
По грамматике так получается. Поскольку, если мы вдруг “передадим аргумент” в такую функцию, то тут же столкнемся с вопросом — “почему это, вызов с параметрами не выдает синтаксической ошибки, а без параметров — имеем
SynaxError
“?На деле же, это никакой не вызов (и в консоль, соответственно, ничего не выводится), а лишь правильно оформленный оператор группировки, который следует за декларацией функции.
То же самое:
Может и так, но я бы не стал исключать элегантность 😉 Поскольку “затратность” тоже разная. В случае с
!
мы, например, экономим один символ в отличие от()
. В случае же со скобками, да, интерпретация возможно будет быстрее.FS вернутся в ES.next, и именно в виде, в котором сейчас присутствуют в SpiderMonkey. Так что “признано”-“непризнано” это лишь временные навешиваемые “лэйблы” конкретных людей 😉
Попробую иначе. Вот интерпретатор понимает, что тут нет оператора вызова. Почему?
Я про “затраты” на выполнение (полагаясь на алгоритмы, изложеные в спецификации).
NOTE Several widely used implementations of ECMAScript are known to support the use of FunctionDeclaration as a Statement. However there are significant and irreconcilable variations among the implementations in the semantics applied to such FunctionDeclarations.
– это из последней версии стандарта…
@AKS
Вероятно потому, что распарсил функцию как FD, которая создается при входе в контекст. Это не
CallExpression
, за которым следуетArguments
, которые и были бы “вызовом”. Если очень интересно, я предлагаю посмотреть грамматику; но по мне — “мануальный парсинг” — утомительно 😉 …хоть и полезно.Я понял, поэтому и сказал, что “затраты” разные бывают и посмотрел на это с другой стороны (и отметил Ваши “затраты”).
А какая разница? У меня раньше тоже было идеализированное восприятие стандарта. Когда же я сам поучаствовал в дискуссиях, увидел, что это обычный человеческий процесс, основанный на локальных причинах и согласно ситуации и времени. Сегодня так, завтра — Function Statements уже будут стандартизованы. Кстати, FS “забанены” в рекомендации ES5-strict. Но самое интересно, что в ES6 их хотят имплементировать, а забанены они только для того, чтобы “кто в лес, кто по дрова” не лез, как в ES3. А вот уже когда ES6 их стандартизует, тогда уже, пожалуйста, имплементируйте.
Проверка синтаксиса наверняка выполняется до того, как что-то создается (контексты, объекты и проч.), не так ли?
Верно. Изучив грамматику, можно избежать вопроса, вроде: “почему это, вызов с параметрами не выдает синтаксической ошибки, а без параметров — имеем SynaxError“?
Зачем вычислять лишнее (булево или что-то еще ненужное)? Все-таки, дело тут не в элегантности.
Как так? Это ведь про тот самый стандарт, о котором повествует данная статья (“ECMA-262-3” есть, как минимум, в названии).
However, this plan may not work if ES5 implementations pollute strict mode…
Естественно, и, в случае данной функции (на уровне
Program
), это будет FD.Здесь нет однозначного ответа, лишь точки зрения и рассуждения 😉
Что не мешает мне иметь собственное мнение и корректировать стандарт, если потребуется (не важно на каком уровне — в виде ли лишь высказанного мнения в какой-то статье или proposal’а в TC-39).
А вообще, говоря в этой статье про то, что SpiderMonkey IMO прав, я имел в виду, что мне больше симпатизирует именно их реализация расширения, чем другие (где функция убегает наружу из
if
-блока).т.е. объект вызова – это некая процедура как продукт конкретной реализации, которая:
1. запускается при вызове функции, например, F();
2. создает контекст исполнения и его свойства: this, AO/VO, Scope;
так?
когда же функция вызывается с ‘new’ new F(), то происходит:
1. Вызов свойства констракт, которое создает native объект;
2. И в последствии вызывается внутреннее свойство call, где опять таки происходит создание контекста исполнения функции с его свойствами, где this уже будет указывать на новый созданый объект в шаге 1, где и происходит его инициализация;
Да, именно так.
Да, тоже все верно.
Было бы неплохо, если бы все русскоязычные посты в этом блоге были помечены специальным тегом.
@thorn
Хорошая идея, добавлю тег.
Доброго дня! Подскажите, пожалуйста, есть ли отличия (помимо синтаксиса) между
и
?
Если есть, то в чем оно заключается (если можно, на примере)?
@Danil
В обоих случаях мы имеем function expression (в скобках — операторе группировки, может быть только expression).
Результатом оператора группировки в первом случае является функция, и затем она применяется. Во втором случае, результатом оператора группировки является уже и сам результат запущенной на исполнение функции.
На практике, обычно, большой разницы нет, но могут быть нюансы, например, такого характера:
Почему выводится
1
, если первая функция даже не запускалась? Почему запуск второй функции выдает ошибку? Все дело в забытой точке с запятой после объявления первой функции, и последующий оператор группировки воспринимаетя, как скобки вызова первой функции, передавая вторую функцию в качестве параметра.Тот же случая, но ошибки уже нет. Однако, по-прежнему, баг в логике.
Чтобы избежать, нужно ставить всегда точку с запятой (и в этом случае, лично мне больше привычен первый вариант, хотя и второй вполне может существовать).
Дмитрий, скажите пожалуйста,
this
в анонимной ф-ии будетnull
–>global
, т.к.Reference
нет для такой ф-ии ( это не identifier и не property accessor). Я правильно понимаю?@tyler, да, абсолютно верно.
Дмитрий, насчет этого кода:
Вы пишете: “На деле же, это никакой не вызов (и в консоль, соответственно, ничего не выводится), а лишь правильно оформленный оператор группировки, который следует за декларацией функции.” Вот я сейчас запустил этот код в консоли хрома с 1 и с пустыми скобками, с 1 оно мне и вывело единицу, а с пустыми скобками написало: “Uncaught SyntaxError: Unexpected token )” Со времени написания статьи что-то изменилось?
P.S. Маленькая просьба – не могли бы вы после каждой статьи указывать какие статьи рекомендуете читать следующими? Я, наприм., задался целью глубоко изучить ECMA Script, и хотел бы изучать его в какой-то осмысленной логической последовательности. Спасибо!
@Сергей
Нет, ничего не изменилось. Chrome просто выводит в консоли последнее вычисленное выражение — и это
(1)
. Можете попробовать просто:Тоже
1
выведет.А вот
()
— это пустой оператор группировки, что является синтаксической ошибкой.Чтобы проверить, что функция не запускается, можете выполнить этот код:
Все статьи цикла ECMA-262-3 идут в хронологической последовательности — т.е., если начнете с первой и продолжите дальше, используемый материал в последующих статьях будет понятен. Также можно читать спецификацию напрямую, сегодня это уже ECMA-262-6 (ES6).