web-dev-qa-db-ja.com

JavaScript関数を再帰的に呼び出す

次のような変数で再帰関数を作成できます。

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

これにより、functionHolder(3);3210を出力します。私が次のことをしたとしましょう:

var copyFunction = functionHolder;

copyFunction(3);は、上記のように3210を出力します。次にfunctionHolderを次のように変更した場合:

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

次に、functionHolder(3);は、期待どおりStop counting!を返します。

copyFunction(3);は、関数(それ自体が指す)ではなく、functionHolderを参照するため、3Stop counting!を提供します。これは状況によっては望ましい場合もありますが、それを保持する変数ではなく自分自身を呼び出すように関数を記述する方法はありますか?

つまり、onlyfunctionHolder(counter-1);を変更して、copyFunction(3);を呼び出すときに、これらすべての手順を実行しても3210が得られるようにすることは可能ですか? this(counter-1);を試しましたが、エラーthis is not a functionが表示されます。

83
Samthere

名前付き関数式の使用:

関数式には、実際にはprivateであり、関数ifselfの内部からのみ表示される名前を付けることができます。

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

ここでmyself関数の内部でのみ表示自体です。

このプライベート名を使用して、関数を再帰的に呼び出すことができます。

ECMAScript 5仕様の 13. Function Definition を参照してください:

FunctionExpressionのIdentifierはFunctionExpressionのFunctionBody内から参照でき、関数がそれ自体を再帰的に呼び出すことができます。ただし、FunctionDeclarationとは異なり、FunctionExpressionのIdentifierは参照できず、FunctionExpressionを囲むスコープに影響しません。

Internet Explorerバージョン8までは、囲んでいる変数環境で実際に名前が見えるため、正しく動作せず、実際の関数の複製を参照することに注意してください(patrick dw 'を参照)以下のコメント)。

Arguments.calleeの使用:

あるいは、 arguments.callee を使用して現在の関数を参照することもできます。

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

ECMAScriptの第5版では、arguments.callee()を strict mode で使用できませんが、

MDN から):通常のコードではarguments.calleeは囲んでいる関数を指します。このユースケースは弱いです。単に囲んでいる関数に名前を付けてください!また、arguments.calleeにアクセスすると、インライン化されていない関数への参照を提供できるようにする必要があるため、arguments.calleeはインライン化関数などの最適化を大幅に妨げます。 strictモード関数のarguments.calleeは、設定または取得時にスローされる削除不可能なプロパティです。

140
Arnaud Le Blanc

arguments.calleeを使用して関数自体にアクセスできます。[MDN]

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

ただし、これは厳密モードでは破損します。

9
Felix Kling

これは古い質問であることは知っていますが、名前付き関数式の使用を避けたい場合に使用できるもう1つの解決策を提示したいと思いました。 (別の解決策を提示するだけで、それらを避けるべきまたは言わないでください)

  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

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

これを次のように使用できます。

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

非常に簡単な例を次に示します。

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

counterは、slugの値が「後方」であることに注意してください。これは、ロギングの前に関数recursとしてこれらの値をロギングする位置のためです。したがって、本質的に深くネストし続けますcall-stackbeforeロギングが行われます。

再帰が最後の呼び出しスタック項目に一致すると、関数呼び出しのtrampolines「out」になりますが、counterの最初の増分最後のネストされた呼び出しの内部で発生します。

私はこれが質問者のコードの「修正」ではないことを知っていますが、再帰をよりよく理解するために、一般的に例示するRecursionのタイトルを考えて、あからさま。

3
Cody