Chiamare una funzione javascript in modo ricorsivo


90

Posso creare una funzione ricorsiva in una variabile in questo modo:

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

Con questo, functionHolder(3);produrrebbe 3 2 1 0. Diciamo che ho fatto quanto segue:

var copyFunction = functionHolder;

copyFunction(3);risulterebbe 3 2 1 0come sopra. Se poi ho cambiato functionHoldercome segue:

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

Quindi functionHolder(3);darebbe Stop counting!, come previsto.

copyFunction(3);ora dà 3 Stop counting!come si riferisce functionHolder, non la funzione (che essa stessa indica). Ciò potrebbe essere desiderabile in alcune circostanze, ma esiste un modo per scrivere la funzione in modo che richiami se stessa anziché la variabile che la contiene?

Cioè, è possibile cambiare solo la linea in functionHolder(counter-1);modo che il passaggio attraverso tutti questi passaggi dia ancora 3 2 1 0quando chiamiamo copyFunction(3);? Ho provato this(counter-1);ma questo mi dà l'errore this is not a function.


1
NB All'interno di una funzione, si riferisce al contesto di esecuzione della funzione, non alla funzione stessa. Nel tuo caso probabilmente questo puntava all'oggetto finestra globale.
antoine

Risposte:


146

Utilizzo di espressioni di funzione denominate:

È possibile assegnare a un'espressione di funzione un nome che sia effettivamente privato ed è visibile solo dall'interno della funzione se stesso:

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

Qui myselfè visibile solo all'interno della funzione stessa.

È possibile utilizzare questo nome privato per chiamare la funzione in modo ricorsivo.

Vedi 13. Function Definitionla specifica ECMAScript 5:

È possibile fare riferimento all'identificatore in una FunctionExpression dall'interno del FunctionBody di FunctionExpression per consentire alla funzione di chiamare se stessa in modo ricorsivo. Tuttavia, a differenza di una FunctionDeclaration, non è possibile fare riferimento all'identificatore in una FunctionExpression e non influisce sull'ambito che racchiude FunctionExpression.

Si noti che Internet Explorer fino alla versione 8 non si comporta correttamente poiché il nome è effettivamente visibile nell'ambiente della variabile che lo racchiude e fa riferimento a un duplicato della funzione effettiva (vedere il commento di patrick dw di seguito).

Utilizzando arguments.callee:

In alternativa puoi usare arguments.calleeper fare riferimento alla funzione corrente:

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

La 5a edizione di ECMAScript proibisce l'uso di arguments.callee () in modalità rigorosa , tuttavia:

(Da MDN ): nel codice normale arguments.callee si riferisce alla funzione che lo racchiude. Questo caso d'uso è debole: nomina semplicemente la funzione che lo racchiude! Inoltre, arguments.callee ostacola sostanzialmente le ottimizzazioni come le funzioni inlining, perché deve essere possibile fornire un riferimento alla funzione non inlined se si accede a arguments.callee. arguments.callee per le funzioni in modalità rigorosa è una proprietà non cancellabile che genera quando impostata o recuperata.


4
+1 Anche se è un po 'bacato in IE8 e inferiore in quanto myselfè effettivamente visibile nell'ambiente della variabile che lo racchiude e fa riferimento a un duplicato della myselffunzione effettiva . Dovresti essere in grado di impostare il riferimento esterno su null.
user113716

Grazie per la risposta! Entrambi sono stati utili e hanno risolto il problema, in 2 modi diversi. Alla fine ho deciso a caso quale accettare: P
Samthere

solo per farmi capire. Qual è la ragione dietro la moltiplicazione della funzione su ogni rendimento? return n * myself(n-1);?
chitzui

perché la funzione funziona in questo modo? jsfiddle.net/jvL5euho/18 dopo se ripetuto 4 volte.
Prashant Tapase

Secondo alcuni argomenti di riferimento, allevamento non funzionerà in modalità rigorosa.
Krunal Limbad

10

È possibile accedere alla funzione stessa utilizzando arguments.callee [MDN] :

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

Tuttavia, questo si interromperà in modalità rigorosa.


6
Credo che questo sia deprecato (e non consentito in modalità rigorosa)
Arnaud Le Blanc

@Felix: Sì, "modalità rigorosa" darà un TypeError, ma non ho trovato nulla che dichiari ufficialmente che arguments.callee (o qualsiasi violazione della modalità rigorosa) sia deprecata al di fuori della "modalità rigorosa".
user113716

Grazie per la risposta! Entrambi sono stati utili e hanno risolto il problema, in 2 modi diversi. Alla fine ho deciso a caso quale accettare: P
Samthere

6

Puoi usare il combinatore 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)));

E puoi usarlo come questo:

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

5

So che questa è una vecchia domanda, ma ho pensato di presentare un'altra soluzione che potrebbe essere utilizzata se desideri evitare di utilizzare espressioni di funzione con nome. (Non dicendo che dovresti o non dovresti evitarli, ma solo presentare un'altra soluzione)

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

3

Ecco un esempio molto semplice:

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

Si noti che i counterconteggi "all'indietro" rispetto a quale slugsia il valore. Ciò è dovuto alla posizione in cui stiamo registrando questi valori, poiché la funzione ricorre prima della registrazione, quindi essenzialmente continuiamo a nidificare sempre più in profondità nello stack di chiamate prima che la registrazione abbia luogo.

Una volta che la ricorsione incontra l'elemento finale dello stack di chiamate, trampolino "fuori" dalle chiamate di funzione, mentre il primo incremento dicounter verifica all'interno dell'ultima chiamata annidata.

So che questa non è una "correzione" del codice del Questionario, ma dato il titolo ho pensato di esemplificare genericamente la ricorsione per una migliore comprensione della ricorsione, a titolo definitivo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.