Xác định một hàm trong một hàm khác trong JavaScript


82
function foo(a) {
    if (/* Some condition */) {
        // perform task 1
        // perform task 3
    }
    else {
        // perform task 2
        // perform task 3
    }
}

Tôi có một hàm có cấu trúc tương tự như ở trên. Tôi muốn tóm tắt nhiệm vụ 3 thành một hàm, bar()nhưng tôi muốn giới hạn quyền truy cập của hàm này chỉ trong phạm vi của foo(a).

Để đạt được những gì tôi muốn, có đúng không khi thay đổi những điều sau đây?

function foo(a) {
    function bar() {
        // Perform task 3
    }

    if (/* Some condition */) {
        // Perform task 1
        bar();
    }
    else {
        // Perform task 2
        bar();
    }
}

Nếu điều trên là chính xác, có bar()được xác định lại mỗi khi foo(a)được gọi không? (Tôi đang lo lắng về việc lãng phí tài nguyên CPU ở đây.)


1
Kiểm tra xem nó có giá trị không trong khi chính bạn: jsperf.com Tôi tưởng tượng nó phụ thuộc vào task3.
tomByrer

1
@tomByer - 1 cho thấy công cụ
tamakisquare

Câu trả lời:


121

Vâng, những gì bạn có ở đó là đúng. Một số lưu ý:

  • barđược tạo trên mọi lệnh gọi hàm foo, nhưng:
    • Trên các trình duyệt hiện đại, đây là một quá trình rất nhanh. (Một số công cụ có thể chỉ biên dịch cho nó một lần và sau đó sử dụng lại mã đó với ngữ cảnh khác nhau mỗi lần; công cụ V8 của Google [trong Chrome và các nơi khác] thực hiện điều đó trong hầu hết các trường hợp.)
    • Và tùy thuộc vào những gì barlàm, một số công cụ có thể xác định rằng chúng có thể "nội tuyến" nó, loại bỏ hoàn toàn lệnh gọi hàm. V8 làm được điều này và tôi chắc chắn rằng nó không phải là động cơ duy nhất làm được điều này. Đương nhiên họ chỉ có thể làm điều này nếu nó không thay đổi hành vi của mã.
  • Tác động hiệu suất, nếu có, của việc bartạo ra mọi lúc sẽ rất khác nhau giữa các công cụ JavaScript. Nếu barlà tầm thường, nó sẽ thay đổi từ không thể phát hiện đến khá nhỏ. Nếu bạn không gọi foohàng nghìn lần liên tiếp (ví dụ: từ một mousemovetrình xử lý), tôi sẽ không lo lắng về điều đó. Ngay cả khi bạn là như vậy, tôi chỉ lo lắng về điều đó nếu tôi gặp sự cố trên động cơ chậm hơn. Đây là một trường hợp thử nghiệm liên quan đến các hoạt động DOM , cho thấy rằng có một tác động, nhưng một tác động nhỏ (có thể bị rửa trôi bởi nội dung DOM). Đây là một trường hợp thử nghiệm thực hiện tính toán thuần túy cho thấy tác động cao hơn nhiều, nhưng thành thật mà nói, chúng ta đang nói về sự khác biệt của micro giây vì thậm chí mức tăng 92% đối với thứ gì đó có vigiây để xảy ra vẫn còn rất, rất nhanh. Cho đến khi / trừ khi bạn nhìn thấy một tác động trong thế giới thực, đó không phải là điều đáng lo ngại.
  • barsẽ chỉ có thể truy cập được từ bên trong hàm và nó có quyền truy cập vào tất cả các biến và đối số cho lệnh gọi hàm đó. Điều này làm cho nó trở thành một mẫu rất tiện dụng.
  • Lưu ý rằng vì bạn đã sử dụng khai báo hàm nên bạn đặt khai báo ở đâu (trên cùng, dưới cùng hoặc giữa - miễn là nó ở cấp trên cùng của hàm, không phải bên trong câu lệnh điều khiển luồng, đó là lỗi cú pháp), nó được xác định trước khi chạy dòng đầu tiên của mã bước khôn ngoan.

Thx cho câu trả lời của bạn. Vì vậy, bạn có nói rằng đây là một thành tích hiệu suất không đáng kể? (cho rằng một bản sao của barđược tạo ra trên tất cả các cuộc gọi của foo)
tamakisquare

2
@ahmoo: Với hiệu suất JavaScript, câu trả lời hầu như luôn luôn là: Nó phụ thuộc. :-) Nó phụ thuộc vào động cơ sẽ chạy nó và tần suất bạn sẽ gọi foo. Nếu bạn không gọi foohàng nghìn lần liên tiếp (ví dụ: không phải trong mousemovetrình xử lý), thì tôi sẽ không lo lắng về điều đó chút nào. Và lưu ý rằng một số động cơ (ví dụ: V8) sẽ nội tuyến mã, loại bỏ hoàn toàn lệnh gọi hàm, miễn là làm như vậy không thay đổi những gì đang xảy ra theo cách có thể được phát hiện bên ngoài.
TJ Crowder

@TJCrowder: bạn có thể bình luận về câu trả lời của robrich không? giải pháp đó có ngăn chặn việc giải trí bar()trên mỗi cuộc gọi không? ngoài ra, sẽ sử dụng foo.prototype.barđể xác định chức năng giúp bất kỳ?
rkw

4
@rkw: Tạo hàm một lần, giống như câu trả lời của robrich, là một cách hữu ích để tránh chi phí tạo nó trong mỗi lần gọi. Bạn mất barquyền truy cập vào các biến và đối số cho lệnh gọi foo(bất cứ thứ gì bạn muốn nó hoạt động, bạn phải chuyển nó), điều này có thể làm phức tạp mọi thứ một chút, nhưng trong một tình huống quan trọng về hiệu suất mà bạn đã gặp một vấn đề thực tế, bạn có thể cấu trúc lại như vậy để xem liệu nó có giải quyết được vấn đề hay không. Không, sử dụng foo.prototypesẽ không thực sự hữu ích (vì một điều, barsẽ không còn là riêng tư nữa).
TJ Crowder

@ahmoo: Đã thêm một trường hợp thử nghiệm. Thật thú vị, tôi nhận được kết quả khác với trường hợp thử nghiệm của Guffa, tôi nghĩ rằng chức năng của anh ta có thể hơi quá đơn giản. Nhưng tôi vẫn không nghĩ rằng hiệu suất là một vấn đề.
TJ Crowder

15

Đây là những gì đóng cửa.

var foo = (function () {
  function bar() {
    // perform task 3
  };

  function innerfoo (a) { 
    if (/* some cond */ ) {
      // perform task 1
      bar();
    }
    else {
      // perform task 2
      bar();
    }
  }
  return innerfoo;
})();

Innerfoo (một bao đóng) giữ một tham chiếu đến thanh và chỉ một tham chiếu đến innerfoo được trả về từ một hàm ẩn danh chỉ được gọi một lần để tạo bao đóng.

Thanh không thể truy cập từ bên ngoài theo cách này.


1
Hấp dẫn. Tôi hạn chế tiếp xúc với javascript, vì vậy việc đóng cửa là một điều gì đó mới mẻ đối với tôi. Tuy nhiên, bạn đã đánh dấu một điểm khởi đầu để tôi nghiên cứu sự đóng cửa. Cảm ơn.
tamakisquare

Bạn sử dụng bao lâu để xử lý phạm vi biến / hàm? Ví dụ: nếu bạn có 2 hàm cần truy cập 3 biến giống nhau, bạn sẽ khai báo 3 biến trong một bao đóng cùng với 2 hàm và sau đó trả về 2 hàm?
doubleOrt

8
var foo = (function () {
    var bar = function () {
        // perform task 3
    }
    return function (a) {

        if (/*some condition*/) {
            // perform task 1
            bar();
        }
        else {
            // perform task 2
            bar();
        }
    };
}());

Việc đóng lại giữ phạm vi bar()chứa, trả về hàm mới từ hàm ẩn danh tự thực thi sẽ đặt phạm vi hiển thị nhiều hơn foo(). Hàm tự thực thi ẩn danh được chạy chính xác một lần, vì vậy chỉ có một bar()phiên bản duy nhất và mỗi lần thực thi foo()sẽ sử dụng nó.


Hấp dẫn. Sau đó tôi phải tìm kiếm sự đóng cửa. Cảm ơn.
tamakisquare

Bạn sử dụng bao lâu để xử lý phạm vi biến / hàm? Ví dụ: nếu bạn có 2 hàm cần truy cập 3 biến giống nhau, bạn có khai báo 3 biến trong một bao đóng cùng với 2 hàm và sau đó trả về 2 hàm không?
doubleOrt

Tôi sử dụng bao lâu một lần? Mọi lúc. Nếu tôi có 3 biến cần thiết cho 2 hàm: 1. (tốt nhất) chuyển 3 biến vào cả hai hàm - các hàm có thể được xác định một lần và chỉ một lần. 2. (tốt) tạo 2 hàm trong đó các biến nằm ngoài phạm vi của cả hai. Đây là một sự đóng cửa. (Về cơ bản câu trả lời ở đây.) Đáng buồn thay, các hàm được định nghĩa lại cho mỗi lần sử dụng biến mới. 3. (xấu) không sử dụng các hàm, chỉ cần có một phương thức dài lớn làm cả hai việc.
robrich

5

Vâng, điều đó hoạt động tốt.

Hàm bên trong không được tạo lại mỗi khi bạn nhập hàm bên ngoài, nhưng nó được gán lại.

Nếu bạn kiểm tra mã này:

function test() {

    function demo() { alert('1'); }

    demo();
    demo = function() { alert('2'); };
    demo();

}

test();
test();

nó sẽ hiển thị 1, 2, 1, 2, không 1, 2, 2, 2.


Cảm ơn câu trả lời của bạn. Việc chỉ định lại demo()mỗi lần test()có được gọi là mối quan tâm về hiệu suất không? Nó phụ thuộc vào độ phức tạp của demo()?
tamakisquare

1
Tôi đã thực hiện một bài kiểm tra hiệu suất: jsperf.com/inner- Chức năng-vs-global- Chức năng Kết luận rằng nói chung đây không phải là vấn đề về hiệu suất (vì bất kỳ mã nào bạn đưa vào các hàm sẽ mất nhiều thời gian để chạy hơn là tạo hàm ), nhưng nếu bạn cần lợi thế hiệu suất bổ sung đó, bạn sẽ phải viết mã khác nhau cho các trình duyệt khác nhau.
Guffa

Thx vì đã dành thời gian để tạo bài kiểm tra và cũng như chia sẻ điểm của bạn về hiệu suất. Nhiều đánh giá cao.
tamakisquare

Bạn đã nói "chức năng bên trong không được tái tạo mỗi lần" với sự tự tin tuyệt vời. Theo thông số kỹ thuật , nó là; một động cơ có tối ưu hóa nó hay không phụ thuộc vào động cơ. (Tôi hy vọng hầu hết sẽ như vậy.) Tuy nhiên, tôi rất tò mò khi thấy trường hợp thử nghiệm của bạn và của tôi có kết quả khác nhau như vậy: jsperf.com/cost-of-creating-inner- Chức năng. Tuy nhiên, tôi không nghĩ rằng hiệu suất là một vấn đề.
TJ Crowder

@TJCrowder: Vâng, đó là một chi tiết triển khai, nhưng vì các công cụ Javascript hiện đại biên dịch mã, nó sẽ không biên dịch lại hàm mỗi lần được gán. Lý do mà kết quả của các bài kiểm tra hiệu suất khác nhau, là vì chúng kiểm tra những thứ khác nhau. Thử nghiệm của tôi so sánh các hàm toàn cục với các hàm cục bộ, trong khi thử nghiệm của bạn so sánh một hàm cục bộ với mã nội tuyến. Nội tuyến mã tất nhiên sẽ nhanh hơn gọi hàm, đó là một kỹ thuật tối ưu hóa phổ biến.
Guffa

0

Tôi đã tạo một jsperf để kiểm tra các biểu thức hàm lồng nhau so với không lồng nhau và biểu thức hàm so với khai báo hàm và tôi rất ngạc nhiên khi thấy rằng các trường hợp kiểm tra lồng nhau hoạt động nhanh hơn 20 lần so với không lồng ghép. (Tôi đã đoán trước sự khác biệt ngược lại hoặc không đáng kể).

https://jsperf.com/nested-functions-vs-not-nested-2/1

Đây là trên Chrome 76, macOS.

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.