Memanggil fungsi javascript secara rekursif

90

Saya dapat membuat fungsi rekursif dalam variabel seperti:

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

Dengan ini, functionHolder(3);akan menghasilkan 3 2 1 0. Katakanlah saya melakukan yang berikut:

var copyFunction = functionHolder;

copyFunction(3);akan menghasilkan 3 2 1 0seperti di atas. Jika saya kemudian berubah functionHoldersebagai berikut:

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

Kemudian functionHolder(3);akan memberi Stop counting!, seperti yang diharapkan.

copyFunction(3);sekarang memberikan 3 Stop counting!apa yang dirujuknya functionHolder, bukan fungsi (yang ditunjukkannya sendiri). Ini mungkin diinginkan dalam beberapa keadaan, tetapi adakah cara untuk menulis fungsi sehingga ia memanggil dirinya sendiri daripada variabel yang menampungnya?

Artinya, apakah mungkin untuk mengubah hanya garis functionHolder(counter-1);sehingga melalui semua langkah ini masih memberikan 3 2 1 0saat kita menelepon copyFunction(3);? Saya mencoba this(counter-1);tetapi itu memberi saya kesalahan this is not a function.

Samthere
sumber
1
NB Di dalam suatu fungsi, ini mengacu pada konteks pelaksanaan fungsi tersebut, bukan fungsi itu sendiri. Dalam kasus Anda, ini mungkin mengarah ke objek jendela global.
antoine

Jawaban:

146

Menggunakan Ekspresi Fungsi Bernama:

Anda dapat memberi ekspresi fungsi nama yang sebenarnya pribadi dan hanya terlihat dari dalam fungsi itu sendiri:

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

Berikut myselfadalah hanya terlihat dalam fungsi itu sendiri.

Anda dapat menggunakan nama pribadi ini untuk memanggil fungsi secara rekursif.

Lihat 13. Function Definitionspesifikasi ECMAScript 5:

Identifier dalam FunctionExpression bisa direferensikan dari dalam FunctionBody FunctionExpression untuk memungkinkan fungsi memanggil dirinya sendiri secara rekursif. Namun, tidak seperti di FunctionDeclaration, Identifier dalam FunctionExpression tidak bisa dirujuk dan tidak memengaruhi cakupan yang melingkupi FunctionExpression.

Harap dicatat bahwa Internet Explorer hingga versi 8 tidak berperilaku dengan benar karena nama sebenarnya terlihat di lingkungan variabel yang melingkupinya, dan ini merujuk pada duplikat dari fungsi sebenarnya (lihat komentar patrick dw di bawah).

Menggunakan arguments.callee:

Atau Anda dapat menggunakan arguments.calleeuntuk merujuk ke fungsi saat ini:

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

Edisi ke-5 ECMAScript melarang penggunaan arguments.callee () dalam mode ketat , namun:

(Dari MDN ): Dalam argumen kode normal. Callee mengacu pada fungsi penutup. Kasus penggunaan ini lemah: cukup beri nama fungsi penutup! Selain itu, arguments.callee secara substansial menghalangi pengoptimalan seperti fungsi sebaris, karena harus dimungkinkan untuk memberikan referensi ke fungsi tak sebaris jika arguments.callee diakses. arguments.callee untuk fungsi mode ketat adalah properti non-deletable yang muncul saat disetel atau diambil.

Arnaud Le Blanc
sumber
4
+1 Meskipun sedikit buggy di IE8 dan lebih rendah yang myselfsebenarnya terlihat di lingkungan variabel yang melingkupi, dan ini mereferensikan duplikat dari myselffungsi sebenarnya . Anda harus bisa mengatur referensi luarnya null.
pengguna113716
Terima kasih atas jawabannya! Keduanya membantu dan memecahkan masalah, dengan 2 cara berbeda. Pada akhirnya saya secara acak memutuskan mana yang akan menerima: P
Samthere
hanya untuk saya pahami. Apa alasan di balik mengalikan fungsi pada setiap pengembalian? return n * myself(n-1);?
chitzui
mengapa fungsinya bekerja seperti ini? jsfiddle.net/jvL5euho/18 setelahnya jika mengulang 4 kali.
Prashant Tapase
Sesuai dengan beberapa argumen referensi, callee tidak akan bekerja dalam mode ketat.
Krunal Limbad
10

Anda dapat mengakses fungsi itu sendiri menggunakan arguments.callee [MDN] :

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

Namun, ini akan rusak dalam mode ketat.

Felix Kling
sumber
6
Saya percaya bahwa ini sudah usang (dan tidak diperbolehkan dalam mode ketat)
Arnaud Le Blanc
@Felix: Ya, "mode ketat" akan memberikan TypeError, tetapi saya belum menemukan apa pun yang secara resmi menyatakan bahwa arguments.callee (atau pelanggaran mode ketat) tidak berlaku lagi di luar "mode ketat".
pengguna113716
Terima kasih atas jawabannya! Keduanya membantu dan memecahkan masalah, dengan 2 cara berbeda. Pada akhirnya saya secara acak memutuskan mana yang akan menerima: P
Samthere
6

Anda dapat menggunakan Y-combinator: ( 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)));

Dan Anda bisa menggunakannya seperti ini:

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

Saya tahu ini adalah pertanyaan lama, tetapi saya pikir saya akan menyajikan satu solusi lagi yang dapat digunakan jika Anda ingin menghindari penggunaan ekspresi fungsi bernama. (Tidak mengatakan Anda harus atau tidak harus menghindarinya, hanya memberikan solusi lain)

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

Inilah satu contoh yang sangat sederhana:

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

Perhatikan bahwa countermenghitung "mundur" sehubungan dengan apa slugnilainya. Ini karena posisi di mana kita mencatat nilai-nilai ini, karena fungsinya berulang sebelum masuk - jadi, pada dasarnya kita terus melakukan penumpukan lebih dalam dan lebih dalam ke tumpukan panggilan sebelum logging dilakukan.

Setelah rekursi memenuhi item stack panggilan terakhir, ia akan melakukan trampolin "keluar" dari pemanggilan fungsi, sedangkan, kenaikan pertama counterterjadi di dalam panggilan bertingkat terakhir.

Saya tahu ini bukan "perbaikan" pada kode Penanya, tetapi dengan judul yang saya pikir secara umum saya akan mencontohkan Rekursi untuk pemahaman yang lebih baik tentang rekursi, langsung.

Cody
sumber