Gọi một hàm javascript một cách đệ quy


90

Tôi có thể tạo một hàm đệ quy trong một biến như vậy:

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

Với điều này, functionHolder(3);sẽ xuất ra 3 2 1 0. Giả sử tôi đã làm như sau:

var copyFunction = functionHolder;

copyFunction(3);sẽ xuất 3 2 1 0như trên. Nếu sau đó tôi đã thay đổi functionHoldernhư sau:

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

Sau đó, functionHolder(3);sẽ cho Stop counting!, như mong đợi.

copyFunction(3);bây giờ cung cấp 3 Stop counting!như nó đề cập đến functionHolder, không phải hàm (mà chính nó trỏ đến). Điều này có thể là mong muốn trong một số trường hợp, nhưng có cách nào để viết hàm để nó gọi chính nó thay vì biến giữ nó không?

Đó là, có thể chỉ thay đổi dòng functionHolder(counter-1);để trải qua tất cả các bước này vẫn cho 3 2 1 0khi chúng ta gọi copyFunction(3);không? Tôi đã thử this(counter-1);nhưng điều đó mang lại cho tôi lỗi this is not a function.


1
NB Bên trong một hàm, điều này đề cập đến ngữ cảnh thực thi của hàm, không phải chính hàm. Trong trường hợp của bạn, điều này có thể chỉ đến đối tượng cửa sổ toàn cầu.
antoine

Câu trả lời:


146

Sử dụng Biểu thức Hàm được Đặt tên:

Bạn có thể đặt tên cho một biểu thức hàm thực sự là riêng tư và chỉ hiển thị từ bên trong hàm nếu bản thân:

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

Dưới đây myselfcó thể nhìn thấy chỉ bên trong của hàm riêng của mình.

Bạn có thể sử dụng tên riêng này để gọi hàm một cách đệ quy.

Xem 13. Function Definitionthông số kỹ thuật ECMAScript 5:

Mã định danh trong Biểu thức hàm có thể được tham chiếu từ bên trong Cơ thể biểu thức của hàm để cho phép hàm gọi chính nó một cách đệ quy. Tuy nhiên, không giống như trong khai báo Hàm, Mã định danh trong Biểu thức hàm không thể được tham chiếu từ và không ảnh hưởng đến phạm vi bao quanh Biểu thức hàm.

Xin lưu ý rằng Internet Explorer lên đến phiên bản 8 không hoạt động chính xác vì tên thực sự hiển thị trong môi trường biến bao quanh và nó tham chiếu đến một bản sao của hàm thực (xem nhận xét của Patrick dw bên dưới).

Sử dụng các đối số.callee:

Ngoài ra, bạn có thể sử dụng arguments.calleeđể tham khảo chức năng hiện tại:

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

Tuy nhiên, ấn bản thứ 5 của ECMAScript cấm sử dụng đối số.callee () ở chế độ nghiêm ngặt :

(Từ MDN ): Trong các đối số mã bình thường .callee đề cập đến hàm bao quanh. Trường hợp sử dụng này là yếu: chỉ cần đặt tên cho hàm bao quanh! Hơn nữa, đối số.callee về cơ bản cản trở việc tối ưu hóa như các hàm nội tuyến, bởi vì nó phải được thực hiện để cung cấp tham chiếu đến hàm không nội dòng nếu đối số.callee được truy cập. đối số.callee cho các hàm chế độ nghiêm ngặt là một thuộc tính không thể xóa được ném khi được thiết lập hoặc truy xuất.


4
+1 Mặc dù nó có một chút lỗi trong IE8 và thấp hơn trong đó myselfthực sự hiển thị trong môi trường biến bao quanh và nó tham chiếu đến một bản sao của myselfhàm thực . Bạn sẽ có thể đặt tham chiếu bên ngoài thành nullmặc dù.
user113716

Cảm ơn vì câu trả lời! Cả hai đều hữu ích và giải quyết được vấn đề, theo 2 cách khác nhau. Cuối cùng tôi quyết định một cách ngẫu nhiên mà chấp nhận: P
Samthere

chỉ để tôi hiểu. Lý do đằng sau việc nhân hàm trên mỗi lần trả về là gì? return n * myself(n-1);?
chitzui

tại sao chức năng hoạt động như thế này? jsfiddle.net/jvL5euho/18 sau vòng lặp if 4 lần.
Prashant Tapase

Theo một số đối số tham chiếu .callee sẽ không hoạt động ở chế độ nghiêm ngặt.
Krunal Limbad

10

Bạn có thể truy cập chính chức năng đó bằng arguments.callee [MDN] :

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

Tuy nhiên, điều này sẽ phá vỡ ở chế độ nghiêm ngặt.


6
Tôi tin rằng điều này bị phản đối (và không được phép vào chế độ nghiêm ngặt)
Arnaud Le Blanc

@Felix: Vâng, "chế độ nghiêm ngặt" sẽ đưa ra TypeError, nhưng tôi chưa tìm thấy bất kỳ điều gì chính thức tuyên bố rằng arguments.callee (hoặc bất kỳ vi phạm chế độ nghiêm ngặt nào) không được chấp nhận ngoài "chế độ nghiêm ngặt".
dùng113716

Cảm ơn vì câu trả lời! Cả hai đều hữu ích và giải quyết được vấn đề, theo 2 cách khác nhau. Cuối cùng tôi quyết định một cách ngẫu nhiên mà chấp nhận: P
Samthere

6

Bạn có thể sử dụng tổ hợp chữ 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)));

Và bạn có thể sử dụng nó như sau:

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

Tôi biết đây là một câu hỏi cũ, nhưng tôi nghĩ tôi sẽ trình bày một giải pháp khác có thể được sử dụng nếu bạn muốn tránh sử dụng các biểu thức hàm đã đặt tên. (Không nói bạn nên hay không nên tránh chúng, chỉ trình bày một giải pháp khác)

  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

Đây là một ví dụ rất đơn giản:

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

Lưu ý rằng countersố đếm "ngược" liên quan đến sluggiá trị của giá trị. Điều này là do vị trí mà chúng ta đang ghi các giá trị này, khi hàm này lặp lại trước khi ghi - vì vậy, về cơ bản, chúng ta tiếp tục lồng sâu hơn và sâu hơn vào ngăn xếp lệnh gọi trước khi ghi nhật ký diễn ra.

Khi đệ quy đáp ứng các cuộc gọi stack mục cuối cùng, nó trampolines "out" của các cuộc gọi chức năng, trong khi đó, thặng dư đầu tiên của counterxảy ra bên trong của cuộc gọi lồng nhau cuối cùng.

Tôi biết đây không phải là một "bản sửa lỗi" đối với mã của Người hỏi, nhưng với tiêu đề mà tôi nghĩ rằng tôi nói chung là minh họa cho Đệ quy để hiểu rõ hơn về đệ quy, hoàn toàn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.