Разные способы определения функций в JavaScript (это сумасшествие!)

Это перевод с английского оригинальной статьи  Different Ways of Defining Functions in JavaScript (This Is Madness!).

Это сумасшествие! Это… JavaScript!

Чтобы добиться какого-то эффекта в JavaScript, всегда есть много способов. Это одновременно и хорошо, и плохо. Безусловно, это плохо для новичков, потому что означает не только то, что придется больше учиться, но и то, что присутствует больше подводных камней и возможных ошибок. И определение функций — не исключение.

Цель этой статьи — доступный обзорный тур, просто чтоб вы знали, что почëм и каковы главные отличия. Обратите внимание на раздел «Дополнительно по теме»! Многое в этой статье основано на посте Юрия Зайцева, в котором все расписано подробнее. Однако, я не нашел ни одного материала, где были бы показаны все виды определения функций.

Что насчет выполнения функций? Это поднимает ещё целый ряд проблем и открывает возможность для новой будущей статьи на эту тему. :)

Обзор: различные способы объявления функций

function A(){}; // объявление функции
var B = function(){}; // функциональное выражение
var C = (function(){}); // функциональное выражение с группирующими операторами
var D = function foo(){}; // именованное функциональное выражение
var E = (function(){ // немедленно исполняемое функциональное выражение (IIFE), которое возвращает функцию
    return function(){}
})();
var F = new Function(); // конструктор Function 
var G = new function(){}; // особый случай: конструктор объекта

Объявления функций: function A(){};

Объявление функций — это, пожалуй, лучше всего знакомый нам способ сделать чего-нибудь в мире JavaScript. Так мы создаем переменную A, которая доступна в текущей области видимости. Область видимости — это отдельная тема, так что во всех последующих примерах все происходит в глобальной области видимости (то, чего обычно следует избегать).

1. Hoisting (поднятие переменной)

Интересно, что функции «поднимаются» (hoist) в самый верх их области видимости, а это означает, что данный код:

A();
function A(){
    console.log('foo');
};

исполняется, как этот код:

function A(){
    console.log('foo');
};
A();

а это на практике значит, что вы можете вызывать функции перед тем, как они написаны в коде. Это не имеет значения, потому что вся функция «поднимается» в самый верх области видимости. (Это контрастирует с переменными, у которых «поднимается» только их объявление, но не их содержимое, как мы увидим в следующем разделе).

2. Никаких объявлений функций в блоках if или циклах и т.д.)

Таким способом нельзя определять функции в выражениях, например, в блоках if, что достаточно часто нужно, если мы хотим определить разные версии функции для разных обстоятельств, обычно, чтобы подстроиться под разные браузеры. Конечно, в некоторых случаях вы можете это сделать, но получившийся код будет неконсистентным (kangax задокументировал неконсистентности здесь). Если хотите использовать этот паттерн, лучше пользуйтесь функциональными выражениями.

3. Объявления функций должны иметь имена

Данный метод не позволяет вам создавать анонимные функции, а это означает, что вы всегда должны присваивать какой-нибудь идентификатор (в данном случае мы использовали «A»).

Функциональные выражения: var B = function(){};

Функциональное выражение выглядит похоже на объявление функции за тем исключением, что функция присваивается переменной. Несмотря на то, что функции не являются примитивными значениями в JavaScript, это тот способ, при котором они могут быть использованы по полной в этом функциональном языке. Функции относятся к «первому классу«:

[JavaScript] поддерживает передачу функций другим функциям в качестве аргументов, возвращая их как значения из других функций и присваивая их переменным или храня их в структурах данных.

1. Анонимные функции (им не нужны имена)

В функциональном выражении имя функции не обязательно, и мы называем их анонимными. Здесь мы делаем переменную B равной анонимной функции:

var B = function(){};

2. Поднятие (hoisting) объявления переменной

Объявления переменных «поднимаются» (hoist) в самый верх их области видимости, это напоминает поднятие функций, но при этом значение переменной не поднимается. Это происходит со всеми переменными, что означает, что то же самое происходит с нашими функциями, которые мы присваиваем переменным.

Этот код:

var A = function(){};
var B = function(){};
var C = function(){};

будет выполнен вот так:

var A, B, C; // объявления переменных поднимаются
A = function(){};
B = function(){};
C = function(){};

Следовательно, важен порядок, в котором функции создаются и вызываются

// Это работает
var B = function(){};
B();

// Это не работает
B2(); // TypeError (B2 не определена)
var B2 = function(){};

Второй пример выдает нам ошибку, потому что у переменной B2 поднимается только ее объявление, но не ее определение, вызывая таким образом ошибку «undefined».

Функциональные выражения с группирующими операторами: var C = (function(){});

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

Вот хороший способ посмотреть, что происходит:

function(){}; // синтаксическая ошибка SyntaxError
(function(){});

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

Второй пример использует скобки — операторы группировки — и таким образом вычисляется по-другому, как функциональное выражение. Операторы группировки мы применяем, чтобы помочь показать, что должно быть вычислено вперёд, как в математических задачах. Мы как бы говорим: «Вычисли сначала вот это, потом возьми результат и сделай с ним что-нибудь»:

(1 + 2) * 3; // 9
1 + (2 * 3); // 7

В первом примере мы говорим: «сначала сложи 1 и 2, затем возьми результат и умножь на 3», тогда как во втором примере мы говорим: «сначала перемножь 2 и 3, затем возьми результат и прибавь к нему 1».

Так как функции относятся к первому классу, то мы можем использовать похожие операторы группировки. Вот несерьезный пример, который, однако, показывает, как мы, в принципе, можем вставить функцию в той же манере:

(function(){} + 1); // function(){}1

Результат — строка (потому что для функции вызывается метод toString(), а затем прибавляется/присоединяется 1), но я надеюсь, вы уловили идею.

Когда движок JavaScript встречает здесь открывающую скобку, мы по сути говорим: «ОК, начни это группировать с чем-то еще». Используя наши технические термины, мы говорим движку, что мы не производим объявление функции, а вместо этого создаем функциональное выражение. И тогда его результат мы можем присвоить переменной:

(function(){}); // результирующая функция не присвоена
var foo = (function(){}); // результирующая функция присвоена foo
var bar = function(){}; // результирующая функция присвоена bar

Здесь мы можем увидеть, что foo и bar — по сути одно и то же, потому что в foo мы группируем функцию ни с чем иным, как с самой собой.

Именованное функциональное выражение: var D = function foo(){};

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

1. Имя функции доступно только внутри самой функции

Мы не сделали имя функции (foo) видимым в содержащей ее области видимости (в этом случае глобальной области видимости):

var D = function foo(){
    console.log(typeof foo);
};
D(); // function
console.log(typeof foo); // undefined

2. Полезно для рекурсии

Так как имя функции доступно в самой функции, то получается, что это полезно для рекурсивных функций, гораздо более полезно, чем обычные старые анонимные функции.

Для иллюстрации вот тривиальная рекурсивная функция, вызывающая саму себя из именованного функционального выражения:

var countdown = function a(count){
    if(count > 0) {
        count--;
        return a(count); // еще мы можем сделать так: a(--count), но это не так очевидно
    }
    console.log('конец рекурсивной функции');
}
countdown(5);

3. Полезно для отладки

Как указывали некоторые, присваивание прежде анонимным функциям имен помогает при отладке, так как имя функции появляется в стеке вызовов.

4. Особенности: плохая реализация в JScript

kangax указывает на то, что именованные функциональные выражения являются по сути ядом для JScript, реализации JavaScript в Internet Explorer.

Именованная функция становится глобальной переменной, она «поднимается» (hoist) так же, как объявление функции и в конце концов создает несколько экземпляров той же самой функции.

Немедленно исполняемые функторы (Immediately-invoked function expressions, IIFE): var E = (function(){return function(){}})();

“Выполни эту функцию, возвращаемое значение которой является другой функцией, и присвой это переменной E”. Это может выглядеть волшебством, но на самом деле это очень просто, а паттерн эффективен  имеет полезные приложения, наиболее известное из которых — паттерн «модуль».

Сначала мы напишем пример, который не выглядит, как волшебство:

var foo = function(){
    return 'bar';
};
var output = foo();
console.log(output); // 'bar'

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

var foo = function(){
    return 'bar';
};
var output = (foo)(); // обратите внимание на дополнительные операторы группировки
console.log(output); // 'bar'

Поскольку foo указывает на на наше функциональное выражение, мы знаем, что можем просто не использовать переменную «foo» и вставить всю функцию как анонимную (ведь функции являются объектами первого класса!):

var output = (function(){
    return 'bar';
})();
console.log(output); // 'bar'

Но подождите, мы только что пришли к волшебной результирующей функции! Оказалось, что она не такая уж и волшебная, как только мы разобрали ее по частям. Это просто сокращение для кода, который мы написали изначально, где мы определили функцию, выполнили ее и определили вывод как ее возвращенное значение.

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

var E = (function(){
    return function(){}
})();

Приложения

Для этого метода есть хорошие варианты использования, включая скрытие информации в паттерне «модуль», (частичное применение (partial application), например) и другие умные штуки, использующие его. Это однозначно не тривиальный паттерн.

Конструктор Function: var F = new Function();

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

1. Определение функции

Вы можете создавать функцию так:

var F = new Function('arg1', 'arg2', 'console.log(arg1 + ", " + arg2)');
F('foo', 'bar'); // 'foo, bar'

2. Вам не нужен оператор new

Вы можете просто написать var F = Function(); чтобы получить тот же результат.

3. Особенности

В документации MDN приведены хорошие примеры особенностей применения (quirks), включая тот факт, что функции, объявленные при помощи конструктора Function, не наследуют нормально их текущую область видимости (то есть замыкание не формируется).

Это значит, что они не имеют доступа к переменным из включающей их области видимости, а это не очень полезно:

function foo(){
    var bar = 'blah';

    var first = new Function('console.log(typeof bar)');
    first(); // undefined

    var second = function(){
        console.log(typeof bar);
    }
    second(); // string
}
foo();

В функции «first» мы используем конструктор Function, так что он не имеет доступа к переменной bar. Однако, если мы используем являющуюся функциональным выражением функцию «second», то она фактически имеет доступ к переменным, определенным в содержащей ее области видимости (через замыкание).

Другими словами, не пользуйтесь конструктором Function .

Особый случай — конструктор объекта: var G = new function foo(){};

Я припас это напоследок, потому что на самом деле мы не определяем функцию, но мы используем ключевое слово function, так что по крайней мере этот случай стоит отметить.

new function(){}; создает новый объект и вызывает анонимную функцию в качестве конструктора. Если из функции возвращается объект, то он становится результирующим объектом, иначе создается с нуля новый объект, а функция выполняется в контектсе этой новой функции (давайте оставим подробности для другого поста!).

Немного необычно видеть это в такой форме. Давайте сделаем это должным образом:

var Person = function(){
    console.log(this); // Person
}
var joe = new Person();

Так что на самом деле оператором new мы даем функции новый контекст «this» и затем выполняем данную функцию с этим новым контекстом. Это сильно отличается от определений функции, с которыми мы имели дело выше! Это ведет к совершенно новой теме, и мы прибережем ее на потом!

Дополнительно по теме (англ.)

Named function expressions demystified (kangax)

Immediately-Invoked Function Expression (IIFE) (Ben Alman)

Functions and function scope (Mozilla Developer Network — MDN)

How does an anonymous function in JavaScript work? (StackOverflow)

Function Declarations vs. Function Expressions (JavaScript, JavaScript by Angus Croll)

JavaScript: The Definitive Guide (классическая книга Дэвида Фленагана)

1 комментарий

Оставить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.