Rekursives Aufrufen einer Javascript-Funktion

90

Ich kann eine rekursive Funktion in einer Variablen wie folgt erstellen:

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

Damit functionHolder(3);würde ausgegeben 3 2 1 0. Angenommen, ich habe Folgendes getan:

var copyFunction = functionHolder;

copyFunction(3);würde 3 2 1 0wie oben ausgeben . Wenn ich mich dann functionHolderwie folgt geändert habe :

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

Dann functionHolder(3);würde Stop counting!wie erwartet geben.

copyFunction(3);Jetzt gibt es, 3 Stop counting!wie es sich bezieht functionHolder, nicht die Funktion (auf die es selbst zeigt). Dies könnte unter bestimmten Umständen wünschenswert sein, aber gibt es eine Möglichkeit, die Funktion so zu schreiben, dass sie sich selbst aufruft und nicht die Variable, die sie enthält?

Das heißt, ist es möglich, nur die Leitung functionHolder(counter-1);so zu ändern , dass das Durchlaufen all dieser Schritte immer noch ergibt, 3 2 1 0wenn wir anrufen copyFunction(3);? Ich habe es versucht, this(counter-1);aber das gibt mir den Fehler this is not a function.

Samthere
quelle
1
NB Innerhalb einer Funktion bezieht sich dies auf den Kontext der Ausführung der Funktion, nicht auf die Funktion selbst. In Ihrem Fall zeigte dies wahrscheinlich auf das globale Fensterobjekt.
Antoine

Antworten:

145

Verwenden benannter Funktionsausdrücke:

Sie können einem Funktionsausdruck einen Namen geben, der tatsächlich privat ist und nur innerhalb der Funktion sichtbar ist, wenn Sie selbst:

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

Hier myselfist nur innerhalb der Funktion selbst sichtbar .

Mit diesem privaten Namen können Sie die Funktion rekursiv aufrufen.

Siehe 13. Function DefinitionECMAScript 5-Spezifikation:

Der Bezeichner in einer FunctionExpression kann im FunctionBody der FunctionExpression referenziert werden, damit sich die Funktion rekursiv aufrufen kann. Im Gegensatz zu einer FunctionDeclaration kann der Bezeichner in einer FunctionExpression jedoch nicht referenziert werden und hat keinen Einfluss auf den Bereich, der die FunctionExpression einschließt.

Bitte beachten Sie, dass sich Internet Explorer bis Version 8 nicht korrekt verhält, da der Name tatsächlich in der umgebenden Variablenumgebung sichtbar ist und auf ein Duplikat der eigentlichen Funktion verweist (siehe den Kommentar von Patrick Dw unten).

Argumente verwenden.callee:

Alternativ können Sie arguments.calleeauf die aktuelle Funktion verweisen:

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

Die 5. Ausgabe von ECMAScript verbietet jedoch die Verwendung von Argumenten.callee () im strengen Modus :

(Von MDN ): In normalen Codeargumenten bezieht sich callee auf die einschließende Funktion. Dieser Anwendungsfall ist schwach: Benennen Sie einfach die umschließende Funktion! Darüber hinaus behindertargument.callee Optimierungen wie Inlining-Funktionen erheblich, da es möglich sein muss, einen Verweis auf die nicht inlinierte Funktion bereitzustellen, wenn auf argument.callee zugegriffen wird. Argumente.callee für Funktionen im strengen Modus ist eine nicht löschbare Eigenschaft, die beim Setzen oder Abrufen ausgelöst wird.

Arnaud Le Blanc
quelle
4
+1 Obwohl es in IE8 und niedriger ein wenig fehlerhaft myselfist , ist es tatsächlich in der umgebenden Variablenumgebung sichtbar und verweist auf ein Duplikat der eigentlichen myselfFunktion. Sie sollten jedoch in der Lage sein, die äußere Referenz nullfestzulegen.
user113716
Danke für die Antwort! Beide waren hilfreich und lösten das Problem auf zwei verschiedene Arten. Am Ende habe ich zufällig entschieden, was ich akzeptieren soll: P
Samthere
nur damit ich es verstehe. Was ist der Grund für die Multiplikation der Funktion bei jeder Rückgabe? return n * myself(n-1);?
Chitzui
Warum funktioniert die Funktion so? jsfiddle.net/jvL5euho/18 nach einer 4- maligen Schleife.
Prashant Tapase
Gemäß einigen Referenzargumenten funktioniert callee nicht im strengen Modus.
Krunal Limbad
10

Sie können mit arguments.callee [MDN] auf die Funktion selbst zugreifen :

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

Dies wird jedoch im strengen Modus unterbrochen.

Felix Kling
quelle
6
Ich glaube, dass dies veraltet ist (und im strengen Modus nicht erlaubt ist)
Arnaud Le Blanc
@Felix: Ja, "strenger Modus" gibt ein TypeError, aber ich habe nichts gefunden, was offiziell besagt, dass arguments.callee (oder eine Verletzung des strengen Modus ) außerhalb des "strengen Modus" veraltet ist.
user113716
Danke für die Antwort! Beide waren hilfreich und lösten das Problem auf zwei verschiedene Arten. Am Ende habe ich zufällig entschieden, was ich akzeptieren soll: P
Samthere
6

Sie können den Y-Kombinator verwenden: ( 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)));

Und Sie können es so verwenden:

// 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ò
quelle
5

Ich weiß, dass dies eine alte Frage ist, aber ich dachte, ich würde eine weitere Lösung vorstellen, die verwendet werden könnte, wenn Sie die Verwendung benannter Funktionsausdrücke vermeiden möchten. (Nicht sagen, dass Sie sie vermeiden sollten oder nicht, sondern nur eine andere Lösung präsentieren)

  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
quelle
3

Hier ist ein sehr einfaches Beispiel:

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);

Beachten Sie, dass die counterAnzahl "rückwärts" in Bezug auf slugden Wert zählt. Dies liegt an der Position, an der wir diese Werte protokollieren, da die Funktion vor der Protokollierung wiederholt wird. Daher verschachteln wir uns im Wesentlichen immer tiefer im Aufrufstapel, bevor die Protokollierung stattfindet.

Sobald die Rekursion das letzte Call-Stack-Element erreicht, trampoliert sie "out" aus den Funktionsaufrufen, während das erste Inkrement von counterinnerhalb des letzten verschachtelten Aufrufs auftritt.

Ich weiß, dass dies keine "Korrektur" des Codes des Fragestellers ist, aber angesichts des Titels dachte ich, ich würde Rekursion generisch veranschaulichen , um die Rekursion direkt besser zu verstehen.

Cody
quelle