Wywołanie rekurencyjne funkcji javascript

90

Mogę utworzyć funkcję rekurencyjną w zmiennej takiej jak ta:

/* Count down to 0 recursively.
 */
var functionHolder = function (counter) {
    output(counter);
    if (counter > 0) {
        functionHolder(counter-1);
    }
}

Dzięki temu functionHolder(3);wyjdzie 3 2 1 0. Powiedzmy, że wykonałem następujące czynności:

var copyFunction = functionHolder;

copyFunction(3);wyświetli się 3 2 1 0jak powyżej. Jeśli potem zmieniłem functionHolderw następujący sposób:

functionHolder = function(whatever) {
    output("Stop counting!");

Wtedy functionHolder(3);dał Stop counting!, zgodnie z oczekiwaniami.

copyFunction(3);teraz podaje 3 Stop counting!to, do czego się odnosi functionHolder, a nie funkcję (na którą sama wskazuje). Może to być pożądane w pewnych okolicznościach, ale czy istnieje sposób na napisanie funkcji tak, aby wywoływała samą siebie, a nie zmienną, która ją przechowuje?

To znaczy, czy można zmienić tylko linię functionHolder(counter-1);, aby przejście przez wszystkie te kroki nadal dawało, 3 2 1 0gdy dzwonimy copyFunction(3);? Próbowałem, this(counter-1);ale to daje mi błąd this is not a function.

Samthere
źródło
1
Uwaga: Wewnątrz funkcji odnosi się to do kontekstu wykonania funkcji, a nie samej funkcji. W twoim przypadku prawdopodobnie wskazywało to na globalny obiekt okna.
antoine

Odpowiedzi:

146

Używanie nazwanych wyrażeń funkcyjnych:

Możesz nadać wyrażeniu funkcyjnemu nazwę, która jest faktycznie prywatna i jest widoczna tylko z wnętrza funkcji, jeśli siebie:

var factorial = function myself (n) {
    if (n <= 1) {
        return 1;
    }
    return n * myself(n-1);
}
typeof myself === 'undefined'

Tutaj myselfjest widoczny tylko wewnątrz samej funkcji .

Możesz użyć tej prywatnej nazwy do rekurencyjnego wywołania funkcji.

Zobacz 13. Function Definitionspecyfikację ECMAScript 5:

Do identyfikatora w FunctionExpression można odwoływać się z wnętrza FunctionBody FunctionExpression, aby umożliwić funkcji rekurencyjne wywoływanie samej siebie. Jednak w przeciwieństwie do FunctionDeclaration nie można odwoływać się do identyfikatora w FunctionExpression i nie ma to wpływu na zakres obejmujący FunctionExpression.

Należy zauważyć, że Internet Explorer do wersji 8 nie zachowuje się poprawnie, ponieważ nazwa jest faktycznie widoczna w otaczającym środowisku zmiennych i odwołuje się do duplikatu rzeczywistej funkcji (patrz komentarz patryka dw poniżej).

Korzystanie arguments.callee:

Alternatywnie możesz użyć, arguments.calleeaby odnieść się do bieżącej funkcji:

var factorial = function (n) {
    if (n <= 1) {
        return 1;
    }
    return n * arguments.callee(n-1);
}

Piąta edycja ECMAScript zabrania używania arguments.callee () w trybie ścisłym , jednakże:

(Z MDN ): W normalnym kodzie arguments.callee odnosi się do otaczającej funkcji. Ten przypadek użycia jest słaby: po prostu nazwij funkcję zamykającą! Ponadto arguments.callee znacznie utrudnia optymalizacje, takie jak funkcje wstawiane, ponieważ musi być możliwe udostępnienie odwołania do funkcji niewymienionej, jeśli uzyskuje się dostęp do arguments.callee. arguments.callee dla funkcji trybu ścisłego to nieusuwalna właściwość, która jest zgłaszana podczas ustawiania lub pobierania.

Arnaud Le Blanc
źródło
4
+1 Chociaż jest trochę błędny w IE8 i niższych, myselfjest faktycznie widoczny w otaczającym środowisku zmiennych i odwołuje się do duplikatu rzeczywistej myselffunkcji. Powinieneś być w stanie ustawić jednak odniesienie zewnętrzne null.
user113716
Dziękuję za odpowiedź! Obaj byli pomocni i rozwiązali problem na 2 różne sposoby. W końcu losowo zdecydowałem, które zaakceptuję: P
Samthere
tylko dla mnie do zrozumienia. Jaki jest powód mnożenia funkcji na każdym powrocie? return n * myself(n-1);?
chitzui,
dlaczego funkcja działa w ten sposób? jsfiddle.net/jvL5euho/18 after if zapętla 4 razy.
Prashant Tapase
Zgodnie z niektórymi argumentami referencyjnymi. Callee nie będzie działać w trybie ścisłym.
Krunal Limbad
10

Możesz uzyskać dostęp do samej funkcji za pomocą arguments.callee [MDN] :

if (counter>0) {
    arguments.callee(counter-1);
}

Spowoduje to jednak przerwanie w trybie ścisłym.

Felix Kling
źródło
6
Uważam, że jest to przestarzałe (i niedozwolone w trybie ścisłym)
Arnaud Le Blanc
@Felix: Tak, „tryb ścisły” da TypeError, ale nie znalazłem niczego, co oficjalnie stwierdza, że arguments.callee (lub jakiekolwiek naruszenie trybu ścisłego) jest przestarzałe poza „trybem ścisłym”.
user113716
Dziękuję za odpowiedź! Obaj byli pomocni i rozwiązali problem na 2 różne sposoby. W końcu losowo zdecydowałem, które zaakceptuję: P
Samthere
6

Możesz użyć kombinatora Y: ( Wikipedia )

// ES5 syntax
var Y = function Y(a) {
  return (function (a) {
    return a(a);
  })(function (b) {
    return a(function (a) {
      return b(b)(a);
    });
  });
};

// ES6 syntax
const Y = a=>(a=>a(a))(b=>a(a=>b(b)(a)));

// If the function accepts more than one parameter:
const Y = a=>(a=>a(a))(b=>a((...a)=>b(b)(...a)));

Możesz go użyć w ten sposób:

// ES5
var fn = Y(function(fn) {
  return function(counter) {
    console.log(counter);
    if (counter > 0) {
      fn(counter - 1);
    }
  }
});

// ES6
const fn = Y(fn => counter => {
  console.log(counter);
  if (counter > 0) {
    fn(counter - 1);
  }
});
Nicolò
źródło
5

Wiem, że to stare pytanie, ale pomyślałem, że przedstawię jeszcze jedno rozwiązanie, którego można by użyć, gdybyś chciał uniknąć używania nazwanych wyrażeń funkcyjnych. (Nie mówię, że powinieneś lub nie powinieneś ich unikać, po prostu przedstawiając inne rozwiązanie)

  var fn = (function() {
    var innerFn = function(counter) {
      console.log(counter);

      if(counter > 0) {
        innerFn(counter-1);
      }
    };

    return innerFn;
  })();

  console.log("running fn");
  fn(3);

  var copyFn = fn;

  console.log("running copyFn");
  copyFn(3);

  fn = function() { console.log("done"); };

  console.log("fn after reassignment");
  fn(3);

  console.log("copyFn after reassignment of fn");
  copyFn(3);
Sandro
źródło
3

Oto jeden bardzo prosty przykład:

var counter = 0;

function getSlug(tokens) {
    var slug = '';

    if (!!tokens.length) {
        slug = tokens.shift();
        slug = slug.toLowerCase();
        slug += getSlug(tokens);

        counter += 1;
        console.log('THE SLUG ELEMENT IS: %s, counter is: %s', slug, counter);
    }

    return slug;
}

var mySlug = getSlug(['This', 'Is', 'My', 'Slug']);
console.log('THE SLUG IS: %s', mySlug);

Zwróć uwagę, że counterliczy się „wstecz” w odniesieniu do tego slug, jaka jest wartość. Dzieje się tak z powodu pozycji, w której rejestrujemy te wartości, ponieważ funkcja powtarza się przed rejestrowaniem - więc zasadniczo zagnieżdżamy się głębiej i głębiej w stosie wywołań, zanim nastąpi rejestracja.

Gdy rekursja dotrze do ostatniego elementu stosu wywołań, trampoliny „wychodzą” z wywołań funkcji, podczas gdy pierwsza inkrementacja counterwystępuje w ostatnim zagnieżdżonym wywołaniu.

Wiem, że to nie jest "poprawka" w kodzie Pytającego, ale biorąc pod uwagę tytuł, pomyślałem, że generalnie zilustruję Rekursję, aby lepiej zrozumieć rekurencję.

Cody
źródło