web-dev-qa-db-fra.com

Appeler une fonction javascript de manière récursive

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 ceci, functionHolder(3); produirait 3210. Disons que j'ai fait ce qui suit:

var copyFunction = functionHolder;

copyFunction(3); afficherait 3210 comme ci-dessus. Si j'ai ensuite changé functionHolder comme suit:

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

Alors, functionHolder(3); donnerait Stop counting!, Comme prévu.

copyFunction(3); donne maintenant 3Stop counting! car il fait référence à functionHolder, pas à la fonction (à laquelle il renvoie lui-même). Cela peut être souhaitable dans certaines circonstances, mais existe-t-il un moyen d'écrire la fonction pour qu'elle s'appelle elle-même plutôt que la variable qui la contient?

Autrement dit, est-il possible de changer niquement la ligne functionHolder(counter-1); afin que toutes ces étapes donnent encore 3210 Lorsque nous appelons copyFunction(3);? J'ai essayé this(counter-1); mais cela me donne l'erreur this is not a function.

83
Samthere

Utilisation des expressions de fonction nommées:

Vous pouvez attribuer à une expression de fonction un nom réellement privé et visible uniquement à l'intérieur de la fonction si

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

Ici, myself est visible uniquement à 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 Definition de la spécification ECMAScript 5:

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

Veuillez noter qu’Internet Explorer jusqu’à la version 8 ne se comporte pas correctement car son nom est réellement visible dans l’environnement variable englobant et renvoie à un duplicata de la fonction réelle (voir patrick dw commentaires ci-dessous).

En utilisant arguments.callee:

Sinon, vous pouvez utiliser arguments.callee pour 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 de ECMAScript interdit l’utilisation de arguments.callee () dans mode strict , cependant:

(De MDN ): En 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 entrave considérablement les optimisations telles que les fonctions en ligne, car il doit être possible de fournir une référence à la fonction non en ligne si l'on accède à arguments.callee. arguments.callee pour les fonctions en mode strict est une propriété non supprimable qui est renvoyée lorsqu'elle est définie ou extraite.

140
Arnaud Le Blanc

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

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

Cela va casser en mode strict, cependant.

9
Felix Kling

Je sais que la question est ancienne, mais je pensais présenter une solution supplémentaire qui pourrait être utilisée si vous ne souhaitez pas utiliser les expressions de fonction nommée. (Ne pas dire que vous devriez ou ne pas les éviter, mais 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);
5
Sandro

Vous pouvez utiliser le combineur 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);
  }
});
4
Nicolò

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 counter compte "à rebours" en ce qui concerne la valeur de slug. Ceci est dû à la position à laquelle nous enregistrons ces valeurs, car la fonction se répète avant d’enregistrer, nous continuons donc à imbriquer de plus en plus profondément dans la pile d'appels avant la journalisation a lieu.

Une fois que la récursivité rencontre le dernier élément de la pile d’appel, il trampoline "en", alors que le premier incrément de counter se produit à l'intérieur du dernier appel imbriqué.

Je sais que ce n'est pas une "solution" au code du questionneur, mais vu le titre, je pensais que je donnerais un exemple générique Récursivité pour une meilleure compréhension de la récursivité , carrément.

3
Cody