Есть множество различных способов объявить функцию:
function A() {}; // декларация функции
var B = function () {}; // функциональное выражение
var C = (function () {}); // функциональное выражение с оператором группировки
var D = function foo () {}; // именованное функциональное выражение
var E = (function () {})(); // самовызывающееся функциональное выражение
var F = new Function(); // конструктор функции
var G = new function() {}; // вырожденный случай: конструктор объекта
В таком обилии сложно не запутаться, не так ли? Как правило, в повседневной жизни мы используем не более трех различных типов объявления функций, и это отлично работает. Однако если копнуть поглубже, то может оказаться, что большинство из нас даже не подозревает какой объём таинств и подводных камней хранит в себе операция объявления функции.
Согласно документации ECMA синтаксис определения функции следующий:
ДекларацияФункции:
function Идентификатор ( Параметры ) { ТелоФункции }
ФункциональноеВыражение:
function Идентификатор (опционально) ( Параметры ) { ТелоФункции }
Хоть и выглядят эти определения вполне схожими, но между декларацией функции и функциональным выражением есть большая разница. Декларация функции (Function Declaration) создается до выполнения любого кода, в то время как функциональное выражение (Function Expression) будет создано только в момент, когда интерпретатор дойдёт до данной строки кода.
Функциональное выражение — это объявление функции в контексте какого-либо выражения.
Рассмотрим несколько примеров функциональных выражений:
Оператор присваивания
var a = function() {};
Это классический пример задания функционального выражения через присваивание. Оператор присваивания ожидает справа выражение, именно поэтому функция становится частью выражения.
Немного пофантазировав, можно придумать следующие примеры:
var a = function() { return 1; }() + 12; // 13
var b = function() { return 1; } + ''; // function (){return 1}
var c = function() { return 1; } + '' - 1; //NaN
Оператор группировки
Декларируя функцию, мы не можем выполнить её сразу же, однако, обернув декларацию функции в круглые скобки — это становится возможно. Оператор группировки выражается круглыми скобками, и в данном случае он превращает декларацию функции в функциональное выражение.
function foo() { return 1; } // undefined
function foo() { return 1; }(); // Uncaught SyntaxError: Expected () to start arrow function, but got '}' instead of '=>'
(function foo() { return 1; }()) // 1
(function foo() { return 1; })() // 1
Принципиальной разницы между третьим и четвертым вариантом нет, так как в первом случае мы выполняем выражение, которое определяет и сразу же исполняет функцию, а во втором случае выполняется выражение, определяющее функцию, которая затем будет выполнена.
Оператор запятая
Оператор запятая вычисляет значение каждого своего операнда (слева направо) и возвращает значение последнего операнда.
0, function() { return 1; }(); // 1
Операторы (+, -, !, ~, void)
+function() { return false; }(); // 0
-function() { return false; }(); // -0
!function() { return false; }(); // true
~function() { return false; }(); // -1
void function() { return false; }(); //undefined
Комбинированные операторы:
!-function () { return false; }(); // true
var c = 5 * (2 - function () {return 1}()) // 5
var c = 5 * 2 - -~function () {return 1}() // 8
Отличие именованных функциональных выражений от не именованных:
Зачастую, имя, присвоенное функциональному выражению, оказывается избыточным в контексте остального кода, кроме случая, когда доступ к функции необходимо получить из неё же самой. Область видимости имени функционального выражения ограничена исключительно самой функцией.
var f = function getFactorial (n) {
return n ? n * getFactorial(n - 1) : 1;
};
f(5); // 120
Важно:
Помните, вы пишете код для людей, поэтому старайтесь избегать написания кода в стиле ниндзя. Приведённые в статье знания полезны для того, чтобы понимать внутреннее устройство языка и не растеряться в случае, если вам вдруг встретятся такие выражения в одном из проектов или на собеседовании.
Комментарии (20)
mwizard
28.12.2015 00:00+2После прочтения статьи возникло несколько вопросов и комментариев:
- чем B отличается от C?
- почему E с G записали в объявления функций, если E — вызов, а G — объект?
- если B и D — разные, то чего уж там, давайте перечислять
function foo() {}
,function bar() {}
,function buzz() {}
как разные формы объявления функции. Почему нет? - почему в примерах присваивания
a
включает результат выполнения функции, аb
иc
— саму функцию? - Почему «оператор группировки» упоминается отдельно, если он не имеет никакого отношения к объявлению IIFE вообще?
- чем примеры ваших операторов, включая запятую, отличаются от того, чтобы заменить IIFE на сразу результат? Где разница между
void function...
иvoid 42
?
Yekver
28.12.2015 00:45-11. Наличием оператора группировки. Функционально они идентичны.
2. G сделан как дополнительный пример использования ключевого словаfunction
3. Они разные в том плане что имени может и не быть. Важно было показать что возможен и такой вариант тоже.
4. Это же примеры, хотелось показать оба варианта. В данном случае не очень принципиально наличие вызова функции.
5. Почему не имеет?
6. Примеры выбраны тривиальными чтобы не нагружать статью лишним кодом. А так да, если бы этот код был в реальном проекте, то ваши замечания очень даже по делу.
k12th
28.12.2015 10:43Недавно пошло дикое поветрие всегда объявлять функции через
var a = function () {}
. Спрашиваешь зачем — мнутся что-то про хойстинг, но никто внятного ответа не дал, зачем в данном конкретном случае хойстинга надо избежать.trikadin
28.12.2015 12:50+1Интересно, где вы это нашли такое. Все мои знакомые спокойно юзают function declaration.
Лично у меня сейчас ко всплытию только одна претензия — из-за него нельзя сделать нормальные декораторы для функций.k12th
28.12.2015 12:53На работе и еще в паре мест, неважно.
Я сегодня отчаянно не выспался — подскажите, почему хойстинг мешает декораторам?
trikadin
28.12.2015 14:23+3let counter = 0; let add = function () { counter++; }; @add function foo() { }
Соль в том, что из-за всплытия объявление функции foo будет происходить раньше объявления и инициализации переменных counter и add (т. к. function declaration объявляются на входе в блок).
Zibx
Основной смысл записи var f = function myFunc(){ } заключается в нормальных именах в стеке вызовов при отладке.
mwizard
Что в ES6 уже не имело смысла из-за автоматического вывода имен анонимных функций из переменных и контекста объявления.
MuLLtiQ
К сожалению, свойство
name
у анонимных функций все еще не выводится из имени переменной, только из явного объявления имени функции. Мелочь, конечно, но иногда все же неудобно.mwizard
Свойство name — нет, правда ваша. В стек-трейсах же все отображается наглядно — github.com/v8/v8/blob/master/test/cctest/test-func-name-inference.cc
rock
Да ладно? :) 12.14.4 -> 1 -> e -> ii
MuLLtiQ
ну пока этого никто, похоже, не реализовал :(
rock
Babel? :)
MuLLtiQ
Да, Babel хорош :)
SerafimArts
Ещё именованные функции распространяются на весь код сразу, а безымянные только на тот, который идёт после объявления этой функции. Тоже вполне важное отличие, но не столь существенное, согласен.
Zenitchik
Не на весь.
Декларация функции — только на тот скоуп, в котором объявлена.
Функциональное выражение — не распространяется, по имени такая функция доступна только внутри себя (кроме ИЕ8).
Zenitchik
Это для удобства рекурсивного вызова. По имени функция доступна внутри себя. Правда, в ИЕ8 есть проблемы — имя оказывается доступно извне.