Llamar a una función de javascript de forma recursiva

90

Puedo crear una función recursiva en una variable así:

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

Con esto, functionHolder(3);saldría 3 2 1 0. Digamos que hice lo siguiente:

var copyFunction = functionHolder;

copyFunction(3);saldría 3 2 1 0como arriba. Si luego cambié de la functionHoldersiguiente manera:

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

Entonces functionHolder(3);daría Stop counting!, como se esperaba.

copyFunction(3);ahora da lo 3 Stop counting!que se refiere functionHolder, no la función (a la que él mismo apunta). Esto podría ser deseable en algunas circunstancias, pero ¿hay alguna forma de escribir la función para que se llame a sí misma en lugar de a la variable que la contiene?

Es decir, ¿es posible cambiar solo la línea functionHolder(counter-1);para que seguir todos estos pasos aún dé 3 2 1 0cuando llamemos copyFunction(3);? Lo intenté this(counter-1);pero eso me da el error this is not a function.

Samthere
fuente
1
NB Dentro de una función, esto se refiere al contexto de ejecución de la función, no a la función en sí. En su caso, esto probablemente apuntaba al objeto de ventana global.
antoine

Respuestas:

146

Uso de expresiones de función con nombre:

Puede darle a una expresión de función un nombre que sea realmente privado y solo sea visible desde el interior de la función ifself:

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

Aquí myselfes visible solo dentro de la función en sí.

Puede utilizar este nombre privado para llamar a la función de forma recursiva.

Consulte 13. Function Definitionla especificación ECMAScript 5:

Se puede hacer referencia al identificador en una FunctionExpression desde dentro del FunctionBody de FunctionExpression para permitir que la función se llame a sí misma de forma recursiva. Sin embargo, a diferencia de una FunctionDeclaration, el identificador en una FunctionExpression no puede ser referenciado desde y no afecta el ámbito que encierra la FunctionExpression.

Tenga en cuenta que Internet Explorer hasta la versión 8 no se comporta correctamente ya que el nombre es realmente visible en el entorno de la variable adjunta y hace referencia a un duplicado de la función real (consulte el comentario de patrick dw a continuación).

Usando argumentos.callee:

Alternativamente, puede usar arguments.calleepara referirse a la función actual:

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

La quinta edición de ECMAScript prohíbe el uso de argumentos.callee () en modo estricto , sin embargo:

(De MDN ): En el código normal, los argumentos, la llamada se refiere a la función adjunta. Este caso de uso es débil: simplemente nombre la función adjunta. Además, argument.callee obstaculiza sustancialmente las optimizaciones como las funciones en línea, porque debe ser posible proporcionar una referencia a la función no en línea si se accede a argumentos.callee. argumentos.callee para funciones de modo estricto es una propiedad no borrable que se lanza cuando se establece o recupera.

Arnaud Le Blanc
fuente
4
+1 Aunque tiene un pequeño error en IE8 y es inferior, myselfes realmente visible en el entorno de la variable adjunta, y hace referencia a un duplicado de la myselffunción real . Sin nullembargo, debería poder establecer la referencia externa a .
user113716
¡Gracias por la respuesta! Ambos fueron útiles y resolvieron el problema de 2 formas diferentes. Al final, decidí al azar cuál aceptar: P
Samthere
solo para que yo lo entienda. ¿Cuál es la razón detrás de multiplicar la función en cada devolución? return n * myself(n-1);?
chitzui
¿Por qué la función funciona así? jsfiddle.net/jvL5euho/18 después si se repite 4 veces.
Prashant Tapase
Según algunos argumentos de referencia, callee no funcionará en modo estricto.
Krunal Limbad
10

Puede acceder a la función en sí usando arguments.callee [MDN] :

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

Sin embargo, esto se romperá en modo estricto.

Felix Kling
fuente
6
Creo que esto está en desuso (y no se permite en modo estricto)
Arnaud Le Blanc
@Felix: Sí, el "modo estricto" dará un TypeError, pero no he encontrado nada que indique oficialmente que arguments.callee (o cualquier violación del modo estricto) está desaprobado fuera del "modo estricto".
user113716
¡Gracias por la respuesta! Ambos fueron útiles y resolvieron el problema de 2 formas diferentes. Al final, decidí al azar cuál aceptar: P
Samthere
6

Puede utilizar el combinador 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)));

Y puedes usarlo así:

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

Sé que esta es una pregunta antigua, pero pensé en presentar una solución más que podría usarse si desea evitar el uso de expresiones de función con nombre. (No digo que debas o no debes evitarlos, solo presenta otra solución)

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

Aquí hay un ejemplo muy 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);

Observe que la countercuenta "al revés" con respecto a cuál sluges el valor. Esto se debe a la posición en la que estamos registrando estos valores, ya que la función se repite antes del registro; por lo tanto, esencialmente seguimos anidando cada vez más profundamente en la pila de llamadas antes de que se realice el registro.

Una vez que la recursividad se encuentra con el último punto pila de llamadas, que trampolines "fuera" de las llamadas de función, mientras que, el primer incremento de counterproduce en el interior de la última llamada anidada.

Sé que esto no es una "solución" en el código del Interlocutor, pero dado el título, pensé que ejemplificaría genéricamente la recursividad para una mejor comprensión de la recursividad, directamente.

Cody
fuente