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.


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.


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.


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

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

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.