Appeler une fonction javascript de manière récursive

90

Je peux créer une fonction récursive dans une variable comme ceci:

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

Avec cela, functionHolder(3);sortirait 3 2 1 0. Disons que j'ai fait ce qui suit:

var copyFunction = functionHolder;

copyFunction(3);produirait 3 2 1 0comme ci-dessus. Si j'ai ensuite changé functionHoldercomme suit:

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

Puis functionHolder(3);donnerait Stop counting!, comme prévu.

copyFunction(3);donne maintenant 3 Stop counting!comme il se réfère functionHolder, pas la fonction (à laquelle il pointe lui-même). Cela pourrait être souhaitable dans certaines circonstances, mais y a-t-il un moyen d'écrire la fonction de sorte qu'elle s'appelle elle-même plutôt que la variable qui la contient?

Autrement dit, est-il possible de changer uniquement la ligne de functionHolder(counter-1);sorte que passer par toutes ces étapes donne encore 3 2 1 0lorsque nous appelons copyFunction(3);? J'ai essayé this(counter-1);mais cela me donne l'erreur this is not a function.

Samthere
la source
1
NB A l'intérieur d'une fonction, cela fait référence au contexte d'exécution de la fonction, pas à la fonction elle-même. Dans votre cas, cela pointait probablement vers l'objet de fenêtre global.
antoine

Réponses:

146

Utilisation d'expressions de fonction nommées:

Vous pouvez donner à une expression de fonction un nom qui est réellement privé et qui n'est visible que de l'intérieur de la fonction si elle-même:

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

Ici myselfn'est visible qu'à l'intérieur de la fonction elle-même.

Vous pouvez utiliser ce nom privé pour appeler la fonction de manière récursive.

Voir 13. Function Definitionla spécification ECMAScript 5:

L'identificateur dans une FunctionExpression peut être référencé à partir de FunctionBody de FunctionExpression pour permettre à la fonction de s'appeler de manière récursive. Toutefois, contrairement à une FunctionDeclaration, l'identificateur d'une FunctionExpression ne peut pas être référencé à partir de et n'affecte pas la portée englobant FunctionExpression.

Veuillez noter qu'Internet Explorer jusqu'à la version 8 ne se comporte pas correctement car le nom est en fait visible dans l'environnement de la variable englobante, et il fait référence à un double de la fonction réelle (voir le commentaire de patrick dw ci-dessous).

En utilisant arguments.callee:

Vous pouvez également utiliser arguments.calleepour faire référence à la fonction actuelle:

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

La 5ème édition d'ECMAScript interdit l'utilisation d'arguments.callee () en mode strict , cependant:

(De MDN ): Dans le code normal arguments.callee fait référence à la fonction englobante. Ce cas d'utilisation est faible: nommez simplement la fonction englobante! De plus, arguments.callee gêne considérablement les optimisations comme les fonctions inlining, car il doit être possible de fournir une référence à la fonction non-inline si l'on accède à arguments.callee. arguments.callee pour les fonctions en mode strict est une propriété non supprimable qui se déclenche lorsqu'elle est définie ou récupérée.

Arnaud Le Blanc
la source
4
+1 Bien que ce soit un peu bogué dans IE8 et les versions inférieures, cela myselfest en fait visible dans l'environnement de variable englobante, et il fait référence à un double de la myselffonction réelle . Vous devriez pouvoir définir la référence externe sur nullcependant.
user113716
Merci d'avoir répondu! Les deux ont été utiles et ont résolu le problème de deux manières différentes. En fin de compte, j'ai décidé au hasard lequel accepter: P
Samthere
juste pour moi de comprendre. Quelle est la raison de multiplier la fonction à chaque retour? return n * myself(n-1);?
chitzui
pourquoi la fonction fonctionne comme ça? jsfiddle.net/jvL5euho/18 après avoir bouclé 4 fois.
Prashant Tapase
Selon certaines références, arguments.callee ne fonctionnera pas en mode strict.
Krunal Limbad
10

Vous pouvez accéder à la fonction elle-même en utilisant arguments.callee [MDN] :

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

Cependant, cela cassera en mode strict.

Félix Kling
la source
6
Je crois que c'est obsolète (et non autorisé en mode strict)
Arnaud Le Blanc
@Felix: Oui, le "mode strict" donnera un TypeError, mais je n'ai rien trouvé qui déclare officiellement que arguments.callee (ou toute violation du mode strict) est obsolète en dehors du "mode strict".
user113716
Merci d'avoir répondu! Les deux ont été utiles et ont résolu le problème de deux manières différentes. En fin de compte, j'ai décidé au hasard lequel accepter: P
Samthere
6

Vous pouvez utiliser le combinateur 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)));

Et vous pouvez l'utiliser comme ceci:

// 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ò
la source
5

Je sais que c'est une vieille question, mais j'ai pensé présenter une autre solution qui pourrait être utilisée si vous souhaitez éviter d'utiliser des expressions de fonction nommées. (Ne pas dire que vous devriez ou ne devriez pas les éviter, simplement présenter une autre solution)

  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
la source
3

Voici un exemple très simple:

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

Notez que le countercompte «à rebours» par rapport à slugla valeur de. Cela est dû à la position à laquelle nous enregistrons ces valeurs, car la fonction se reproduit avant la journalisation - donc, nous continuons essentiellement à nous imbriquer de plus en plus profondément dans la pile d'appels avant la journalisation.

Une fois que la récursion rencontre l'élément final de la pile d'appels, elle trampoline "hors" des appels de fonction, tandis que le premier incrément de counterse produit à l'intérieur du dernier appel imbriqué.

Je sais que ce n'est pas un «correctif» sur le code du questionneur, mais étant donné le titre, j'ai pensé que je donnerais un exemple générique de récursion pour une meilleure compréhension de la récursivité, carrément.

Cody
la source