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


90

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

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

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

var copyFunction = functionHolder;

copyFunction(3);3 2 1 0上記のように出力されます。その後functionHolder、次のように変更した場合:

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

その後、期待どおりにfunctionHolder(3);を与えるでしょうStop counting!

copyFunction(3);今与え3 Stop counting!、それが指すようにfunctionHolder(それ自体が指す)、機能しません。これは状況によっては望ましいかもしれませんが、関数を保持する変数ではなくそれ自体を呼び出すように関数を作成する方法はありますか?

つまり、これらのすべての手順を実行しても、呼び出し時にまだ与えることができるように、行のみを変更することは可能ですか?私は試しましたが、それは私にエラーを与えます。functionHolder(counter-1);3 2 1 0copyFunction(3);this(counter-1);this is not a function


1
NB関数内では、これは関数自体ではなく、関数の実行のコンテキストを指します。あなたの場合、これはおそらくグローバルウィンドウオブジェクトを指しています。
アントワーヌ2015年

回答:


146

名前付き関数式の使用:

関数式には、実際にはプライベートで、関数内部からのみ表示される名前を付けることができます。

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

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

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

13. Function DefinitionECMAScript 5仕様の参照:

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

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

arguments.calleeを使用:

またはarguments.callee、現在の関数を参照するために使用できます。

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

ただし、ECMAScriptの第5版では、strictモードでのarguments.callee()の使用を禁止しています。

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


4
+1 IE8以下では少しバグがありmyselfますが、実際には囲んでいる変数環境で表示され、実際の関数の複製を参照していmyselfます。nullただし、外部参照を設定できるはずです。
user113716 2011

答えてくれてありがとう!どちらも役に立ち、問題を2つの異なる方法で解決しました。結局、私はどちらを受け入れるかをランダムに決めました:P
Samthere

私が理解するためだけに。リターンごとに関数を乗算する理由は何ですか?return n * myself(n-1);
chitzui 2017

なぜ関数はこのように機能するのですか?jsfiddle.net/jvL5euho/18後4回ループした場合。
Prashant Tapase

一部の参照のとおり、arguments.calleeはstrictモードでは機能しません。
Krunal Limbad

10

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

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

ただし、これはストリクトモードでは機能しません。


6
これは非推奨(厳密モードでは許可されない)だと思います
Arnaud Le Blanc

@Felix:ええ、「ストリクトモード」ではが表示されますが、「ストリクトモード」以外では非推奨でTypeErrorあるarguments.callee (またはストリクトモード違反)と公式に述べているものは見つかりませんでした。
user113716 2011

答えてくれてありがとう!どちらも役に立ち、問題を2つの異なる方法で解決しました。結局、私はどちらを受け入れるかをランダムに決めました:P
Samthere

6

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

5

これは古い質問であることはわかっていますが、名前付き関数式の使用を避けたい場合に使用できるもう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);

3

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

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。これは、関数がロギングの前に繰り返されるため、これらの値をロギングする位置が原因​​です。したがって、ロギングが行われる前に、基本的に呼び出しスタック 深くネストし続けます

再帰が最後の呼び出しスタック項目に出会うと、関数呼び出しから「外に」トランポリンされますが、最初の増分counterは、最後にネストされた呼び出し内で発生します。

これはクエスチョナーのコードの「修正」ではないことを知っていますが、タイトルを考えると、再帰を完全に理解するために再帰を一般的に例示するつもりだと思いました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.