in ECMAScript

Тонкости ECMA-262-3. Часть 5. Функции.

Read this article in: English, French.

Введение

В этой заметке мы подробней поговорим об одном из основных видов объектов 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):

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:

Другие статьи:


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

Write a Comment

Comment

24 Comments

  1. Очень интересная статья, только я хотел бы уточнить, в разделе “NFE и SpiderMonkey” в самом первом примере в прототип Object добавляем св-во x, после этого создаем не именованную FE, исходя из текста alert не должен показать 10, т.к. спец. объект для FE не будет создан, потому что как я уже писал FE не именованная. Это так?

  2. @Anton

    только я хотел бы уточнить, в разделе “NFE и SpiderMonkey” в самом первом примере в прототип Object добавляем св-во x, после этого создаем не именованную FE, исходя из текста alert не должен показать 10, т.к. спец. объект для FE не будет создан, потому что как я уже писал FE не именованная. Это так?

    Да, но не забывай, что в *Mokey реализациях и глобальный объект тоже наследует (в одном из звеньев цепи прототипов) от Object.prototype.

    Поэтому первый пример относился к глобальному объекту, а не спец. объекту именованной FE — чтобы показать, что анализ scope chain – двумерный: (1) по звеньям scope chain, (2) и на каждом из звеньев scope chain — вглубь по звеньям prototype chain. И аналогичная ситуация дальше разбирается на примере со спец. объектом NFE. Изменил немного описание, чтобы неоднозначности не возникало.

  3. А к какому типу функций можно отнести фабрики объектов?:

    var obj = new function(){
       this.foo = function(){
          return 1
       };
       return this;
    }
  4. @Фелекс

    А к какому типу функций можно отнести фабрики объектов?

    Фабрики, или точнее в данном случае конструкторы, являются обычными функциями. Подробно работа конструкторов (и ООП в JS в целом) разбирается в седьмой части.

    В данном случае у Вас функциональное-выражение (FE). Т.е. функция создается по ходу выполнения программы и сразу же применяется к оператору new, который в свою очередь создает новый объект и выполняет функцию в контексте вновь созданного объекта (поэтому this указывает на этот новый объект).

    Технически Ваш вариант не отличается от вызова функции без new (кроме разницы в установке прототипа, см. ниже):

    var obj = (function () {
      var foo = 1;
      return {
        foo: function () {
          return foo;
        }
      };
    })();

    Подобные одноразовые фабрики (конструкторы) используется, чтобы создать одиночный объект и использовать внутреннее состояние (переменная foo в примере выше).

    Кстати, возвращать this в Вашем примере не обязательно, он и так вернется по умолчанию.

    Существенная разница между Вашим примером и просто вызовом функции с возвратом объекта в том, что при new прототипом нового объекта будет значение свойства prototype функции-конструктора (фабрики). В моем примере выше, прототипом является Object.prototype. Однако, в данном конкретном случае Вы не исопльзуете свойство prototype, поэтому прототипом Вашего объекта будет пустая прослойка, которая также наследует от Object.prototype.

    Равно так же можно декларировать функцию:

    function Obj() {
      this.foo = function () {
        return 1;
      };
    }
    
    var foo = new Obj;

    Теперь это FD, но используется в том же ключе.

  5. Огромное спасибо Дмитрий, за столь скорый развернутый ответ!

    Кстати, возвращать this в Вашем примере не обязательно, он и так вернется по умолчанию.

    Привычка (однако не пока знаю вредная ли):))

    Получaется, что если в фабрике FE не используется свойство prototype, и ее можно представить в нотации FD, то лучше тогда задействовать prototype?

    function Obj(){};
    
    Obj.prototype.foo = function() {
        return 1;
    };
    
    var foo = new Obj;
    

    Или все-таки у фабрик есть какие-то преимущества?

    Но все-таки у меня отстралось небольшое недопонимание почему эта нотация не описывается в спецификации (по смыслу – FE, но это нигде не описывается)?

    И еще хотел бы задать один вопрос, на который я не смог найти ответ в спецификации (уже давно он меня мучает, просто не знал у кого спросить).
    Почему в вашем первом примере нельзя сразу вернуть выражение (не создавая локальной переменной)?

  6. @Фелекс

    Привычка (однако не пока знаю вредная ли):))

    Возвращают обычно для построения цепочек, но, как правило, из других методов, потому что, повторю — из конструктора this возвращается по умолчанию.

    Получaется, что если в фабрике FE не используется свойство prototype, и ее можно представить в нотации FD, то лучше тогда задействовать prototype?

    “Не используется” — в смысле, не задействовано по прямому назначению (Вы там ничего не храните). А так — свойство prototype есть у любой функции.

    Не важно — в нотации FD или FE — если Вам нужно задействовать конструктор несколько раз, то дайте ему имя и используйте столько раз с операцией new, сколько нужно:

    var Foo = function () {}; // первый конструктор, FE
    Foo.prototype.x = 10;
    
    function Bar() {} // второй конструктор, FD
    Bar.prototype.x = 10;
    
    var foo1 = new Foo; // можно без скобок, если нет аргументов
    var foo2 = new Foo();
    
    // аналогично
    var bar1 = new Bar();
    var bar2 = new Bar;
    
    // третий конструктор, тоже FE;
    // используется один раз, создает
    // объект "baz" и уничтожается
    var baz = new function () {
      //
    };
    

    Или все-таки у фабрик есть какие-то преимущества?

    В случае Вашей фабрики — создание одиночного (т.е. когда конструктор больше не нужен) объекта со скрытым состоянием. Но, повторю, равно так же можно использовать обычную функцию с возвратом объекта, наследуемого от Object.prototype (см. мой пример в предыдущем ответе).

    Но все-таки у меня отстралось небольшое недопонимание почему эта нотация не описывается в спецификации (по смыслу – FE, но это нигде не описывается)?

    “Фабрика” — это абстрактное определение функции-генератора (т.е. функции, которая может создавать объекты). В JavaScript такие функции называются конструкторами. Еще раз — обычная функция, возвращающая объект — это “фабрика”. Но это понятие не имеет никакого отношения к спецификации ECMAScript.

    Почему в вашем первом примере нельзя сразу вернуть выражение (не создавая локальной переменной)?

    В смысле, var foo = 1;? Можно конечно без нее; это просто для наглядности скрытого состояния.

  7. В 3-ей главе вы говорите о .apply и .call, а здесь нет. Хотя они к теме функций относятся.

  8. @Ruzzz

    .apply и .call

    Да, возможно имеет смысл их упомянуть в плане применения функций к изменяемому (и неизвестному заранее) количеству аргументов.

  9. Обратите внимание, в данном случае — это именно оператор группировки, который следует за декларацией функции, а не скобки вызова функции!

    А почему именно круглые скобки интерпретируются оператором группировки, а не оператором вызова?

    Однако скобки являются в данном случае наиболее распространенным и элегантным способом.

    Может, это менее “затратный” способ, а элегантность тут не при чем?

    Моё мнение, что SpiderMonkey поступает в данном случае правильно, выделяя негласный средний вид функции — (FE + FD).

    Нет, не правильно. Доп. возможности и “баловство” с семантикой языка – не одно и то же. Короче, признано существенным противоречием.

  10. @AKS

    А почему именно круглые скобки интерпретируются оператором группировки, а не оператором вызова?

    По грамматике так получается. Поскольку, если мы вдруг “передадим аргумент” в такую функцию, то тут же столкнемся с вопросом — “почему это, вызов с параметрами не выдает синтаксической ошибки, а без параметров — имеем SynaxError“?

    function foo(x) {
      console.log(x);
    }(1); // "вызываем" с параметром 1
    

    На деле же, это никакой не вызов (и в консоль, соответственно, ничего не выводится), а лишь правильно оформленный оператор группировки, который следует за декларацией функции.

    То же самое:

    // декларация
    function foo(x) {
      console.log(x);
    }
    
    (1); // группировка 1
    
    foo(1); // вызов

    Может, это менее “затратный” способ, а элегантность тут не при чем?

    Может и так, но я бы не стал исключать элегантность 😉 Поскольку “затратность” тоже разная. В случае с ! мы, например, экономим один символ в отличие от (). В случае же со скобками, да, интерпретация возможно будет быстрее.

    Нет, не правильно. Доп. возможности и “баловство” с семантикой языка – не одно и то же. Короче, признано существенным противоречием.

    FS вернутся в ES.next, и именно в виде, в котором сейчас присутствуют в SpiderMonkey. Так что “признано”-“непризнано” это лишь временные навешиваемые “лэйблы” конкретных людей 😉

  11. тут же столкнемся с вопросом — “почему это, вызов с параметрами не выдает синтаксической ошибки, а без параметров — имеем SynaxError“?

    Попробую иначе. Вот интерпретатор понимает, что тут нет оператора вызова. Почему?

    В случае с ! мы, например, экономим один символ в отличие от ().

    Я про “затраты” на выполнение (полагаясь на алгоритмы, изложеные в спецификации).

    Так что “признано”-”непризнано” это лишь временные навешиваемые “лэйблы” конкретных людей

    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.
    – это из последней версии стандарта…

  12. @AKS

    Вот интерпретатор понимает, что тут нет оператора вызова. Почему?

    Вероятно потому, что распарсил функцию как FD, которая создается при входе в контекст. Это не CallExpression, за которым следует Arguments, которые и были бы “вызовом”. Если очень интересно, я предлагаю посмотреть грамматику; но по мне — “мануальный парсинг” — утомительно 😉 …хоть и полезно.

    Я про “затраты” на выполнение (полагаясь на алгоритмы, изложеные в спецификации).

    Я понял, поэтому и сказал, что “затраты” разные бывают и посмотрел на это с другой стороны (и отметил Ваши “затраты”).

    это из последней версии стандарта

    А какая разница? У меня раньше тоже было идеализированное восприятие стандарта. Когда же я сам поучаствовал в дискуссиях, увидел, что это обычный человеческий процесс, основанный на локальных причинах и согласно ситуации и времени. Сегодня так, завтра — Function Statements уже будут стандартизованы. Кстати, FS “забанены” в рекомендации ES5-strict. Но самое интересно, что в ES6 их хотят имплементировать, а забанены они только для того, чтобы “кто в лес, кто по дрова” не лез, как в ES3. А вот уже когда ES6 их стандартизует, тогда уже, пожалуйста, имплементируйте.

  13. Вероятно потому, что распарсил функцию как FD, которая создается при входе в контекст.

    Проверка синтаксиса наверняка выполняется до того, как что-то создается (контексты, объекты и проч.), не так ли?

    Если очень интересно, я предлагаю посмотреть грамматику

    Верно. Изучив грамматику, можно избежать вопроса, вроде: “почему это, вызов с параметрами не выдает синтаксической ошибки, а без параметров — имеем SynaxError“?

    Я понял, поэтому и сказал, что “затраты” разные бывают и посмотрел на это с другой стороны (и отметил Ваши “затраты”).

    Зачем вычислять лишнее (булево или что-то еще ненужное)? Все-таки, дело тут не в элегантности.

    А какая разница?

    Как так? Это ведь про тот самый стандарт, о котором повествует данная статья (“ECMA-262-3” есть, как минимум, в названии).

    Но самое интересно, что в ES6 их хотят имплементировать

    However, this plan may not work if ES5 implementations pollute strict mode…

  14. Проверка синтаксиса наверняка выполняется до того, как что-то создается (контексты, объекты и проч.), не так ли?

    Естественно, и, в случае данной функции (на уровне Program), это будет FD.

    Все-таки, дело тут не в элегантности.

    Здесь нет однозначного ответа, лишь точки зрения и рассуждения 😉

    Как так? Это ведь про тот самый стандарт, о котором повествует данная статья (“ECMA-262-3″ есть, как минимум, в названии).

    Что не мешает мне иметь собственное мнение и корректировать стандарт, если потребуется (не важно на каком уровне — в виде ли лишь высказанного мнения в какой-то статье или proposal’а в TC-39).

    А вообще, говоря в этой статье про то, что SpiderMonkey IMO прав, я имел в виду, что мне больше симпатизирует именно их реализация расширения, чем другие (где функция убегает наружу из if-блока).

  15. // объект вызова, именно он активируется
    // и создаёт контекст посредством выражения вызова F()
    F.[[Call]] = callObject

    т.е. объект вызова – это некая процедура как продукт конкретной реализации, которая:
    1. запускается при вызове функции, например, F();
    2. создает контекст исполнения и его свойства: this, AO/VO, Scope;

    так?

    когда же функция вызывается с ‘new’ new F(), то происходит:
    1. Вызов свойства констракт, которое создает native объект;
    2. И в последствии вызывается внутреннее свойство call, где опять таки происходит создание контекста исполнения функции с его свойствами, где this уже будет указывать на новый созданый объект в шаге 1, где и происходит его инициализация;

  16. т.е. объект вызова – это некая процедура как продукт конкретной реализации, которая:
    1. запускается при вызове функции, например, F();
    2. создает контекст исполнения и его свойства: this, AO/VO, Scope;

    так?

    Да, именно так.

    когда же функция вызывается с ‘new’ new F(), то происходит:
    1. Вызов свойства констракт, которое создает native объект;
    2. И в последствии вызывается внутреннее свойство call, где опять таки происходит создание контекста исполнения функции с его свойствами, где this уже будет указывать на новый созданый объект в шаге 1, где и происходит его инициализация;

    Да, тоже все верно.

  17. Было бы неплохо, если бы все русскоязычные посты в этом блоге были помечены специальным тегом.

  18. Доброго дня! Подскажите, пожалуйста, есть ли отличия (помимо синтаксиса) между

     (function () {}()); 

    и

     (function () {})(); 

    ?
    Если есть, то в чем оно заключается (если можно, на примере)?

  19. @Danil

    В обоих случаях мы имеем function expression (в скобках — операторе группировки, может быть только expression).

    Результатом оператора группировки в первом случае является функция, и затем она применяется. Во втором случае, результатом оператора группировки является уже и сам результат запущенной на исполнение функции.

    На практике, обычно, большой разницы нет, но могут быть нюансы, например, такого характера:

    var foo = function () {
      console.log(1);
    }
    
    (function() {})(); // TypeError

    Почему выводится 1, если первая функция даже не запускалась? Почему запуск второй функции выдает ошибку? Все дело в забытой точке с запятой после объявления первой функции, и последующий оператор группировки воспринимаетя, как скобки вызова первой функции, передавая вторую функцию в качестве параметра.

    var foo = function () {
      console.log(1);
    }
    
    (function() {}()); // нет ошибки

    Тот же случая, но ошибки уже нет. Однако, по-прежнему, баг в логике.

    Чтобы избежать, нужно ставить всегда точку с запятой (и в этом случае, лично мне больше привычен первый вариант, хотя и второй вполне может существовать).

  20. (function( )  {
        alert(this);
    })( );
    

    Дмитрий, скажите пожалуйста, this в анонимной ф-ии будет null –> global, т.к. Reference нет для такой ф-ии ( это не identifier и не property accessor). Я правильно понимаю?

  21. Дмитрий, насчет этого кода:

    function foo(x) {
      console.log(x);
    }(1); // "вызываем" с параметром 1

    Вы пишете: “На деле же, это никакой не вызов (и в консоль, соответственно, ничего не выводится), а лишь правильно оформленный оператор группировки, который следует за декларацией функции.” Вот я сейчас запустил этот код в консоли хрома с 1 и с пустыми скобками, с 1 оно мне и вывело единицу, а с пустыми скобками написало: “Uncaught SyntaxError: Unexpected token )” Со времени написания статьи что-то изменилось?
    P.S. Маленькая просьба – не могли бы вы после каждой статьи указывать какие статьи рекомендуете читать следующими? Я, наприм., задался целью глубоко изучить ECMA Script, и хотел бы изучать его в какой-то осмысленной логической последовательности. Спасибо!

  22. @Сергей

    Вот я сейчас запустил этот код в консоли хрома с 1 и с пустыми скобками, с 1 оно мне и вывело единицу, а с пустыми скобками написало: “Uncaught SyntaxError: Unexpected token )” Со времени написания статьи что-то изменилось?

    Нет, ничего не изменилось. Chrome просто выводит в консоли последнее вычисленное выражение — и это (1). Можете попробовать просто:

    (1);
    

    Тоже 1 выведет.

    А вот () — это пустой оператор группировки, что является синтаксической ошибкой.

    Чтобы проверить, что функция не запускается, можете выполнить этот код:

    //Выдаст 1, а не 2.
    function foo() {
      console.log(2);
    }(1); // "вызываем" с параметром 1

    Я, наприм., задался целью глубоко изучить ECMA Script, и хотел бы изучать его в какой-то осмысленной логической последовательности.

    Все статьи цикла ECMA-262-3 идут в хронологической последовательности — т.е., если начнете с первой и продолжите дальше, используемый материал в последующих статьях будет понятен. Также можно читать спецификацию напрямую, сегодня это уже ECMA-262-6 (ES6).