image


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


Короткая справка


Named Function Expression — расширение функциональных выражений в JavaScript, позволяющее именовать функцию, созданную как часть выражения (FunctionExpression):


let fe = function named(...) { /* function body */ };

Вся суть в том, что изнутри функции, на которую ссылается переменная fe, есть доступ к самой функции через имя named. Только изнутри функции и перезаписать имя нельзя!


Как это можно использовать


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


const count = f => function df() {
    df.calls = (df.calls || 0) + 1;
    return f(...arguments);
};

Здесь мы получили доступ к возвращаемой функции df, чтобы при ее вызове сохранять счетчик в свойство calls, которое будет доступно для чтения при необходимости:


const csum = count((x, y) => x + y);
csum(5, 10); // 15
csum(50, 1); // 51
csum.calls; // 2

Или сохраняем все результаты вызова функции:


const accum = f => function df() {
    let res = f(...arguments);
    (df.results = (df.results || [])).push(res);
    return res;
};

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


Очевидно, функцию мы можем и вызывать. Использовать NFE для рекурсии особенно приятно. Допустим, мы хотим найти n-ый член последовательности Фибоначчи (каждый почему-то рано или поздно этого хочет):


const rf = function getfn(n) {
    return n > 2 ? getfn(n - 2) + getfn(n - 1) : 1;
};
rf(1); // 1
rf(10); // 55

Не стоит здесь обращать внимание на "нехвостоватость". А вот на возможность сделать то же самое через FunctionDeclaration — да. Но у функции-выражения есть одно преимущество: если вам захочется перенести функцию из rf в другую переменную, не потребуется править рекурсивный вызов внутри.


Аккуратный пример реализации таймера:


const ping = (callback, t) => setTimeout(function pf() {
    callback();
    setTimeout(pf, t);
}, t);

Здесь NFE мы используем литералом в качестве аргумента вместо объявления ненужной переменной. Почему не setInterval? Вместо callback'а здесь может быть ожидание резолва промиса перед следующим тиком таймера.


Интересным примером будет комбинация NFE и IIFE (Immediately-Invoked Function Expression), когда по-месту необходим только результат рекурсивной функции. Всего один раз:


let data = {
    f10: function fact(n) {
        return n > 1 ? n * fact(n - 1) : 1;
    }(10)
};
data.f10; // 3628800

Такое себе? ну хорошо, вот реальная задачка: Есть некоторая строго-возрастающая функция f, действующая во множестве натуральных чисел. Найти крайнюю точку x, при которой значение функции не превышает заданного y. Примером такой функции могут быть f(x) = 3 * x + 5 или f(x) = 2^x + 11.


const find = (f, y) => function bs(a, b) {
    if (a + 1 === b) return a;
    let m = Math.ceil((a + b) / 2);
    return f(m) <= y ? bs(m, b) : bs(a, m);
}(-1, y + 1);

find(x => 3 * x + 5, 200); // 65
find(x => Math.pow(2, x) + 11, 1000); // 9

Вдаваться в математические детали не будем. Рассмотрим реализацию:


  1. Требуемая функция find имеет два наших параметра f, y и возвращает результат IIFE.
  2. Немедленно-вызываемая функция реализует бинарный поиск решения, первый вызов получает начальный диапазон.
  3. Сам поиск реализован через рекурсию, что использует имя NFE для последующих вызовов. Мы не декларируем функцию поиска и не создаем новую переменную.

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


Наконец пара слов про трассировку стека. У нас есть некоторая NFE-функция, в теле которой вылетело исключение:


const fe = function named() {
    throw new Error('Something went wrong');
};

Так как в выражении функция у нас именованная, мы увидим ее в стек-трейсе:


Uncaught Error: Something went wrong
    at named (<anonymous>:2:11)

Однако, начиная с ES-2015 многие анонимные выражения функций создают на самом деле функцию с именем, выводя его из контекста:


const unnamed = function() {
    throw new Error('Something went wrong');
};

Функция в правой части выражения ассоциируется с переменной в левой:


Uncaught Error: Something went wrong
    at unnamed (<anonymous>:2:7)

Но не всегда это возможно. Классический пример — инициализация скрипта внешней библиотеки через IIFE, как вариант:


/**
 * @license
 * @description
 */
;(function() {
    // some awesome code
}.call(this));

Или частый пример функции, возвращающей другую функцию:


const bind = (f, ctx) => function() {
    return f.apply(ctx, arguments);
};

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


Короткий вывод


NFE хорошо помогает писать лаконичный и неподдерживаемый код для быстрого прототипирования широкого спектра задач. Использовать их красоту в конечном коде следует пару раз подумав: JavaScript и без того перегружен интересными спецификами.

Комментарии (7)


  1. Sirion
    29.10.2018 14:00

    del


  1. andres_kovalev
    30.10.2018 01:23

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

    На мой взгляд, плохой пример — тут лучше подойдет замыкание. Поля вашей функции могут быть изменены извне:
    const count = f => function df() {
        df.calls = (df.calls || 0) + 1;
        return f(...arguments);
    };
    
    const foo = count(console.log);
    foo.calls = 100;
    ...
    


    1. cerberus_ab Автор
      30.10.2018 01:35

      Да, в случае с замыканием нам потребуется вернуть объект-контейнер: вызываемая функция + геттер для получения количества обращений. Настоящий пример демонстрирует работу с функцией внутри NFE как с объектом. JavaScript позволяет решить простую задачу, избегая лишнего кода.

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


  1. vagonpidarasov
    30.10.2018 13:13

    В чем преимущество назначать константе функциональное выражение, пусть даже это именованная функция? Почему не делать это по-старому:

    export function foo() {}

    ?


    1. cerberus_ab Автор
      30.10.2018 13:22

      В примере с числами Фибоначчи. Если мы захотим перенести функцию в другую переменную, то есть:


      let rf2 = rf;
      rf = null;

      В случае с декларированием функции (FunctionDeclaration), мы получим ошибку. Так как внутри функция будет вызывать себя по старому имени. Такая защита.


      Ну а сам FunctionExpression полезен например при условном объявлении функции, так как такая функция создается в процессе выполнения выражения:


      let fe;
      if (cond) {
          fe = function() { /* 1 */ };
      } else {
          fe = function() { /* 2 */ };
      }

      Это уже другая тема )


      1. vagonpidarasov
        30.10.2018 13:27

        Это надуманный пример из разряда олимпиадных задач. В 95% случаях делать такого не требуется.


        1. cerberus_ab Автор
          30.10.2018 13:32
          +1

          Конечно, это Ненормальное программирование ) в то же время — особенности языка.