Определите функцию в другой функции в JavaScript

83
function foo(a) {
    if (/* Some condition */) {
        // perform task 1
        // perform task 3
    }
    else {
        // perform task 2
        // perform task 3
    }
}

У меня есть функция, структура которой аналогична приведенной выше. Я хочу абстрагировать задачу 3 в функцию, bar()но я хочу ограничить доступ этой функции только в рамках foo(a).

Правильно ли переходить к следующему, чтобы достичь того, чего я хочу?

function foo(a) {
    function bar() {
        // Perform task 3
    }

    if (/* Some condition */) {
        // Perform task 1
        bar();
    }
    else {
        // Perform task 2
        bar();
    }
}

Если приведенное выше верно, bar()переопределяется ли каждый раз при foo(a)вызове? (Здесь меня беспокоит потеря ресурсов ЦП.)

Тамаки
источник
1
Попробуйте сами: jsperf.com Полагаю, это зависит от task3.
tomByrer
1
@tomByer - +1 за предложение инструмента
tamakisquare

Ответы:

123

Да, то, что у вас есть, правильно. Некоторые примечания:

  • barсоздается при каждом вызове функции foo, но:
    • В современных браузерах это очень быстрый процесс. (Некоторые движки вполне могут компилировать только код для него только один раз, а затем повторно использовать этот код каждый раз в другом контексте; движок Google V8 [в Chrome и других местах] делает это в большинстве случаев.)
    • И в зависимости от того bar, что происходит, некоторые движки могут определить, что они могут «встроить» его, полностью исключив вызов функции. V8 делает это, и я уверен, что это не единственный двигатель. Естественно, они могут это сделать, только если это не изменит поведение кода.
  • Влияние на производительность, если таковое имеется, от barсоздания каждый раз будет широко варьироваться в зависимости от движка JavaScript. Если barэто тривиально, оно будет варьироваться от необнаружимого до довольно маленького. Если вы не звоните fooтысячи раз подряд (например, из mousemoveобработчика), я бы не стал беспокоиться об этом. Даже если да, я бы беспокоился об этом, только если бы увидел проблему с более медленными двигателями. Вот тестовый пример, связанный с операциями DOM , который предполагает, что влияние есть, но тривиально (вероятно, смыто материалом DOM). Вот тестовый пример, выполняющий чистые вычисления, который показывает гораздо большее влияние, но, честно говоря, даже мы говорим о разнице в микросекунды, потому что даже 92% -ное увеличение того, что требует микросекунды - это еще очень и очень быстро. До тех пор, пока вы не увидите столкновения в реальном мире, не о чем беспокоиться.
  • barбудет доступен только изнутри функции, и он имеет доступ ко всем переменным и аргументам для этого вызова функции. Это делает этот шаблон очень удобным.
  • Обратите внимание, что, поскольку вы использовали объявление функции , не имеет значения, где вы поместите объявление (вверху, внизу или в середине - пока оно находится на верхнем уровне функции, а не внутри оператора управления потоком, который синтаксическая ошибка), он определяется до запуска первой строки пошагового кода.
TJ Crowder
источник
Спасибо за ответ. Так вы говорите, что это незначительное снижение производительности? (учитывая, что копия barсоздается при каждом вызове foo)
tamakisquare
2
@ahmoo: С производительностью JavaScript ответ почти всегда: это зависит от обстоятельств. :-) Это зависит от того, на каком движке он будет работать и как часто вы будете звонить foo. Если вы не звоните fooтысячи раз подряд (например, не в mousemoveобработчике), я бы вообще не беспокоился об этом. И обратите внимание, что некоторые движки (например, V8) все равно встраивают код, полностью исключая вызов функции, при условии, что это не меняет происходящего таким образом, чтобы его можно было обнаружить извне.
TJ Crowder
@TJCrowder: не могли бы вы прокомментировать ответ Робрича? предотвращает ли это решение воссоздание при bar()каждом вызове? Кроме того, поможет ли использование foo.prototype.barдля определения функции?
rkw
4
@rkw: Создание функции один раз, как это делает ответ Робриха, - полезный способ избежать затрат на ее создание при каждом вызове. Вы теряете тот факт, что у вас barесть доступ к переменным и аргументам для вызова foo(все, что вы хотите, чтобы он работал, вы должны передать это), что может немного усложнить ситуацию, но в критической для производительности ситуации, когда вы Если вы заметили реальную проблему, вы можете провести подобный рефакторинг, чтобы увидеть, решит ли это проблему. Нет, использование на foo.prototypeсамом деле не поможет (во-первых, barбольше не будет закрытым).
TJ Crowder
@ahmoo: Добавлен тестовый пример. Интересно, что я получаю другой результат из тестового примера Гуффа, я думаю, что его функция может быть слишком простой. Но я все еще не думаю, что производительность должна быть проблемой.
TJ Crowder
15

Вот для чего нужны закрытия.

var foo = (function () {
  function bar() {
    // perform task 3
  };

  function innerfoo (a) { 
    if (/* some cond */ ) {
      // perform task 1
      bar();
    }
    else {
      // perform task 2
      bar();
    }
  }
  return innerfoo;
})();

Innerfoo (замыкание) содержит ссылку на bar, и только ссылка на innerfoo возвращается из анонимной функции, которая вызывается только один раз для создания замыкания.

Таким образом, бар недоступен снаружи.

Мишель Боркент
источник
1
Интересно. У меня ограниченный доступ к javascript, поэтому закрытие для меня в новинку. Однако вы отметили для меня отправную точку в изучении закрытия. Благодарю.
tamakisquare
Как часто вы используете закрытие для работы с областями действия переменных / функций? Например, если у вас есть 2 функции, которым требуется доступ к одним и тем же трем переменным, не могли бы вы объявить эти 3 переменные в замыкании вместе с 2 функциями, а затем вернуть 2 функции?
doubleOrt
8
var foo = (function () {
    var bar = function () {
        // perform task 3
    }
    return function (a) {

        if (/*some condition*/) {
            // perform task 1
            bar();
        }
        else {
            // perform task 2
            bar();
        }
    };
}());

Замыкание сохраняет область bar()содержания, возвращая новую функцию из самоисполняющейся анонимной функции, устанавливает более видимую область видимости foo(). Анонимная самовыполняющаяся функция запускается ровно один раз, поэтому существует только один bar()экземпляр, и каждое выполнение foo()будет использовать его.

Робрих
источник
Интересно. Тогда я должен поискать закрытие. Благодарю.
tamakisquare
Как часто вы используете закрытие для работы с областями действия переменных / функций? Например, если у вас есть 2 функции, которым требуется доступ к одним и тем же трем переменным, не могли бы вы объявить эти 3 переменные в замыкании вместе с 2 функциями, а затем вернуть 2 функции?
doubleOrt
Как часто я использую закрытие? Все время. Если бы у меня были 3 переменные, необходимые для 2 функций: 1. (лучше всего) передать 3 переменные в обе функции - функции могут быть определены один раз и только один раз. 2. (хорошо) создать 2 функции, в которых переменные выходят за рамки обеих. Это закрытие. (В основном ответ здесь.) К сожалению, функции переопределяются для каждого нового использования переменных. 3. (плохо) не используйте функции, просто используйте один большой длинный метод, выполняющий обе задачи.
robrich
5

Да, отлично работает.

Внутренняя функция не воссоздается каждый раз, когда вы вводите внешнюю функцию, но она переназначается.

Если вы протестируете этот код:

function test() {

    function demo() { alert('1'); }

    demo();
    demo = function() { alert('2'); };
    demo();

}

test();
test();

она покажет 1, 2, 1, 2, не 1, 2, 2, 2.

Гуффа
источник
Спасибо за Ваш ответ. Следует ли называть переназначение demo()каждого времени test()проблемой производительности? Это зависит от сложности demo()?
tamakisquare
1
Я провел тест производительности: jsperf.com/inner-function-vs-global-function Вывод таков : обычно это не проблема производительности (поскольку любой код, который вы добавляете в функции, будет выполняться намного дольше, чем создание функции сам), но если вам понадобится это дополнительное преимущество в производительности, вам придется писать другой код для разных браузеров.
Guffa
Спасибо за то, что потратили время на создание теста, а также за то, что поделился своими оценками по производительности. Очень признателен.
tamakisquare
Вы с большой уверенностью сказали, что «внутренняя функция не воссоздается каждый раз». Согласно спецификации , это так; оптимизирует ли двигатель это, зависит от двигателя. (Я ожидаю, что большинство будет.) Я заинтригован, увидев, что ваш и мой тестовый пример имеют такие разные результаты: jsperf.com/cost-of-creating-inner-function Однако я не думаю, что производительность является проблемой.
TJ Crowder
@TJCrowder: Ну, да, это деталь реализации, но поскольку современные движки Javascript компилируют код, он не будет перекомпилировать функцию каждый раз, когда она назначается. Причина, по которой результаты тестов производительности различаются, заключается в том, что они тестируют разные вещи. Мой тест сравнивает глобальные функции с локальными функциями, а ваш тест сравнивает локальную функцию с встроенным кодом. Встраивание кода, конечно, будет быстрее, чем вызов функции, это распространенный метод оптимизации.
Guffa
0

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

https://jsperf.com/nested-functions-vs-not-nested-2/1

Это в Chrome 76, macOS.

Майкл Ликури
источник