JavaScript đóng so với các hàm ẩn danh


562

Một người bạn của tôi và tôi hiện đang thảo luận về việc đóng cửa trong JS là gì và không phải là gì. Chúng tôi chỉ muốn chắc chắn rằng chúng tôi thực sự hiểu nó một cách chính xác.

Hãy lấy ví dụ này. Chúng tôi có một vòng lặp đếm và muốn in biến đếm trên bàn điều khiển bị trì hoãn. Do đó, chúng tôi sử dụng setTimeoutđóng để nắm bắt giá trị của biến đếm để đảm bảo rằng nó sẽ không in N lần giá trị N.

Giải pháp sai mà không đóng cửa hoặc bất cứ điều gì gần đóng cửa sẽ là:

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

Tất nhiên sẽ in 10 lần giá trị isau vòng lặp, cụ thể là 10.

Vì vậy, nỗ lực của anh là:

for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();
}

in 0 đến 9 như mong đợi.

Tôi nói với anh ta rằng anh ta không sử dụng đóng cửa để bắt i, nhưng anh ta khăng khăng rằng anh ta là như vậy. Tôi đã chứng minh rằng anh ta không sử dụng các bao đóng bằng cách đặt thân vòng lặp for trong một khối khác setTimeout(chuyển chức năng ẩn danh của mình cho setTimeout), in lại 10 lần 10 lần nữa. Điều tương tự cũng áp dụng nếu tôi lưu trữ chức năng của anh ta trong một varvà thực hiện nó sau vòng lặp, cũng in 10 lần 10. Vì vậy, lập luận của tôi là anh ta không thực sự nắm bắt được giá trị củai , làm cho phiên bản của anh ta không phải là một bao đóng.

Nỗ lực của tôi là:

for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);
}

Vì vậy, tôi chụp i(được đặt tên i2trong bao đóng), nhưng bây giờ tôi trả lại một hàm khác và chuyển cái này xung quanh. Trong trường hợp của tôi, hàm được truyền cho setTimeout thực sự bắt được i.

Bây giờ ai đang sử dụng đóng cửa và ai không?

Lưu ý rằng cả hai giải pháp đều in 0 đến 9 trên bàn điều khiển bị trì hoãn, vì vậy chúng giải quyết được vấn đề ban đầu, nhưng chúng tôi muốn hiểu giải pháp nào trong hai giải pháp đó sử dụng các bao đóng để thực hiện việc này.



1
@leeme: Xem chỉnh sửa ninja của tôi cho một liên kết thứ hai.
Máy xay sinh tố

2
chúng tôi vừa thực hiện một thỏa thuận: người đúng sẽ nhận được điểm SO liên quan đến câu hỏi này
sáng chói

1
@leeme - Cả hai bạn đang sử dụng bao đóng. Cả hai bạn đã thực hiện hai chức năng - một chức năng bên ngoài và một chức năng bên trong; và cả hai chức năng bên trong của bạn là đóng cửa. Tất cả các chức năng của bạn là lambdas ( chức năng ẩn danh ). Đọc câu trả lời của tôi để biết chi tiết.
Aadit M Shah

1
@bledh - Tôi không biết đóng cửa sửa đổi là gì. Tôi thấy rằng liên kết của bạn trỏ đến mã C #. Các bao đóng sửa đổi có được JavaScript hỗ trợ không?
Aadit M Shah

Câu trả lời:


650

Lưu ý của biên tập viên: Tất cả các chức năng trong JavaScript đều được đóng lại như được giải thích trong bài viết này . Tuy nhiên, chúng tôi chỉ quan tâm đến việc xác định một tập hợp con của các hàm này thú vị theo quan điểm lý thuyết. Do đó, bất kỳ tham chiếu nào đến việc đóng từ sẽ đề cập đến tập hợp con các hàm này trừ khi có quy định khác.

Một lời giải thích đơn giản cho việc đóng cửa:

  1. Lấy một chức năng. Hãy gọi nó là F.
  2. Liệt kê tất cả các biến của F.
  3. Các biến có thể có hai loại:
    1. Biến cục bộ (biến ràng buộc)
    2. Biến không cục bộ (biến miễn phí)
  4. Nếu F không có biến miễn phí thì nó không thể đóng.
  5. Nếu F có bất kỳ biến miễn phí nào (được xác định trong một phạm vi mẹ của F) thì:
    1. Chỉ có một phạm vi cha mẹ của F mà a biến miễn phí bị ràng buộc.
    2. Nếu F được tham chiếu từ bên ngoài phạm vi phụ huynh, sau đó nó trở thành một đóng cho rằng biến miễn phí.
    3. Biến miễn phí đó được gọi là giá trị tăng của đóng F.

Bây giờ chúng ta hãy sử dụng điều này để tìm ra ai sử dụng các bao đóng và ai không (để giải thích tôi đã đặt tên cho các hàm):

Trường hợp 1: Chương trình của bạn bè của bạn

for (var i = 0; i < 10; i++) {
    (function f() {
        var i2 = i;
        setTimeout(function g() {
            console.log(i2);
        }, 1000);
    })();
}

Trong chương trình trên có hai chức năng: fg . Hãy xem họ có đóng cửa không:

Dành cho f:

  1. Liệt kê các biến:
    1. i2là người địa phương khác nhau.
    2. imiễn phíbiến .
    3. setTimeoutmiễn phíbiến .
    4. glà người địa phương khác nhau.
    5. consolelà một biến miễn phí .
  2. Tìm phạm vi cha mẹ mà mỗi biến miễn phí bị ràng buộc:
    1. iđược ràng buộc với phạm vi toàn cầu.
    2. setTimeoutđược ràng buộc với phạm vi toàn cầu.
    3. consoleđược ràng buộc với phạm vi toàn cầu.
  3. Trong phạm vi nào là chức năng được tham chiếu ? Các phạm vi toàn cầu .
    1. Do đó ikhông được đóng lại bởi f.
    2. Do đó setTimeoutkhông được đóng lại bởi f.
    3. Do đó consolekhông được đóng lại bởi f.

Do đó, chức năng fkhông phải là một đóng cửa.

Dành cho g:

  1. Liệt kê các biến:
    1. consolelà một biến miễn phí .
    2. i2là một biến miễn phí .
  2. Tìm phạm vi cha mẹ mà mỗi biến miễn phí bị ràng buộc:
    1. consoleđược ràng buộc với phạm vi toàn cầu.
    2. i2được ràng buộc với phạm vi f.
  3. Trong phạm vi nào là chức năng được tham chiếu ? Các phạm visetTimeout .
    1. Do đó consolekhông được đóng lại bởi g.
    2. Do đó i2được đóng lại bởi g.

Do đó, hàm glà một bao đóng cho biến miễn phí i2(là giá trị gia tăng g) khi nó được tham chiếu từ bên trong setTimeout.

Xấu cho bạn: Bạn của bạn đang sử dụng một đóng cửa. Các chức năng bên trong là một đóng cửa.

Trường hợp 2: Chương trình của bạn

for (var i = 0; i < 10; i++) {
    setTimeout((function f(i2) {
        return function g() {
            console.log(i2);
        };
    })(i), 1000);
}

Trong chương trình trên có hai chức năng: fg . Hãy xem họ có đóng cửa không:

Dành cho f:

  1. Liệt kê các biến:
    1. i2là một địa phương khác nhau.
    2. glà một địa phương khác nhau.
    3. consolelà một biến miễn phí .
  2. Tìm phạm vi cha mẹ mà mỗi biến miễn phí bị ràng buộc:
    1. consoleđược ràng buộc với phạm vi toàn cầu.
  3. Trong phạm vi nào là chức năng được tham chiếu ? Các phạm vi toàn cầu .
    1. Do đó consolekhông được đóng lại bởi f.

Do đó, chức năng fkhông phải là một đóng cửa.

Dành cho g:

  1. Liệt kê các biến:
    1. consolelà một biến miễn phí .
    2. i2là một biến miễn phí .
  2. Tìm phạm vi cha mẹ mà mỗi biến miễn phí bị ràng buộc:
    1. consoleđược ràng buộc với phạm vi toàn cầu.
    2. i2được ràng buộc với phạm vi f.
  3. Trong phạm vi nào là chức năng được tham chiếu ? Các phạm visetTimeout .
    1. Do đó consolekhông được đóng lại bởi g.
    2. Do đó i2được đóng lại bởi g.

Do đó, hàm glà một bao đóng cho biến miễn phí i2(là giá trị gia tăng g) khi nó được tham chiếu từ bên trong setTimeout.

Tốt cho bạn: Bạn đang sử dụng một đóng cửa. Các chức năng bên trong là một đóng cửa.

Vì vậy, cả bạn và bạn của bạn đang sử dụng đóng cửa. Đừng cãi nhau nữa. Tôi hy vọng tôi đã xóa khái niệm đóng cửa và làm thế nào để xác định chúng cho cả hai bạn.

Chỉnh sửa: Một lời giải thích đơn giản về lý do tại sao tất cả các chức năng đóng (tín dụng @Peter):

Trước tiên, hãy xem xét chương trình sau (đó là điều khiển ):

lexicalScope();

function lexicalScope() {
    var message = "This is the control. You should be able to see this message being alerted.";

    regularFunction();

    function regularFunction() {
        alert(eval("message"));
    }
}

  1. Chúng tôi biết rằng cả hai lexicalScoperegularFunctionkhông đóng cửa từ định nghĩa trên .
  2. Khi chúng tôi thực hiện chương trình, chúng tôi hy vọng message sẽ được cảnh báo regularFunction không phải là một bao đóng (tức là nó có quyền truy cập vào tất cả các biến trong phạm vi cha của nó - bao gồm message).
  3. Khi chúng tôi thực hiện chương trình, chúng tôi quan sát thấy điều đó messagethực sự được cảnh báo.

Tiếp theo hãy xem xét chương trình sau (đây là chương trình thay thế ):

var closureFunction = lexicalScope();

closureFunction();

function lexicalScope() {
    var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure.";

    return function closureFunction() {
        alert(eval("message"));
    };
}

  1. Chúng tôi biết rằng chỉ closureFunctioncó một đóng cửa từ định nghĩa trên .
  2. Khi chúng tôi thực hiện chương trình, chúng tôi hy vọng sẽ message không bị cảnh báo closureFunction là một bao đóng (tức là nó chỉ có quyền truy cập vào tất cả các biến không cục bộ của nó tại thời điểm hàm được tạo ( xem câu trả lời này ) - điều này không bao gồm message).
  3. Khi chúng tôi thực hiện chương trình, chúng tôi quan sát thấy rằng messagethực sự đang được cảnh báo.

Chúng ta suy luận điều gì?

  1. Trình thông dịch JavaScript không xử lý các bao đóng khác với cách chúng xử lý các hàm khác.
  2. Mỗi chức năng mang chuỗi phạm vi của nó cùng với nó. Đóng cửa không có một môi trường tham chiếu riêng .
  3. Một đóng cửa cũng giống như mọi chức năng khác. Chúng tôi chỉ gọi họ là đóng cửa khi họ được tham chiếu trong một phạm vi ngoài phạm vi mà họ thuộc về đây là một trường hợp thú vị.

40
Được chấp nhận bởi vì bạn đi rất nhiều chi tiết, giải thích rất tốt những gì đang diễn ra. Và cuối cùng, bây giờ tôi đã hiểu rõ hơn về việc đóng cửa là gì, hay nói tốt hơn: cách liên kết biến hoạt động trong JS.
leeme

3
Trong trường hợp 1, bạn nói rằng gchạy trong phạm vi setTimeout, nhưng trong trường hợp 2 bạn nói rằng fchạy trong phạm vi toàn cầu. Cả hai đều nằm trong setTimeout, vậy sự khác biệt là gì?
rosscj2533

9
Bạn sẽ vui lòng cho biết nguồn của bạn cho điều này? Tôi chưa bao giờ thấy một định nghĩa trong đó một chức năng có thể là một bao đóng nếu được gọi trong một phạm vi nhưng không phải trong một phạm vi khác. Do đó, định nghĩa này có vẻ giống như một tập hợp con của định nghĩa tổng quát hơn mà tôi đã sử dụng (xem câu trả lời của kev ) trong đó một bao đóng là một bao đóng bất kể phạm vi của nó được gọi, hoặc ngay cả khi nó không bao giờ được gọi!
Briguy37

11
@AaditMShah Tôi đồng ý với bạn về việc đóng cửa là gì, nhưng bạn nói như thể có sự khác biệt giữa các hàm thông thường và các bao đóng trong JavaScript. Không có sự khác biệt; bên trong mỗi hàm sẽ mang theo nó một tham chiếu đến chuỗi phạm vi cụ thể mà nó được tạo. Công cụ JS không coi đó là một trường hợp khác. Không cần cho một danh sách kiểm tra phức tạp; chỉ biết rằng mọi đối tượng chức năng đều mang phạm vi từ vựng. Thực tế là các biến / thuộc tính có sẵn trên toàn cầu không làm cho hàm bị đóng lại (đây chỉ là một trường hợp vô dụng).
Peter

13
@Peter - Bạn biết gì không, bạn đã đúng. Không có sự khác biệt giữa một chức năng thông thường và đóng cửa. Tôi đã chạy thử nghiệm để chứng minh điều này và kết quả có lợi cho bạn: đây là sự kiểm soát và đây là sự thay thế . Những gì bạn nói không có ý nghĩa. Trình thông dịch JavaScript cần làm sổ sách kế toán đặc biệt để đóng. Chúng chỉ đơn giản là sản phẩm phụ của ngôn ngữ có phạm vi từ vựng với các chức năng hạng nhất. Kiến thức của tôi bị giới hạn ở những gì tôi đọc (đó là sai). Cảm ơn bạn đã sửa chữa cho tôi. Tôi sẽ cập nhật câu trả lời của tôi để phản ánh tương tự.
Aadit M Shah

96

Theo closuređịnh nghĩa:

"Đóng" là một biểu thức (thường là một hàm) có thể có các biến miễn phí cùng với một môi trường liên kết các biến đó ("đóng" biểu thức).

Bạn đang sử dụng closurenếu bạn xác định một hàm sử dụng một biến được xác định bên ngoài hàm. (chúng tôi gọi biến là biến miễn phí ).
Tất cả đều sử dụng closure(ngay cả trong ví dụ 1).


1
Làm thế nào để phiên bản thứ ba sử dụng một biến được xác định bên ngoài chức năng?
Jon

1
@Jon sử dụng hàm trả về i2được xác định bên ngoài.
kev

1
@kev Bạn đang sử dụng bao đóng nếu bạn xác định hàm sử dụng biến được xác định bên ngoài hàm ...... thì trong "Trường hợp 1: Chương trình bạn bè của bạn" của câu trả lời "Aadit M Shah" là "hàm f" đóng cửa? nó sử dụng i (biến được định nghĩa bên ngoài hàm). phạm vi toàn cầu tham chiếu một định thức?
nội bộ-vào


54

Tóm lại, Javascript Closures cho phép một hàm truy cập vào một biến được khai báo trong hàm cha mẹ từ vựng .

Chúng ta hãy xem một lời giải thích chi tiết hơn. Để hiểu các bao đóng, điều quan trọng là phải hiểu cách JavaScript phạm vi các biến.

Phạm vi

Trong phạm vi JavaScript được xác định với các chức năng. Mỗi chức năng xác định một phạm vi mới.

Hãy xem xét ví dụ sau đây;

function f()
{//begin of scope f
  var foo='hello'; //foo is declared in scope f
  for(var i=0;i<2;i++){//i is declared in scope f
     //the for loop is not a function, therefore we are still in scope f
     var bar = 'Am I accessible?';//bar is declared in scope f
     console.log(foo);
  }
  console.log(i);
  console.log(bar);
}//end of scope f

gọi cho bản in

hello
hello
2
Am I Accessible?

Bây giờ chúng ta hãy xem xét trường hợp chúng ta có một hàm gđược định nghĩa trong một hàm khác f.

function f()
{//begin of scope f
  function g()
  {//being of scope g
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

Chúng tôi sẽ gọi fcác mẹ từ vựng của g. Như đã giải thích trước đây chúng ta có 2 phạm vi; phạm vi fvà phạm vig .

Nhưng một phạm vi là "trong" phạm vi khác, vậy phạm vi của hàm chức năng con là một phần của phạm vi của hàm cha? Điều gì xảy ra với các biến được khai báo trong phạm vi của hàm cha; Tôi có thể truy cập chúng từ phạm vi của hàm con không? Đó chính xác là nơi đóng cửa bước vào.

Đóng cửa

Trong JavaScript, hàm gkhông chỉ có thể truy cập bất kỳ biến nào được khai báo trong phạm vi gmà còn truy cập vào bất kỳ biến nào được khai báo trong phạm vi của hàm cha f.

Cân nhắc làm theo;

function f()//lexical parent function
{//begin of scope f
  var foo='hello'; //foo declared in scope f
  function g()
  {//being of scope g
    var bar='bla'; //bar declared in scope g
    console.log(foo);
  }//end of scope g
  g();
  console.log(bar);
}//end of scope f

gọi cho bản in

hello
undefined

Hãy nhìn vào dòng console.log(foo);. Tại thời điểm này, chúng tôi đang ở trong phạm vi gvà chúng tôi cố gắng truy cập vào biến foođược khai báo trong phạm vi f. Nhưng như đã nêu trước khi chúng ta có thể truy cập bất kỳ biến nào được khai báo trong hàm cha từ vựng, đó là trường hợp ở đây; glà cha mẹ từ vựng của f. Do đó hellođược in.
Bây giờ chúng ta hãy nhìn vào dòng console.log(bar);. Tại thời điểm này, chúng tôi đang ở trong phạm vi fvà chúng tôi cố gắng truy cập vào biến barđược khai báo trong phạm vi g. barkhông được khai báo trong phạm vi hiện tại và hàm gkhông phải là cha của f, do đó barkhông được xác định

Trên thực tế, chúng ta cũng có thể truy cập các biến được khai báo trong phạm vi của hàm "grand Parent" từ vựng. Do đó, nếu có một hàm hđược định nghĩa trong hàmg

function f()
{//begin of scope f
  function g()
  {//being of scope g
    function h()
    {//being of scope h
      /*...*/
    }//end of scope h
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

sau đó hsẽ có thể truy cập vào tất cả các biến được khai báo trong phạm vi chức năng h, gf. Điều này được thực hiện với việc đóng cửa . Trong các bao đóng JavaScript cho phép chúng ta truy cập bất kỳ biến nào được khai báo trong hàm cha từ vựng, trong hàm cha từ vựng lớn, trong hàm cha lớn từ vựng, v.v. Điều này có thể được xem như là một chuỗi phạm vi ; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... cho đến khi hàm cha cuối cùng không có cha mẹ từ vựng.

Đối tượng cửa sổ

Trên thực tế, chuỗi không dừng ở chức năng cha mẹ cuối cùng. Có một phạm vi đặc biệt hơn; các phạm vi toàn cầu . Mỗi biến không được khai báo trong hàm được coi là khai báo trong phạm vi toàn cục. Phạm vi toàn cầu có hai chuyên ngành;

  • mọi biến được khai báo trong phạm vi toàn cầu đều có thể truy cập ở mọi nơi
  • các biến được khai báo trong phạm vi toàn cục tương ứng với các thuộc tính của windowđối tượng.

Do đó, có chính xác hai cách khai báo một biến footrong phạm vi toàn cầu; hoặc bằng cách không khai báo nó trong một hàm hoặc bằng cách đặt thuộc tính foocủa đối tượng cửa sổ.

Cả hai lần thử đều sử dụng số lần đóng

Bây giờ bạn đã đọc một lời giải thích chi tiết hơn, bây giờ có thể rõ ràng rằng cả hai giải pháp đều sử dụng các bao đóng. Nhưng để chắc chắn, hãy làm một bằng chứng.

Hãy tạo một ngôn ngữ lập trình mới; JavaScript-Không đóng cửa. Như tên cho thấy, JavaScript-No-Clos giống hệt với JavaScript trừ khi nó không hỗ trợ Đóng.

Nói cách khác;

var foo = 'hello';
function f(){console.log(foo)};
f();
//JavaScript-No-Closure prints undefined
//JavaSript prints hello

Được rồi, hãy xem điều gì xảy ra với giải pháp đầu tiên với JavaScript-Không đóng cửa;

for(var i = 0; i < 10; i++) {
  (function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2); //i2 is undefined in JavaScript-No-Closure 
    }, 1000)
  })();
}

do đó, điều này sẽ in undefined10 lần trong JavaScript-Không đóng.

Do đó giải pháp đầu tiên sử dụng đóng cửa.

Hãy xem xét giải pháp thứ hai;

for(var i = 0; i < 10; i++) {
  setTimeout((function(i2){
    return function() {
        console.log(i2); //i2 is undefined in JavaScript-No-Closure
    }
  })(i), 1000);
}

do đó, điều này sẽ in undefined10 lần trong JavaScript-Không đóng.

Cả hai giải pháp sử dụng đóng cửa.

Chỉnh sửa: Giả định rằng 3 đoạn mã này không được xác định trong phạm vi toàn cầu. Mặt khác, các biến fooisẽ được liên kết với windowđối tượng và do đó có thể truy cập thông qua windowđối tượng trong cả JavaScript và JavaScript-Không đóng.


Tại sao ikhông xác định? Bạn chỉ cần tham khảo phạm vi cha, vẫn còn hiệu lực nếu không có bao đóng.
leeme

vì lý do tương tự như foo không được xác định trong JavaScript-Không đóng. <code> i </ code> không được xác định trong JavaScript nhờ một tính năng trong JavaScript cho phép truy cập các biến được xác định trong cha mẹ từ vựng. Tính năng này được gọi là đóng cửa.
sáng chói

Bạn đã không hiểu sự khác biệt giữa việc tham chiếu đến các biến đã được xác định và các biến miễn phí . Trong các bao đóng, chúng tôi xác định các biến miễn phí phải được ràng buộc trong bối cảnh bên ngoài. Trong mã của bạn, bạn chỉ cần đặt i2 thành itại thời điểm khi bạn xác định chức năng của mình. Điều này làm cho iKHÔNG phải là một biến miễn phí. Tuy nhiên, chúng tôi coi chức năng của bạn là một bao đóng, nhưng không có bất kỳ biến miễn phí nào, đó là điểm chính.
leeme

2
@leeme, tôi đồng ý. Và so với câu trả lời được chấp nhận, điều này không thực sự cho thấy những gì đang thực sự xảy ra. :)
Abel

3
Tôi nghĩ rằng đây là câu trả lời tốt nhất, giải thích các bao đóng nói chung và đơn giản và sau đó đi vào trường hợp sử dụng cụ thể. cảm ơn!
tim peterson 18/03/13

22

Tôi chưa bao giờ hài lòng với cách mọi người giải thích điều này.

Chìa khóa để hiểu các bao đóng là hiểu được JS sẽ như thế nào nếu không đóng.

Nếu không đóng cửa, điều này sẽ gây ra lỗi

function outerFunc(){
    var outerVar = 'an outerFunc var';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); //returns inner function and fires it

Khi outsFunc đã trở lại trong phiên bản JavaScript bị vô hiệu hóa đóng cửa tưởng tượng, tham chiếu đến outsVar sẽ là rác được thu thập và không để lại gì cho func bên trong tham chiếu.

Đóng cửa về cơ bản là các quy tắc đặc biệt khởi động và làm cho các vars đó tồn tại khi một hàm bên trong tham chiếu các biến của hàm ngoài. Với các lần đóng, các vars được tham chiếu được duy trì ngay cả sau khi chức năng bên ngoài được thực hiện hoặc 'đóng' nếu điều đó giúp bạn ghi nhớ điểm.

Ngay cả khi đóng cửa, vòng đời của các vars cục bộ trong một hàm không có chức năng bên trong tham chiếu đến các địa phương của nó hoạt động giống như trong phiên bản không đóng. Khi chức năng kết thúc, người dân địa phương sẽ thu gom rác.

Khi bạn có một tham chiếu trong một func bên trong đến một var bên ngoài, tuy nhiên, nó giống như một khung cửa được đưa vào cách thu gom rác cho các vars được tham chiếu.

Một cách có lẽ chính xác hơn để xem xét các bao đóng, là về cơ bản, hàm bên trong sử dụng phạm vi bên trong như là foudnation phạm vi riêng của nó.

Nhưng bối cảnh được tham chiếu là trên thực tế, liên tục, không giống như một ảnh chụp nhanh. Liên tục bắn một hàm bên trong được trả về, tiếp tục tăng và ghi nhật ký var cục bộ của hàm ngoài sẽ tiếp tục cảnh báo các giá trị cao hơn.

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2

Bạn nói đúng về 'ảnh chụp nhanh' (tôi nghĩ rằng, bạn đề cập đến câu trả lời của tôi) bởi điều đó. Tôi đang tìm kiếm một từ sẽ chỉ ra hành vi. Trong ví dụ của bạn, nó có thể được xem như là một cấu trúc đóng 'hotlink'. Khi bắt đóng bao là tham số trong hàm bên trong, người ta có thể nói nó hoạt động như một 'ảnh chụp nhanh'. Nhưng tôi đồng ý, những từ bị lạm dụng chỉ làm tăng thêm sự nhầm lẫn cho chủ đề. Nếu bạn có bất kỳ đề nghị về điều đó, tôi sẽ cập nhật câu trả lời của tôi.
Andries

Nó có thể giúp giải thích nếu bạn cho hàm bên trong là một hàm được đặt tên.
Phillip Senn 18/03/13

Nếu không đóng cửa, bạn sẽ gặp lỗi vì bạn đang cố sử dụng một biến không tồn tại.
Juan Mendes

Hmm ... điểm tốt. Có phải việc tham chiếu một var không xác định đã bao giờ không gây ra lỗi vì cuối cùng nó sẽ tìm kiếm như một tài sản trên đối tượng toàn cầu hay tôi nhầm lẫn với việc gán cho các vars không xác định?
Erik Reppen 6/07/2015

17

Cả hai bạn đang sử dụng đóng cửa.

Tôi đang đi với định nghĩa Wikipedia ở đây:

Trong khoa học máy tính, một bao đóng (cũng là đóng từ vựng hoặc đóng hàm) là một hàm hoặc tham chiếu đến một hàm cùng với một môi trường tham chiếu. Một bảng lưu trữ một tham chiếu đến từng biến không cục bộ (còn gọi là biến tự do) của hàm đó . Một bao đóng không giống như một con trỏ hàm đơn giản, cho phép một hàm truy cập vào các biến không cục bộ đó ngay cả khi được gọi bên ngoài phạm vi từ vựng ngay lập tức của nó.

Nỗ lực của bạn của bạn rõ ràng sử dụng biến ikhông phải cục bộ bằng cách lấy giá trị của nó và tạo một bản sao để lưu trữ vào cục bộ i2.

Nỗ lực của bạn chuyển i(mà tại trang web cuộc gọi nằm trong phạm vi) cho một chức năng ẩn danh làm đối số. Đây không phải là một bao đóng cho đến nay, nhưng sau đó hàm đó trả về một hàm khác tham chiếu tương tựi2 . Vì bên trong hàm ẩn danh bên trong i2không phải là cục bộ, điều này tạo ra một bao đóng.


Vâng, nhưng tôi nghĩ vấn đề là cách anh ấy làm việc đó. Ông chỉ là bản sao iđể i2, sau đó xác định một số logic và thực hiện chức năng này. Nếu tôi không thực hiện nó ngay lập tức, nhưng lưu nó trong một var và thực hiện nó sau vòng lặp, nó sẽ in 10, phải không? Vì vậy, nó đã không chụp tôi.
leeme

6
@leeme: Nó bắt itốt. Hành vi bạn đang mô tả không phải là kết quả của việc đóng cửa và không đóng cửa; trong khi đó, đó là kết quả của biến đóng. Bạn đang làm điều tương tự bằng cách sử dụng cú pháp khác nhau bằng cách gọi ngay một hàm và chuyển inhư một đối số (sao chép giá trị hiện tại của nó tại chỗ). Nếu bạn đặt cái riêng của mình setTimeoutvào trong cái khác setTimeoutthì điều tương tự sẽ xảy ra.
Jon

13

Cả bạn và bạn của bạn đều sử dụng các bao đóng:

Một bao đóng là một loại đối tượng đặc biệt kết hợp hai thứ: một hàm và môi trường mà hàm đó được tạo. Môi trường bao gồm bất kỳ biến cục bộ nào trong phạm vi tại thời điểm đóng được tạo.

MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

Trong hàm mã của bạn bè bạn function(){ console.log(i2); }được xác định bên trong đóng hàm ẩn danh function(){ var i2 = i; ...và có thể đọc / ghi biến cục bộ i2.

Trong hàm mã của bạn function(){ console.log(i2); }được xác định bên trong hàm đóng function(i2){ return ...và có thể đọc / ghi giá trị cục bộ i2(được khai báo trong trường hợp này là tham số).

Trong cả hai trường hợp chức năng function(){ console.log(i2); }sau đó được truyền vào setTimeout.

Một tương đương khác (nhưng với việc sử dụng ít bộ nhớ hơn) là:

function fGenerator(i2){
    return function(){
        console.log(i2);
    }
}
for(var i = 0; i < 10; i++) {
    setTimeout(fGenerator(i), 1000);
}

1
Tôi không thấy lý do tại sao giải pháp của bạn so với giải pháp của bạn tôi "nhanh hơn và sử dụng ít bộ nhớ hơn", bạn có thể nói rõ hơn không?
sáng

3
Trong giải pháp của bạn, bạn tạo 20 đối tượng hàm (2 đối tượng trên mỗi vòng lặp: 2x10 = 20). Kết quả tương tự trong giải pháp của bạn. Trong giải pháp "của tôi" chỉ có 11 đối tượng hàm được tạo: 1 trước cho vòng lặp và 10 "bên trong" - 1 + 1x10 = 11. Kết quả là - sử dụng ít bộ nhớ hơn và tăng tốc độ.
Andrew D.

1
Về lý thuyết, điều đó sẽ đúng. Trong thực tế, cũng: Xem tiêu chuẩn này của JSPerf: jsperf.com/clenses-vs-name-feft-in-a-loop/2
Rob W

10

Khép kín

Một bao đóng không phải là một chức năng, và không phải là một biểu thức. Nó phải được xem như một loại 'ảnh chụp nhanh' từ các biến được sử dụng bên ngoài kính viễn vọng và được sử dụng bên trong hàm. Về mặt ngữ pháp, người ta nên nói: 'hãy đóng các biến'.

Một lần nữa, nói cách khác: Một bao đóng là một bản sao của bối cảnh có liên quan của các biến mà hàm phụ thuộc vào.

Một lần nữa (naïf): Một bao đóng có quyền truy cập vào các biến không được truyền dưới dạng tham số.

Hãy nhớ rằng các khái niệm chức năng này phụ thuộc mạnh mẽ vào ngôn ngữ lập trình / môi trường bạn sử dụng. Trong JavaScript, việc đóng cửa phụ thuộc vào phạm vi từ vựng (điều này đúng trong hầu hết các ngôn ngữ c).

Vì vậy, trả về một chức năng chủ yếu là trả về một chức năng ẩn danh / không tên. Khi các biến truy cập hàm, không được truyền dưới dạng tham số và trong phạm vi (từ vựng) của nó, một bao đóng đã được thực hiện.

Vì vậy, liên quan đến ví dụ của bạn:

// 1
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // closure, only when loop finishes within 1000 ms,
    }, 1000);           // i = 10 for all functions
}
// 2
for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i; // closure of i (lexical scope: for-loop)
        setTimeout(function(){
            console.log(i2); // closure of i2 (lexical scope:outer function)
        }, 1000)
    })();
}
// 3
for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2); // closure of i2 (outer scope)

        }
    })(i), 1000); // param access i (no closure)
}

Tất cả đang sử dụng đóng cửa. Đừng nhầm lẫn điểm thực hiện với các bao đóng. Nếu 'ảnh chụp nhanh' của các lần đóng được thực hiện không đúng lúc, các giá trị có thể bất ngờ nhưng chắc chắn là một lần đóng được thực hiện!



10

Hãy xem xét cả hai cách:

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();

Khai báo và thực hiện ngay một hàm ẩn danh chạy setTimeout()trong ngữ cảnh của chính nó. Giá trị hiện tại của iđược bảo tồn bằng cách tạo một bản sao thành i2đầu tiên; nó hoạt động vì thực hiện ngay lập tức.

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);

Khai báo một bối cảnh thực thi cho hàm bên trong, theo đó giá trị hiện tại của iđược bảo toàn thànhi2 ; phương pháp này cũng sử dụng thực thi ngay lập tức để bảo tồn giá trị.

Quan trọng

Cần phải đề cập rằng ngữ nghĩa chạy KHÔNG giống nhau giữa cả hai cách tiếp cận; chức năng bên trong của bạn được chuyển đến setTimeout()trong khi chức năng bên trong của anh ta gọisetTimeout() chính nó.

Gói cả hai mã bên trong mã khác setTimeout() không chứng minh rằng chỉ có cách tiếp cận thứ hai sử dụng các bao đóng, không có gì giống nhau để bắt đầu.

Phần kết luận

Cả hai phương pháp đều sử dụng các bao đóng, vì vậy nó đi theo sở thích cá nhân; cách tiếp cận thứ hai dễ dàng hơn để "di chuyển" xung quanh hoặc khái quát hóa.


Tôi nghĩ rằng sự khác biệt là: Giải pháp của anh ấy (thứ 1) được nắm bắt bởi tham chiếu, của tôi (thứ 2) là nắm bắt theo giá trị. Trong trường hợp này, nó không tạo ra sự khác biệt, nhưng nếu tôi đặt thực thi vào một setTimeout khác, chúng ta sẽ thấy rằng giải pháp của anh ta có vấn đề là sau đó nó sử dụng giá trị cuối cùng của i, chứ không phải hiện tại, trong khi ngưỡng của tôi sử dụng giá trị hiện tại (kể từ khi bị bắt bởi giá trị).
leeme

@leeme Cả hai bạn chụp theo cùng một cách; chuyển một biến thông qua đối số chức năng hoặc gán là cùng một điều ... bạn có thể thêm vào câu hỏi của bạn làm thế nào bạn sẽ bọc thực thi trong một khác setTimeout()?
Ja͢ck

hãy để tôi kiểm tra điều này ... Tôi muốn chỉ ra rằng đối tượng hàm có thể được truyền xung quanh và biến ban đầu icó thể được thay đổi mà không ảnh hưởng đến chức năng in ra, không phụ thuộc vào nơi hoặc khi chúng ta thực thi nó.
leeme

Đợi đã, bạn không chuyển một hàm cho setTimeout (bên ngoài). Loại bỏ những cái đó (), do đó truyền một hàm và bạn thấy đầu ra gấp 10 lần 10.
leeme

@leeme Như đã đề cập trước đây, ()chính xác là những gì làm cho mã của anh ấy hoạt động, giống như của bạn (i); bạn không chỉ bọc mã của anh ấy, bạn đã thay đổi mã đó .. do đó bạn không thể so sánh hợp lệ nữa.
Ja͢ck

8

Tôi đã viết điều này một thời gian trước để nhắc nhở bản thân về việc đóng cửa là gì và cách thức hoạt động trong JS.

Một bao đóng là một hàm, khi được gọi, sử dụng phạm vi mà nó được khai báo, không phải là phạm vi mà nó được gọi. Trong javaScript, tất cả các hàm hoạt động như thế này. Các giá trị biến trong một phạm vi vẫn tồn tại miễn là có một hàm vẫn trỏ đến chúng. Ngoại lệ cho quy tắc là 'this', dùng để chỉ đối tượng mà hàm nằm bên trong khi nó được gọi.

var z = 1;
function x(){
    var z = 2; 
    y(function(){
      alert(z);
    });
}
function y(f){
    var z = 3;
    f();
}
x(); //alerts '2' 

6

Sau khi kiểm tra chặt chẽ, có vẻ như cả hai bạn đang sử dụng đóng cửa.

Trong trường hợp bạn bè của bạn, iđược truy cập bên trong chức năng ẩn danh 1 và i2được truy cập trong chức năng ẩn danh 2 nơi console.logcó mặt.

Trong trường hợp của bạn, bạn đang truy cập i2bên trong chức năng ẩn danh, nơi console.logcó mặt. Thêm một debugger;câu lệnh trước console.logvà trong các công cụ dành cho nhà phát triển chrome trong "Biến phạm vi", nó sẽ cho biết biến đó thuộc phạm vi nào.


2
Phần "Đóng cửa" ở bảng bên phải được sử dụng vì không có tên cụ thể hơn. "Địa phương" là một dấu hiệu mạnh hơn "Đóng cửa".
Cướp W


4

Hãy xem xét những điều sau đây. Điều này tạo và tạo lại một chức năng fđóng i, nhưng những chức năng khác nhau!:

i=100;

f=function(i){return function(){return ++i}}(0);
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

f=function(i){return new Function('return ++i')}(0);        /*  function declarations ~= expressions! */
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

trong khi phần sau đóng trên "một" chức năng "chính nó"
(chính nó! đoạn trích sau này sử dụng một tham chiếu duy nhất f)

for(var i = 0; i < 10; i++) {
    setTimeout( new Function('console.log('+i+')'),  1000 );
}

hoặc để rõ ràng hơn:

for(var i = 0; i < 10; i++) {
    console.log(    f = new Function( 'console.log('+i+')' )    );
    setTimeout( f,  1000 );
}

Lưu ý định nghĩa cuối cùng ffunction(){ console.log(9) } trước 0 được in.

Hãy cẩn thận! Khái niệm đóng cửa có thể là một sự phân tâm cưỡng chế từ bản chất của lập trình cơ bản:

for(var i = 0; i < 10; i++) {     setTimeout( 'console.log('+i+')',  1000 );      }

x-refs.:
Làm thế nào để đóng JavaScript hoạt động?
Giải thích về đóng cửa Javascript Việc đóng
(JS) có yêu cầu một chức năng bên trong chức năng
Làm thế nào để hiểu các bao đóng trong Javascript không?
Javascript nhầm lẫn biến cục bộ và toàn cầu


đoạn thử cho thời gian 1 - không chắc chắn làm thế nào để kiểm soát - Run' only was desired - not sure how to remove the Copy`
ekim

-1

Tôi muốn chia sẻ ví dụ của tôi và một lời giải thích về việc đóng cửa. Tôi đã làm một ví dụ về con trăn và hai hình để chứng minh trạng thái ngăn xếp.

def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n * margin_top, a * n, 
            ' ‘ * padding, msg, '  * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)

f('hello')
g(‘good bye!')

Đầu ra của mã này sẽ như sau:

*****      hello      #####

      good bye!    ♥♥♥

Dưới đây là hai hình để hiển thị ngăn xếp và đóng cửa được gắn vào đối tượng chức năng.

khi chức năng được trả về từ hãng sản xuất

khi hàm được gọi sau

Khi hàm được gọi thông qua một tham số hoặc một biến không nhắm mục tiêu, mã cần các ràng buộc biến cục bộ như margin_top, padding cũng như a, b, n. Để đảm bảo mã chức năng hoạt động, có thể truy cập khung ngăn xếp của hàm tạo đã bị loại bỏ từ lâu, được sao lưu trong bao đóng mà chúng ta có thể tìm thấy cùng với đối tượng thông báo hàm.


Tôi muốn loại bỏ câu trả lời này. Tôi nhận ra rằng câu hỏi không phải là về những gì đang đóng cửa, vì vậy tôi muốn chuyển nó sang câu hỏi khác.
Eunjung Lee

2
Tôi tin rằng bạn có khả năng xóa nội dung của riêng bạn. Nhấp vào deleteliên kết dưới câu trả lời.
Rory McCrossan
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.