Zdefiniuj funkcję w innej funkcji w JavaScript

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

Mam funkcję, której struktura jest podobna do powyższej. Chcę wyodrębnić zadanie 3 do funkcji, bar()ale chcę ograniczyć dostęp do tej funkcji tylko do zakresu foo(a).

Aby osiągnąć to, czego chcę, czy słuszne jest przejście na następujące?

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

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

Jeśli powyższe jest poprawne, czy za bar()każdym foo(a)wywołaniem jest zmieniane? (Martwię się tutaj o marnowanie zasobów procesora).

tamakisquare
źródło
1
Sprawdź, czy warto samemu: jsperf.com Wyobrażam sobie, że to zależy od task3.
tomByrer
1
@tomByer - +1 za zasugerowanie narzędzia
tamakisquare

Odpowiedzi:

122

Tak, to, co tam masz, jest właściwe. Kilka uwag:

  • barjest tworzony przy każdym wywołaniu funkcji foo, ale:
    • W nowoczesnych przeglądarkach jest to bardzo szybki proces. (Niektóre silniki mogą kompilować tylko kod tylko raz, a następnie za każdym razem ponownie używać tego kodu w innym kontekście; silnik V8 firmy Google [w Chrome i nie tylko] robi to w większości przypadków).
    • W zależności od tego bar, co się dzieje, niektóre silniki mogą określić, że mogą to „wbudować”, całkowicie eliminując wywołanie funkcji. V8 to robi i jestem pewien, że to nie jedyny silnik, który to robi. Oczywiście mogą to zrobić tylko wtedy, gdy nie zmienia to zachowania kodu.
  • Wpływ na wydajność bartworzenia za każdym razem będzie się znacznie różnić w zależności od silnika JavaScript. Jeśli barjest trywialny, będzie się różnić od niewykrywalnego do dość małego. Jeśli nie dzwonisz footysiące razy z rzędu (na przykład od mousemoveobsługi), nie martwiłbym się tym. Nawet jeśli tak, martwiłbym się tylko, gdybym zobaczył problem z wolniejszymi silnikami. Oto przypadek testowy obejmujący operacje DOM , który sugeruje, że jest wpływ, ale trywialny (prawdopodobnie wyparty przez rzeczy DOM). Oto przypadek testowy wykonujący czyste obliczenia, które pokazują znacznie większy wpływ, ale szczerze mówiąc nawet, mówimy o różnicy mikro sekund, ponieważ nawet 92% wzrost w przypadku czegoś, co wymaga mikrosekundy, które mają nastąpić, są nadal bardzo, bardzo szybkie. Dopóki nie zobaczysz rzeczywistego wpływu, nie ma się czym martwić.
  • barbędzie dostępny tylko z poziomu funkcji i będzie miał dostęp do wszystkich zmiennych i argumentów dla tego wywołania funkcji. To sprawia, że ​​jest to bardzo poręczny wzór.
  • Zwróć uwagę, że ponieważ użyłeś deklaracji funkcji , nie ma znaczenia, gdzie umieścisz deklarację (na górze, na dole lub na środku - o ile znajduje się na najwyższym poziomie funkcji, a nie wewnątrz instrukcji sterującej przepływem), która jest błąd składni), zostaje zdefiniowany przed uruchomieniem pierwszej linii kodu krokowego.
TJ Crowder
źródło
Dzięki za odpowiedź. Więc mówisz, że to pomijalny hit wydajnościowy? (biorąc pod uwagę, że kopia barjest tworzona przy każdym wywołaniu foo)
tamakisquare
2
@ahmoo: W przypadku wydajności JavaScript odpowiedź prawie zawsze brzmi: to zależy. :-) To zależy od tego, jaki silnik będzie go uruchamiał i jak często będziesz dzwonić foo. Jeśli nie dzwonisz footysiące razy z rzędu (na przykład nie w programie mousemoveobsługi), nie martwiłbym się tym w ogóle. Zwróć uwagę, że niektóre silniki (na przykład V8) i tak wbudują kod, całkowicie eliminując wywołanie funkcji, pod warunkiem, że nie zmieni to tego, co się dzieje w sposób, który można wykryć zewnętrznie.
TJ Crowder
@TJCrowder: czy możesz skomentować odpowiedź Robricha? czy to rozwiązanie zapobiega odtwarzaniu bar()się każdego połączenia? również, czy użycie foo.prototype.bardo zdefiniowania funkcji pomogłoby każdemu?
rkw
4
@rkw: Jednokrotne utworzenie funkcji, tak jak w przypadku odpowiedzi Robricha, jest użytecznym sposobem na uniknięcie kosztów tworzenia jej przy każdym wywołaniu. Tracisz fakt, że masz bardostęp do zmiennych i argumentów dla wywołania foo(cokolwiek chcesz, żeby działało, musisz to przekazać), co może trochę skomplikować sprawę, ale w krytycznej dla wydajności sytuacji, w której masz Widziałeś rzeczywisty problem, możesz dokonać takiej zmiany, aby zobaczyć, czy to rozwiązuje problem. Nie, używanie foo.prototypenie pomogłoby (po pierwsze, barnie byłoby już prywatne).
TJ Crowder
@ahmoo: Dodano przypadek testowy. Co ciekawe, otrzymuję inny wynik z przypadku testowego Guffy, myślę, że jego funkcja może być trochę za prosta. Ale nadal nie sądzę, żeby wydajność była problemem.
TJ Crowder
15

Po to są zamknięcia.

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 (zamknięcie) przechowuje odniesienie do baru, a anonimowa funkcja, która jest wywoływana tylko raz, aby utworzyć zamknięcie, zwraca tylko odwołanie do innerfoo.

W ten sposób bar nie jest dostępny z zewnątrz.

Michiel Borkent
źródło
1
Ciekawy. Mam ograniczony kontakt z JavaScriptem, więc zamknięcie jest dla mnie czymś nowym. Jednakże wyznaczyłeś mi punkt wyjścia do studiowania zamknięcia. Dzięki.
tamakisquare
Jak często używasz zamknięcia, aby poradzić sobie z zakresami zmiennych / funkcji? Np. Jeśli masz 2 funkcje, które potrzebują dostępu do tych samych 3 zmiennych, czy zadeklarowałbyś 3 zmienne w zamknięciu razem z 2 funkcjami, a następnie zwróciłby 2 funkcje?
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();
        }
    };
}());

Zamknięcie zachowuje zakres bar()zawarty, zwracając nową funkcję z samowykonującej się funkcji anonimowej ustawia bardziej widoczny zakres na foo(). Anonimowa funkcja samowykonująca się jest uruchamiana dokładnie raz, więc jest tylko jedna bar()instancja i każde wykonanie foo()będzie jej używać.

robrich
źródło
Ciekawy. Muszę więc spojrzeć na zamknięcie. Dzięki.
tamakisquare
Jak często używasz zamknięcia, aby poradzić sobie z zakresami zmiennych / funkcji? Np. Jeśli masz 2 funkcje, które potrzebują dostępu do tych samych 3 zmiennych, czy zadeklarowałbyś 3 zmienne w zamknięciu razem z 2 funkcjami, a następnie zwróciłby 2 funkcje?
doubleOrt
Jak często używam domknięć? Cały czas. Gdybym miał 3 zmienne potrzebne 2 funkcjom: 1. (najlepiej) przekazać 3 zmienne do obu funkcji - funkcje można zdefiniować raz i tylko raz. 2. (dobrze) stwórz 2 funkcje, w których zmienne są poza zakresem obu. To jest zamknięcie. (Zasadniczo odpowiedź tutaj.) Niestety, funkcje są redefiniowane przy każdym nowym użyciu zmiennych. 3. (źle) nie używaj funkcji, po prostu użyj jednej dużej, długiej metody wykonującej obie rzeczy.
robrich
5

Tak, to działa dobrze.

Funkcja wewnętrzna nie jest odtwarzana za każdym razem, gdy wchodzisz do funkcji zewnętrznej, ale jest ponownie przypisywana.

Jeśli przetestujesz ten kod:

function test() {

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

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

}

test();
test();

pokaże 1, 2, 1, 2, nie 1, 2, 2, 2.

Guffa
źródło
Dzięki za odpowiedź. Czy zmiana przypisania za demo()każdym razem test()powinna być spowodowana wydajnością? Czy to zależy od złożoności demo()?
tamakisquare
1
Zrobiłem test wydajności: jsperf.com/inner-function-vs-global-function Wniosek jest taki, że generalnie nie jest to problem z wydajnością (ponieważ każdy kod, który umieścisz w funkcjach, będzie działał znacznie dłużej niż utworzenie funkcji sama), ale gdybyś potrzebował dodatkowej wydajności, musiałbyś napisać inny kod dla różnych przeglądarek.
Guffa,
Dzięki za poświęcenie czasu na stworzenie testu i dzielenie się swoimi punktami na temat wydajności. Bardzo cenione.
tamakisquare
Powiedziałeś „funkcja wewnętrzna nie jest odtwarzana za każdym razem” z wielką pewnością. Zgodnie ze specyfikacją jest; to, czy silnik optymalizuje, zależy od silnika. (Oczekuję, że większość będzie.) Zaintrygowało mnie, że Twój przypadek testowy i mój mają tak różne wyniki: jsperf.com/cost-of-creating-inner-function Nie wydaje mi się jednak, że wydajność jest problemem.
TJ Crowder
@TJCrowder: Cóż, tak, to szczegół implementacji, ale ponieważ nowoczesne silniki JavaScript kompilują kod, nie będą rekompilować funkcji za każdym razem, gdy zostanie przypisana. Powodem, dla którego wyniki testów wydajnościowych się różnią, jest to, że testują różne rzeczy. Mój test porównuje funkcje globalne z funkcjami lokalnymi, podczas gdy twój test porównuje funkcję lokalną z wbudowanym kodem. Wbudowanie kodu będzie oczywiście szybsze niż wywołanie funkcji, jest to powszechna technika optymalizacji.
Guffa,
0

Utworzyłem jsperf, aby przetestować zagnieżdżone i niezagnieżdżone wyrażenia funkcyjne i deklaracje funkcji i byłem zaskoczony, widząc, że zagnieżdżone przypadki testowe działały 20 razy szybciej niż niezagnieżdżone. (Spodziewałem się albo przeciwnych, albo nieistotnych różnic).

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

To jest w Chrome 76, macOS.

Michael Liquori
źródło