Chamar uma função javascript recursivamente

90

Posso criar uma função recursiva em uma variável assim:

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

Com isso, functionHolder(3);daria saída 3 2 1 0. Digamos que eu fiz o seguinte:

var copyFunction = functionHolder;

copyFunction(3);produziria 3 2 1 0como acima. Se eu mudar da functionHolderseguinte forma:

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

Então functionHolder(3);daria Stop counting!, conforme o esperado.

copyFunction(3);agora dá 3 Stop counting!como se refere functionHolder, não a função (para a qual ele mesmo aponta). Isso pode ser desejável em algumas circunstâncias, mas há uma maneira de escrever a função de modo que ela se chame a si mesma em vez da variável que a contém?

Ou seja, é possível mudar apenas a linha functionHolder(counter-1);para que passando por todas essas etapas ainda dê 3 2 1 0quando ligamos copyFunction(3);? Eu tentei, this(counter-1);mas deu um erro this is not a function.

Samthere
fonte
1
NB Dentro de uma função, isso se refere ao contexto de execução da função, não à própria função. No seu caso, provavelmente estava apontando para o objeto janela global.
antoine

Respostas:

146

Usando expressões de função nomeada:

Você pode dar a uma expressão de função um nome que seja realmente privado e só seja visível de dentro da própria função:

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

Aqui myselfé visível apenas dentro da própria função .

Você pode usar este nome privado para chamar a função recursivamente.

Consulte 13. Function Definitiona especificação ECMAScript 5:

O Identifier em uma FunctionExpression pode ser referenciado de dentro do FunctionBody da FunctionExpression para permitir que a função chame a si mesma recursivamente. No entanto, ao contrário de uma FunctionDeclaration, o Identifier em uma FunctionExpression não pode ser referenciado e não afeta o escopo que envolve a FunctionExpression.

Observe que o Internet Explorer até a versão 8 não se comporta corretamente, pois o nome está realmente visível no ambiente da variável envolvente e faz referência a uma duplicata da função real (consulte o comentário de patrick dw abaixo).

Usando argumentos.callee:

Como alternativa, você pode usar arguments.calleepara se referir à função atual:

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

A 5ª edição do ECMAScript proíbe o uso de arguments.callee () no modo estrito , no entanto:

(Do MDN ): Em argumentos de código normal. Callee refere-se à função envolvente. Este caso de uso é fraco: basta nomear a função envolvente! Além disso, argument.callee atrapalha substancialmente as otimizações como funções embutidas, porque deve ser possível fornecer uma referência à função não embutida se argumentos.callee for acessado. argumentos.callee para funções de modo estrito é uma propriedade não deletável que é lançada quando definida ou recuperada.

Arnaud Le Blanc
fonte
4
+1 Embora seja um pouco problemático no IE8 e inferior, isso myselfé realmente visível no ambiente de variável envolvente e faz referência a uma duplicata da myselffunção real . Você deve ser capaz de definir a referência externa para nullembora.
user113716
Obrigado pela resposta! Ambos foram úteis e resolveram o problema de 2 maneiras diferentes. No final, decidi aleatoriamente qual aceitar: P
Samthere
só para eu entender. Qual é a razão para multiplicar a função em cada retorno? return n * myself(n-1);?
chitzui
porque a função funciona assim? jsfiddle.net/jvL5euho/18 após se repetir 4 vezes.
Prashant Tapase
De acordo com alguns argumentos de referência, o callee não funcionará no modo estrito.
Krunal Limbad
10

Você pode acessar a função em si usando arguments.callee [MDN] :

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

No entanto, isso será interrompido no modo estrito.

Felix Kling
fonte
6
Eu acredito que isso está obsoleto (e não permitido no modo estrito)
Arnaud Le Blanc
@Felix: Sim, "modo estrito" dará um TypeError, mas não encontrei nada que declare oficialmente que arguments.callee (ou qualquer violação do modo estrito) está obsoleto fora do "modo estrito".
user113716
Obrigado pela resposta! Ambos foram úteis e resolveram o problema de 2 maneiras diferentes. No final, decidi aleatoriamente qual aceitar: P
Samthere
6

Você pode usar o 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)));

E você pode usá-lo assim:

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

Sei que essa é uma pergunta antiga, mas pensei em apresentar mais uma solução que poderia ser usada se você quiser evitar o uso de expressões de função nomeadas. (Não estou dizendo que você deve ou não evitá-los, apenas apresentando outra solução)

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

Aqui está um exemplo muito simples:

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 a countercontagem "para trás" em relação ao slugvalor é. Isso ocorre por causa da posição em que estamos registrando esses valores, pois a função se repete antes do registro - portanto, basicamente mantemos o aninhamento cada vez mais profundo na pilha de chamadas antes que o registro ocorra.

Uma vez que a recursividade encontra o item final call-pilha, ele trampolins "fora" das chamadas de função, enquanto que, o primeiro incremento de counterocorrer dentro da última chamada aninhada.

Eu sei que isso não é uma "correção" no código do Questionador, mas dado o título, pensei em exemplificar genericamente a Recursão para uma melhor compreensão da recursão, de imediato.

Cody
fonte