'Đóng cửa' là gì?


432

Tôi đã hỏi một câu hỏi về Currying và đóng cửa đã được đề cập. Đóng cửa là gì? Làm thế nào nó liên quan đến cà ri?


22
Bây giờ chính xác những gì đóng cửa ??? Một số câu trả lời cho biết, việc đóng cửa là chức năng. Một số người nói đó là chồng. Một số câu trả lời cho biết, đó là giá trị "ẩn". Theo hiểu biết của tôi, nó là hàm + biến kèm theo.
Roland

3
Giải thích đóng cửa là gì: stackoverflow.com/questions/4103750/ từ
dietbuddha

Cũng có một cái nhìn về đóng cửa là gì? tại softwareengineering.stackexchange
B12Toaster

Giải thích thế nào là đóng cửa và trường hợp sử dụng phổ biến: trungk18.com/experience/javascript-clenses
Sasuke91

Câu trả lời:


742

Phạm vi biến đổi

Khi bạn khai báo một biến cục bộ, biến đó có một phạm vi. Nói chung, các biến cục bộ chỉ tồn tại trong khối hoặc hàm mà bạn khai báo chúng.

function() {
  var a = 1;
  console.log(a); // works
}    
console.log(a); // fails

Nếu tôi cố gắng truy cập một biến cục bộ, hầu hết các ngôn ngữ sẽ tìm kiếm nó trong phạm vi hiện tại, sau đó đi qua phạm vi cha mẹ cho đến khi chúng đạt đến phạm vi gốc.

var a = 1;
function() {
  console.log(a); // works
}    
console.log(a); // works

Khi một khối hoặc hàm được thực hiện, các biến cục bộ của nó không còn cần thiết nữa và thường bị xóa khỏi bộ nhớ.

Đây là cách chúng ta thường mong đợi mọi thứ hoạt động.

Một bao đóng là một phạm vi biến cục bộ liên tục

Một bao đóng là một phạm vi liên tục giữ các biến cục bộ ngay cả sau khi thực thi mã đã di chuyển ra khỏi khối đó. Các ngôn ngữ hỗ trợ đóng (như JavaScript, Swift và Ruby) sẽ cho phép bạn giữ tham chiếu đến một phạm vi (bao gồm cả phạm vi chính của nó), ngay cả sau khi khối đó được khai báo đã thực hiện xong, miễn là bạn giữ tham chiếu đến khối hoặc chức năng đó ở đâu đó.

Đối tượng phạm vi và tất cả các biến cục bộ của nó được gắn với hàm và sẽ tồn tại miễn là hàm đó vẫn tồn tại.

Điều này cung cấp cho chúng tôi tính di động chức năng. Chúng ta có thể mong đợi bất kỳ biến nào có trong phạm vi khi hàm được xác định đầu tiên vẫn nằm trong phạm vi khi chúng ta gọi hàm sau, ngay cả khi chúng ta gọi hàm trong ngữ cảnh hoàn toàn khác.

Ví dụ

Đây là một ví dụ thực sự đơn giản trong JavaScript minh họa điểm:

outer = function() {
  var a = 1;
  var inner = function() {
    console.log(a);
  }
  return inner; // this returns a function
}

var fnc = outer(); // execute outer to get inner 
fnc();

Ở đây tôi đã định nghĩa một hàm trong một hàm. Hàm bên trong có quyền truy cập vào tất cả các biến cục bộ của hàm ngoài, bao gồm a. Các biến alà trong phạm vi cho các chức năng bên trong.

Thông thường khi một chức năng thoát ra, tất cả các biến cục bộ của nó bị thổi bay. Tuy nhiên, nếu chúng ta trả về hàm bên trong và gán nó cho một biến fncđể nó tồn tại sau khi outerthoát, tất cả các biến trong phạm vi khi innerđược xác định cũng tồn tại . Biến ađã được đóng lại - nó nằm trong một bao đóng.

Lưu ý rằng biến alà hoàn toàn riêng tư fnc. Đây là cách tạo các biến riêng tư trong ngôn ngữ lập trình chức năng như JavaScript.

Như bạn có thể đoán, khi tôi gọi fnc()nó sẽ in giá trị của a, đó là "1".

Trong một ngôn ngữ không có đóng, biến asẽ là rác được thu thập và vứt đi khi hàm outerthoát. Gọi fnc sẽ có lỗi vì akhông còn tồn tại.

Trong JavaScript, biến avẫn tồn tại vì phạm vi biến được tạo khi hàm được khai báo lần đầu và tồn tại miễn là hàm tiếp tục tồn tại.

athuộc phạm vi của outer. Phạm vi của innercó một con trỏ cha mẹ đến phạm vi outer. fnclà một biến mà trỏ đến inner. aVẫn tồn tại miễn là fncvẫn tồn tại. alà trong đóng cửa.


116
Tôi nghĩ rằng đây là một ví dụ khá tốt và dễ hiểu.
user12345613

16
Cảm ơn lời giải thích tuyệt vời, tôi đã thấy nhiều nhưng đây là lúc tôi thực sự hiểu nó.
Dimitar Dimitrov

2
Tôi có thể có một ví dụ về cách thức hoạt động của nó trong một thư viện như JQuery như đã nêu trong đoạn 2 đến đoạn cuối không? Tôi hoàn toàn không hiểu điều đó.
DPM

6
Xin chào Jubbat, vâng, mở jquery.js và xem dòng đầu tiên. Bạn sẽ thấy một chức năng được mở. Bây giờ bỏ qua đến cuối, bạn sẽ thấy window.jQuery = window. $ = JQuery. Sau đó, chức năng được đóng lại và tự thực hiện. Bây giờ bạn có quyền truy cập vào hàm $, đến lượt nó có quyền truy cập vào các hàm khác được xác định trong bao đóng. Câu trả lời đó có đáp ứng được câu hỏi của bạn không?
siêu sáng

4
Giải thích tốt nhất trên web. Cách đơn giản hơn tôi nghĩ
Thần chú

95

Tôi sẽ đưa ra một ví dụ (bằng JavaScript):

function makeCounter () {
  var count = 0;
  return function () {
    count += 1;
    return count;
  }
}

var x = makeCounter();

x(); returns 1

x(); returns 2

...etc...

Hàm này, makeCorer, là gì, nó trả về một hàm, mà chúng ta đã gọi là x, sẽ được tính bằng một lần mỗi lần nó được gọi. Vì chúng tôi không cung cấp bất kỳ tham số nào cho x nên bằng cách nào đó phải nhớ số đếm. Nó biết nơi tìm nó dựa trên cái được gọi là phạm vi từ vựng - nó phải tìm đến vị trí được xác định để tìm giá trị. Giá trị "ẩn" này là cái được gọi là bao đóng.

Đây là ví dụ cà ri của tôi một lần nữa:

function add (a) {
  return function (b) {
    return a + b;
  }
}

var add3 = add(3);

add3(4); returns 7

Những gì bạn có thể thấy là khi bạn gọi add với tham số a (là 3), giá trị đó được chứa trong bao đóng của hàm trả về mà chúng ta xác định là add3. Theo cách đó, khi chúng ta gọi add3, nó biết nơi tìm giá trị để thực hiện phép cộng.


4
IDK, ngôn ngữ nào (có thể là F #) bạn đã sử dụng ngôn ngữ trên. Xin vui lòng cho ví dụ trên trong mã giả? Tôi đang có thời gian khó khăn để hiểu điều này.
người dùng


3
@KyleCronin Ví dụ tuyệt vời, cảm ơn. Q: Có đúng hơn không khi nói "giá trị ẩn được gọi là bao đóng" hay "hàm ẩn giá trị là bao đóng"? Hay "quá trình che giấu giá trị là sự đóng cửa"? Cảm ơn!

2
@RobertHume Câu hỏi hay. Về mặt ngữ nghĩa, thuật ngữ "đóng cửa" có phần mơ hồ. Định nghĩa cá nhân của tôi là sự kết hợp của cả giá trị ẩn và việc sử dụng hàm kèm theo của nó tạo thành sự đóng cửa.
Kyle Cronin

1
@KyleCronin Cảm ơn - Tôi có một Đề án giữa kỳ vào thứ Hai. :) Muốn có khái niệm "đóng cửa" vững chắc trong đầu tôi. Cảm ơn bạn đã đăng câu trả lời tuyệt vời này cho câu hỏi của OP!

58

Câu trả lời của Kyle khá hay. Tôi nghĩ rằng sự làm rõ bổ sung duy nhất là việc đóng về cơ bản là một ảnh chụp nhanh của ngăn xếp tại điểm mà hàm lambda được tạo. Sau đó, khi chức năng được thực hiện lại, ngăn xếp được khôi phục về trạng thái đó trước khi thực hiện chức năng. Do đó, như Kyle đề cập, giá trị ẩn ( count) đó khả dụng khi hàm lambda thực thi.


14
Đó không chỉ là ngăn xếp - đó là phạm vi từ vựng kèm theo được bảo tồn, bất kể chúng được lưu trữ trên ngăn xếp hay đống (hoặc cả hai).
Matt Fenwick

38

Trước hết, trái với những gì hầu hết những người ở đây nói với bạn, đóng cửa không phải là một chức năng ! Vậy nó gì?
Nó là một tập hợp các ký hiệu được định nghĩa trong "bối cảnh xung quanh" của hàm (được gọi là môi trường của nó ) làm cho nó là một biểu thức ĐÓNG (nghĩa là một biểu thức trong đó mọi ký hiệu được xác định và có giá trị, do đó nó có thể được đánh giá).

Ví dụ: khi bạn có chức năng JavaScript:

function closed(x) {
  return x + 3;
}

nó là một biểu thức đóng bởi vì tất cả các biểu tượng xuất hiện trong nó được định nghĩa trong đó (ý nghĩa của chúng là rõ ràng), vì vậy bạn có thể đánh giá nó. Nói cách khác, nó là khép kín .

Nhưng nếu bạn có một chức năng như thế này:

function open(x) {
  return x*y + 3;
}

nó là một biểu thức mở vì có những ký hiệu trong đó chưa được định nghĩa trong đó. Cụ thể, y. Khi xem xét hàm này, chúng ta không thể biết yý nghĩa của nó là gì và chúng ta không biết giá trị của nó, vì vậy chúng ta không thể đánh giá biểu thức này. Tức là chúng ta không thể gọi hàm này cho đến khi chúng ta nói ynghĩa của nó là gì. Đây yđược gọi là một biến miễn phí .

Điều này yđòi hỏi một định nghĩa, nhưng định nghĩa này không phải là một phần của hàm - nó được định nghĩa ở một nơi khác, trong "bối cảnh xung quanh" (còn được gọi là môi trường ). Ít nhất đó là những gì chúng ta hy vọng: P

Ví dụ: nó có thể được định nghĩa trên toàn cầu:

var y = 7;

function open(x) {
  return x*y + 3;
}

Hoặc nó có thể được định nghĩa trong một hàm bao bọc nó:

var global = 2;

function wrapper(y) {
  var w = "unused";

  return function(x) {
    return x*y + 3;
  }
}

Một phần của môi trường cung cấp các biến miễn phí trong biểu thức nghĩa của chúng là đóng . Nó được gọi theo cách này, bởi vì nó biến một biểu thức mở thành một biểu thức đóng , bằng cách cung cấp các định nghĩa còn thiếu này cho tất cả các biến miễn phí của nó , để chúng ta có thể đánh giá nó.

Trong ví dụ trên, hàm bên trong (mà chúng ta không đặt tên vì chúng ta không cần nó) là một biểu thức mở vì biến ytrong nó là miễn phí - định nghĩa của nó nằm ngoài hàm, trong hàm bao bọc nó . Các môi trường cho rằng chức năng ẩn danh là tập hợp các biến:

{
  global: 2,
  w: "unused",
  y: [whatever has been passed to that wrapper function as its parameter `y`]
}

Bây giờ, bao đóng là một phần của môi trường này đóng hàm bên trong bằng cách cung cấp các định nghĩa cho tất cả các biến miễn phí của nó . Trong trường hợp của chúng ta, biến tự do duy nhất trong hàm bên trong là y, vì vậy việc đóng hàm đó là tập hợp con của môi trường của nó:

{
  y: [whatever has been passed to that wrapper function as its parameter `y`]
}

Hai biểu tượng khác được xác định trong môi trường không phải là một phần của việc đóng hàm đó, vì nó không yêu cầu chúng chạy. Họ không cần thiết phải đóng nó.

Tìm hiểu thêm về lý thuyết đằng sau đó tại đây: https://stackoverflow.com/a/36878651/434562

Cần lưu ý rằng trong ví dụ trên, hàm bao trả về hàm bên trong của nó dưới dạng giá trị. Thời điểm chúng ta gọi hàm này có thể được điều khiển từ xa theo thời gian kể từ thời điểm hàm được xác định (hoặc được tạo). Cụ thể, chức năng gói của nó không còn chạy nữa và các tham số của nó trên ngăn xếp cuộc gọi không còn nữa: P Điều này gây ra sự cố, bởi vì chức năng bên trong cần yphải ở đó khi được gọi! Nói cách khác, nó yêu cầu các biến từ đóng của nó để bằng cách nào đó tồn tại lâu hơn chức năng bao bọc và có mặt khi cần thiết. Do đó, hàm bên trong phải tạo một ảnh chụp nhanh các biến này để đóng và lưu trữ chúng ở nơi nào đó an toàn để sử dụng sau này. (Một nơi nào đó bên ngoài ngăn xếp cuộc gọi.)

Và đây là lý do tại sao mọi người thường nhầm lẫn thuật ngữ đóng là loại hàm đặc biệt có thể thực hiện các ảnh chụp nhanh như vậy của các biến bên ngoài mà họ sử dụng hoặc cấu trúc dữ liệu được sử dụng để lưu trữ các biến này cho sau này. Nhưng tôi hy vọng bạn hiểu rằng bây giờ chúng không phảibao đóng - chúng chỉ là cách để thực hiện các bao đóng trong ngôn ngữ lập trình hoặc các cơ chế ngôn ngữ cho phép các biến từ đóng của hàm có mặt khi cần. Có rất nhiều quan niệm sai lầm xung quanh việc đóng cửa (không cần thiết) làm cho chủ đề này trở nên khó hiểu và phức tạp hơn nhiều so với thực tế.


1
Một sự tương tự có thể giúp người mới bắt đầu điều này là một sự đóng cửa gắn kết tất cả các kết thúc lỏng lẻo , đó là những gì một người làm khi họ tìm cách đóng cửa (hoặc nó giải quyết tất cả các tài liệu tham khảo cần thiết, hoặc ...). Chà, nó giúp tôi nghĩ về nó theo cách đó: o)
Will Crawford

Tôi đã đọc rất nhiều định nghĩa về việc đóng cửa trong nhiều năm qua, nhưng tôi nghĩ rằng đây là định nghĩa yêu thích của tôi cho đến nay. Tôi đoán tất cả chúng ta đều có cách riêng để lập bản đồ các khái niệm như thế này và điều này rất giống với tôi.
Jason S.

29

Một bao đóng là một chức năng có thể tham chiếu trạng thái trong một chức năng khác. Ví dụ, trong Python, cái này sử dụng bao đóng "bên trong":

def outer (a):
    b = "variable in outer()"
    def inner (c):
        print a, b, c
    return inner

# Now the return value from outer() can be saved for later
func = outer ("test")
func (1) # prints "test variable in outer() 1

23

Để giúp tạo điều kiện cho sự hiểu biết về việc đóng cửa, có thể hữu ích để kiểm tra cách chúng có thể được thực hiện bằng ngôn ngữ thủ tục. Giải thích này sẽ tuân theo việc thực hiện đơn giản các bao đóng trong Đề án.

Để bắt đầu, tôi phải giới thiệu khái niệm về một không gian tên. Khi bạn nhập lệnh vào trình thông dịch Scheme, nó phải đánh giá các ký hiệu khác nhau trong biểu thức và lấy giá trị của chúng. Thí dụ:

(define x 3)

(define y 4)

(+ x y) returns 7

Các biểu thức xác định lưu trữ giá trị 3 tại chỗ cho x và giá trị 4 tại chỗ cho y. Sau đó, khi chúng ta gọi (+ xy), trình thông dịch sẽ tra cứu các giá trị trong không gian tên và có thể thực hiện thao tác và trả về 7.

Tuy nhiên, trong Lược đồ có các biểu thức cho phép bạn tạm thời ghi đè giá trị của biểu tượng. Đây là một ví dụ:

(define x 3)

(define y 4)

(let ((x 5))
   (+ x y)) returns 9

x returns 3

Từ khóa let làm gì sẽ giới thiệu một không gian tên mới với x là giá trị 5. Bạn sẽ nhận thấy rằng nó vẫn có thể thấy y là 4, làm cho tổng trở về là 9. Bạn cũng có thể thấy rằng một khi biểu thức đã kết thúc x trở lại là 3. Theo nghĩa này, x đã tạm thời bị che bởi giá trị cục bộ.

Ngôn ngữ thủ tục và hướng đối tượng có một khái niệm tương tự. Bất cứ khi nào bạn khai báo một biến trong hàm có cùng tên với biến toàn cục, bạn sẽ có cùng hiệu ứng.

Làm thế nào chúng ta sẽ thực hiện điều này? Một cách đơn giản là với một danh sách được liên kết - phần đầu chứa giá trị mới và phần đuôi chứa không gian tên cũ. Khi bạn cần tìm kiếm một biểu tượng, bạn bắt đầu từ đầu và đi xuống đuôi.

Bây giờ chúng ta hãy bỏ qua việc thực hiện các chức năng hạng nhất trong thời điểm này. Ít nhiều, một hàm là một tập hợp các lệnh để thực thi khi hàm được gọi là đỉnh trong giá trị trả về. Khi chúng ta đọc trong một hàm, chúng ta có thể lưu trữ các hướng dẫn này phía sau hậu trường và chạy chúng khi hàm được gọi.

(define x 3)

(define (plus-x y)
  (+ x y))

(let ((x 5))
  (plus-x 4)) returns ?

Chúng tôi xác định x là 3 và plus-x là tham số của nó, y, cộng với giá trị của x. Cuối cùng, chúng ta gọi plus-x trong một môi trường trong đó x đã bị che bởi một x mới, giá trị này là 5. Nếu chúng ta chỉ lưu trữ thao tác, (+ xy), cho hàm plus-x, vì chúng ta đang ở trong bối cảnh trong số x là 5 kết quả trả về sẽ là 9. Đây là cái gọi là phạm vi động.

Tuy nhiên, Scheme, Common Lisp và nhiều ngôn ngữ khác có cái gọi là phạm vi từ vựng - ngoài việc lưu trữ thao tác (+ xy), chúng tôi cũng lưu trữ không gian tên tại điểm cụ thể đó. Theo cách đó, khi chúng ta tìm kiếm các giá trị, chúng ta có thể thấy rằng x, trong bối cảnh này, thực sự là 3. Đây là một đóng cửa.

(define x 3)

(define (plus-x y)
  (+ x y))

(let ((x 5))
  (plus-x 4)) returns 7

Tóm lại, chúng ta có thể sử dụng danh sách được liên kết để lưu trữ trạng thái của không gian tên tại thời điểm xác định hàm, cho phép chúng ta truy cập các biến từ bao quanh phạm vi, cũng như cung cấp cho chúng ta khả năng che dấu một biến cục bộ mà không ảnh hưởng đến phần còn lại của chương trình.


được rồi, nhờ câu trả lời của bạn, tôi nghĩ rằng cuối cùng tôi cũng có ý tưởng gì đó về việc đóng cửa. Nhưng có một câu hỏi lớn: "chúng ta có thể sử dụng một danh sách được liên kết để lưu trữ trạng thái của không gian tên tại thời điểm định nghĩa hàm, cho phép chúng ta truy cập các biến mà nếu không sẽ không còn trong phạm vi." Why do we want to access variables that are out of scope? when we say let x = 5, we want x to be 5 and not 3. What is happening?
Lazer

@Laser: Xin lỗi, câu đó không có nhiều ý nghĩa, vì vậy tôi đã cập nhật nó. Tôi hy vọng nó có ý nghĩa hơn bây giờ. Ngoài ra, đừng nghĩ danh sách được liên kết là một chi tiết triển khai (vì nó rất không hiệu quả) mà là một cách đơn giản để khái niệm hóa cách nó có thể được thực hiện.
Kyle Cronin

10

Đây là một ví dụ thực tế về lý do tại sao Closures kick ass ... Đây không phải là mã Javascript của tôi. Hãy để tôi minh họa.

Function.prototype.delay = function(ms /*[, arg...]*/) {
  var fn = this,
      args = Array.prototype.slice.call(arguments, 1);

  return window.setTimeout(function() {
      return fn.apply(fn, args);
  }, ms);
};

Và đây là cách bạn sẽ sử dụng nó:

var startPlayback = function(track) {
  Player.play(track);  
};
startPlayback(someTrack);

Bây giờ hãy tưởng tượng bạn muốn phát lại bắt đầu bị trì hoãn, ví dụ như 5 giây sau khi đoạn mã này chạy. Thật dễ dàng với delaynó và nó đóng cửa:

startPlayback.delay(5000, someTrack);
// Keep going, do other things

Khi bạn gọi delaybằng 5000ms, đoạn mã đầu tiên sẽ chạy và lưu trữ các đối số được truyền trong phần đóng của nó. Sau đó 5 giây, khi cuộc setTimeoutgọi lại xảy ra, việc đóng vẫn duy trì các biến đó, do đó nó có thể gọi hàm ban đầu với các tham số ban đầu.
Đây là một loại cà ri, hoặc trang trí chức năng.

Nếu không đóng, bạn sẽ phải duy trì bằng cách nào đó duy trì các trạng thái biến bên ngoài hàm, do đó, xả rác mã bên ngoài hàm với thứ gì đó thuộc về logic. Sử dụng các bao đóng có thể cải thiện đáng kể chất lượng và khả năng đọc mã của bạn.


1
Cần lưu ý rằng việc mở rộng ngôn ngữ hoặc các đối tượng máy chủ thường được coi là một điều xấu vì chúng là một phần của không gian tên toàn cầu
Jon Cooke

9

Các hàm không chứa biến miễn phí được gọi là các hàm thuần túy.

Các hàm chứa một hoặc nhiều biến miễn phí được gọi là bao đóng.

var pure = function pure(x){
  return x 
  // only own environment is used
}

var foo = "bar"

var closure = function closure(){
  return foo 
  // foo is a free variable from the outer environment
}

src: https://leanpub.com/javascriptallongesix/read#leanpub-auto-if-fifts-without-free-variabled-are-pure-are-closures-impure


Tại sao điều này được sử dụng? Nó thực sự là "đi đúng hướng" hơn với sự khác biệt đó là các biến tự do và biến bị ràng buộc, và các hàm thuần / đóng và hàm không tinh khiết / mở, so với hầu hết các câu trả lời không rõ ràng khác ở đây: P (giảm giá cho việc đóng nhầm lẫn với các hàm đang đóng cửa).
SasQ

Tôi không có ý tưởng, thực sự. Đây là lý do tại sao StackOverflow hút. Chỉ cần nhìn vào nguồn trả lời của tôi. Ai có thể tranh luận với điều đó?
soundyogi

SO không hút và tôi chưa bao giờ nghe về thuật ngữ "biến miễn phí"
Kai

Thật khó để nói về việc đóng cửa mà không đề cập đến các biến miễn phí. Chỉ cần nhìn chúng lên. Thuật ngữ CS tiêu chuẩn.
ComDubh

"Các hàm chứa một hoặc nhiều biến miễn phí được gọi là bao đóng" không phải là một định nghĩa đúng mặc dù - các bao đóng luôn là các đối tượng hạng nhất.
ComDubh

7

tl; dr

Một bao đóng là một hàm và phạm vi của nó được gán cho (hoặc được sử dụng như) một biến. Do đó, việc đóng tên: phạm vi và hàm được bao quanh và được sử dụng giống như bất kỳ thực thể nào khác.

Giải thích sâu về phong cách Wikipedia

Theo Wikipedia, việc đóng cửa là:

Kỹ thuật để thực hiện ràng buộc tên phạm vi từ vựng trong các ngôn ngữ với các chức năng hạng nhất.

Điều đó nghĩa là gì? Hãy xem xét một số định nghĩa.

Tôi sẽ giải thích các bao đóng và các định nghĩa liên quan khác bằng cách sử dụng ví dụ này:

function startAt(x) {
    return function (y) {
        return x + y;
    }
}

var closure1 = startAt(1);
var closure2 = startAt(5);

console.log(closure1(3)); // 4 (x == 1, y == 3)
console.log(closure2(3)); // 8 (x == 5, y == 3)

Chức năng hạng nhất

Về cơ bản điều đó có nghĩa là chúng ta có thể sử dụng các chức năng giống như bất kỳ thực thể nào khác . Chúng ta có thể sửa đổi chúng, chuyển chúng thành đối số, trả về chúng từ các hàm hoặc gán chúng cho các biến. Về mặt kỹ thuật, họ là công dân hạng nhất , do đó có tên: chức năng hạng nhất.

Trong ví dụ trên, startAttrả về một hàm ( ẩn danh ) mà hàm được gán cho closure1closure2. Vì vậy, khi bạn thấy JavaScript xử lý các hàm giống như bất kỳ thực thể nào khác (công dân hạng nhất).

Tên ràng buộc

Liên kết tên là về việc tìm ra dữ liệu tham chiếu (định danh) dữ liệu nào . Phạm vi thực sự quan trọng ở đây, vì đó là điều sẽ quyết định cách thức ràng buộc được giải quyết.

Trong ví dụ trên:

  • Trong phạm vi của hàm ẩn danh bên trong, ybị ràng buộc 3.
  • Trong startAtphạm vi, xbị ràng buộc 1hoặc 5(tùy thuộc vào việc đóng cửa).

Bên trong phạm vi của hàm ẩn danh, xkhông bị ràng buộc với bất kỳ giá trị nào, do đó, nó cần được giải quyết trong startAtphạm vi ( s) trên.

Phạm vi từ điển

Như Wikipedia nói , phạm vi:

Là khu vực của một chương trình máy tính có ràng buộc hợp lệ: nơi tên có thể được sử dụng để chỉ thực thể .

Có hai kỹ thuật:

  • Phạm vi từ vựng (tĩnh): Định nghĩa của một biến được giải quyết bằng cách tìm kiếm khối hoặc hàm chứa của nó, sau đó nếu không tìm kiếm khối chứa bên ngoài, v.v.
  • Phạm vi động: Chức năng gọi được tìm kiếm, sau đó là chức năng gọi chức năng gọi đó, v.v., tiến lên ngăn xếp cuộc gọi.

Để giải thích thêm, hãy xem câu hỏi nàyxem Wikipedia .

Trong ví dụ trên, chúng ta có thể thấy rằng JavaScript có phạm vi từ vựng, bởi vì khi xđược giải quyết, ràng buộc được tìm kiếm trong startAtphạm vi (trên ), dựa trên mã nguồn (hàm ẩn danh tìm x được xác định bên trong startAt) và không dựa trên ngăn xếp cuộc gọi, cách (phạm vi) hàm được gọi.

Gói (đóng cửa) lên

Trong ví dụ của chúng tôi, khi chúng tôi gọi startAt, nó sẽ trả về một hàm (hạng nhất) sẽ được gán cho closure1closure2do đó một bao đóng được tạo, bởi vì các biến được truyền 15sẽ được lưu trong startAtphạm vi của nó, sẽ được kèm theo trả về chức năng ẩn danh. Khi chúng ta gọi hàm ẩn danh này thông qua closure1closure2với cùng một đối số ( 3), giá trị của ysẽ được tìm thấy ngay lập tức (vì đó là tham số của hàm đó), nhưng xkhông bị ràng buộc trong phạm vi của hàm ẩn danh, vì vậy độ phân giải tiếp tục trong phạm vi chức năng trên (từ vựng) (đã được lưu trong bao đóng) trong đóx được tìm thấy bị ràng buộc với một trong hai 1hoặc5. Bây giờ chúng tôi biết tất cả mọi thứ cho tổng kết để kết quả có thể được trả lại, sau đó được in.

Bây giờ bạn nên hiểu các bao đóng và cách chúng hoạt động, đó là một phần cơ bản của JavaScript.

Cà ri

Ồ, và bạn cũng đã học được về currying là gì: bạn sử dụng các hàm (bao đóng) để truyền từng đối số của một thao tác thay vì sử dụng một hàm với nhiều tham số.


5

Đóng là một tính năng trong JavaScript trong đó một hàm có quyền truy cập vào các biến phạm vi của chính nó, truy cập vào các biến chức năng bên ngoài và truy cập vào các biến toàn cục.

Đóng có quyền truy cập vào phạm vi chức năng bên ngoài của nó ngay cả sau khi chức năng bên ngoài đã trở lại. Điều này có nghĩa là một bao đóng có thể nhớ và truy cập các biến và đối số của hàm ngoài của nó ngay cả khi hàm đã kết thúc.

Hàm bên trong có thể truy cập các biến được xác định trong phạm vi của chính nó, phạm vi của hàm ngoài và phạm vi toàn cục. Và hàm ngoài có thể truy cập vào biến được định nghĩa trong phạm vi của chính nó và phạm vi toàn cục.

Ví dụ về đóng cửa :

var globalValue = 5;

function functOuter() {
  var outerFunctionValue = 10;

  //Inner function has access to the outer function value
  //and the global variables
  function functInner() {
    var innerFunctionValue = 5;
    alert(globalValue + outerFunctionValue + innerFunctionValue);
  }
  functInner();
}
functOuter();  

Đầu ra sẽ là 20 tổng của biến bên trong của chính hàm của nó, biến hàm ngoài và giá trị biến toàn cục.


4

Trong một tình huống bình thường, các biến bị ràng buộc bởi quy tắc phạm vi: Các biến cục bộ chỉ hoạt động trong hàm được xác định. Đóng cửa là một cách để phá vỡ quy tắc này tạm thời cho thuận tiện.

def n_times(a_thing)
  return lambda{|n| a_thing * n}
end

trong đoạn mã trên, lambda(|n| a_thing * n}là bao đóng vì a_thingđược gọi bởi lambda (một trình tạo hàm ẩn danh).

Bây giờ, nếu bạn đặt hàm ẩn danh kết quả trong một biến chức năng.

foo = n_times(4)

foo sẽ phá vỡ quy tắc phạm vi bình thường và bắt đầu sử dụng 4 nội bộ.

foo.call(3)

trả về 12.


2

Nói tóm lại, con trỏ hàm chỉ là một con trỏ tới một vị trí trong cơ sở mã chương trình (như bộ đếm chương trình). Trong khi đó Đóng = Hàm con trỏ + Khung ngăn xếp .

.


1

• Đóng là một chương trình con và môi trường tham chiếu nơi nó được xác định

- Môi trường tham chiếu là cần thiết nếu chương trình con có thể được gọi từ bất kỳ vị trí tùy ý nào trong chương trình

- Một ngôn ngữ có phạm vi tĩnh không cho phép các chương trình con lồng nhau không cần phải đóng

- Đóng chỉ cần thiết nếu một chương trình con có thể truy cập các biến trong phạm vi lồng nhau và nó có thể được gọi từ bất cứ đâu

- Để hỗ trợ các bao đóng, việc triển khai có thể cần cung cấp phạm vi không giới hạn cho một số biến (vì chương trình con có thể truy cập vào một biến không nhắm mục tiêu thường không còn tồn tại)

Thí dụ

function makeAdder(x) {
return function(y) {return x + y;}
}
var add10 = makeAdder(10);
var add5 = makeAdder(5);
document.write(″add 10 to 20: ″ + add10(20) +
″<br />″);
document.write(″add 5 to 20: ″ + add5(20) +
″<br />″);

0

Dưới đây là một ví dụ thực tế khác và sử dụng ngôn ngữ kịch bản phổ biến trong các trò chơi - Lua. Tôi cần thay đổi một chút cách thức hoạt động của chức năng thư viện để tránh sự cố với stdin không khả dụng.

local old_dofile = dofile

function dofile( filename )
  if filename == nil then
    error( 'Can not use default of stdin.' )
  end

  old_dofile( filename )
end

Giá trị của old_dofile biến mất khi khối mã này kết thúc phạm vi của nó (vì nó là cục bộ), tuy nhiên giá trị đã được đặt trong một bao đóng, do đó, hàm dofile được xác định lại mới CÓ THỂ truy cập vào nó, hoặc đúng hơn là một bản sao được lưu trữ cùng với chức năng như một "Giá trị gia tăng".


0

Từ Lua.org :

Khi một chức năng được viết kèm theo trong một chức năng khác, nó có toàn quyền truy cập vào các biến cục bộ từ chức năng kèm theo; tính năng này được gọi là phạm vi từ vựng. Mặc dù điều đó nghe có vẻ rõ ràng, nhưng nó không phải là. Phạm vi từ vựng, cộng với các chức năng hạng nhất, là một khái niệm mạnh mẽ trong ngôn ngữ lập trình, nhưng rất ít ngôn ngữ hỗ trợ khái niệm đó.


0

Nếu bạn đến từ thế giới Java, bạn có thể so sánh một bao đóng với hàm thành viên của một lớp. Nhìn vào ví dụ này

var f=function(){
  var a=7;
  var g=function(){
    return a;
  }
  return g;
}

Hàm gnày là một bao đóng: gđóng avào. Vì vậy, gcó thể so sánh với một hàm thành viên, acó thể được so sánh với một trường lớp và hàm fvới một lớp.


0

Đóng cửa Bất cứ khi nào chúng ta có một chức năng được xác định bên trong một chức năng khác, chức năng bên trong có quyền truy cập vào các biến được khai báo trong chức năng bên ngoài. Đóng cửa được giải thích tốt nhất với các ví dụ. Trong Liệt kê 2-18, bạn có thể thấy rằng hàm bên trong có quyền truy cập vào một biến (biếnInOuterFunction) từ phạm vi bên ngoài. Các biến trong hàm ngoài đã được đóng bởi (hoặc ràng buộc) hàm bên trong. Do đó, thời hạn đóng cửa. Bản thân khái niệm này đủ đơn giản và khá trực quan.

Listing 2-18:
    function outerFunction(arg) {
     var variableInOuterFunction = arg;

     function bar() {
             console.log(variableInOuterFunction); // Access a variable from the outer scope
     }
     // Call the local function to demonstrate that it has access to arg
     bar(); 
    }
    outerFunction('hello closure!'); // logs hello closure!

nguồn: http://index-of.es/Vario/Basarat%20Ali%20Syed%20(auth.)-Beginning%20Node.js-Apress%20(2014).pdf


0

Xin vui lòng xem mã dưới đây để hiểu đóng cửa sâu hơn:

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

Ở đây những gì sẽ được đầu ra? 0,1,2,3,4không phải là 5,5,5,5,5vì đóng cửa

Vậy nó sẽ giải quyết như thế nào? Câu trả lời dưới đây:

       for(var i=0; i< 5; i++){
           (function(j){     //using IIFE           
                setTimeout(function(){
                               console.log(j);
                           },1000);
            })(i);          
        }

Hãy để tôi giải thích đơn giản, khi một hàm được tạo ra không có gì xảy ra cho đến khi nó được gọi như vậy cho vòng lặp trong mã 1 được gọi 5 lần nhưng không được gọi ngay lập tức vì vậy khi nó được gọi là sau 1 giây và điều này không đồng bộ nên trước khi vòng lặp này kết thúc và lưu trữ giá trị 5 trong var i và cuối cùng thực hiện setTimeoutchức năng năm lần và in5,5,5,5,5

Dưới đây là cách giải quyết bằng cách sử dụng IIFE tức là gọi biểu thức hàm ngay lập tức

       (function(j){  //i is passed here           
            setTimeout(function(){
                           console.log(j);
                       },1000);
        })(i);  //look here it called immediate that is store i=0 for 1st loop, i=1 for 2nd loop, and so on and print 0,1,2,3,4

Để biết thêm, xin vui lòng hiểu bối cảnh thực hiện để hiểu đóng cửa.

  • Có một giải pháp nữa để giải quyết vấn đề này bằng cách sử dụng let (tính năng ES6) nhưng dưới chức năng trên, chức năng trên đã hoạt động

     for(let i=0; i< 5; i++){           
         setTimeout(function(){
                        console.log(i);
                    },1000);                        
     }
    
    Output: 0,1,2,3,4
    

=> Giải thích thêm:

Trong bộ nhớ, khi vòng lặp thực hiện hình ảnh thực hiện như dưới đây:

Vòng 1)

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

Vòng 2)

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

Vòng 3)

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

Vòng 4)

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

Vòng 5)

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

Ở đây tôi không được thực thi và sau khi hoàn thành vòng lặp, var i đã lưu giá trị 5 trong bộ nhớ nhưng phạm vi của nó luôn hiển thị trong hàm con của nó, vì vậy khi hàm thực hiện bên trong setTimeoutnăm lần nó sẽ in5,5,5,5,5

vì vậy để giải quyết điều này sử dụng IIFE như giải thích ở trên.


cảm ơn câu trả lời của bạn. nó sẽ dễ đọc hơn nếu bạn tách mã khỏi lời giải thích. (không thụt dòng không phải mã)
eMBee

0

Currying: Nó cho phép bạn đánh giá một phần hàm bằng cách chỉ truyền vào một tập hợp con các đối số của nó. Xem xét điều này:

function multiply (x, y) {
  return x * y;
}

const double = multiply.bind(null, 2);

const eight = double(4);

eight == 8;

Đóng cửa: Việc đóng cửa không gì khác hơn là truy cập vào một biến nằm ngoài phạm vi của hàm. Điều quan trọng cần nhớ là một hàm bên trong một hàm hoặc một hàm lồng nhau không phải là một bao đóng. Đóng luôn được sử dụng khi cần truy cập vào các biến ngoài phạm vi chức năng.

function apple(x){
   function google(y,z) {
    console.log(x*y);
   }
   google(7,2);
}

apple(3);

// the answer here will be 21

0

Đóng cửa rất dễ dàng. Chúng ta có thể xem xét nó như sau: Đóng = hàm + môi trường từ vựng của nó

Hãy xem xét các chức năng sau:

function init() {
    var name = “Mozilla”;
}

Điều gì sẽ được đóng cửa trong trường hợp trên? Hàm init () và các biến trong môi trường từ vựng của nó tức là tên. Đóng = init () + tên

Hãy xem xét một chức năng khác:

function init() {
    var name = “Mozilla”;
    function displayName(){
        alert(name);
}
displayName();
}

Điều gì sẽ đóng cửa ở đây? Hàm bên trong có thể truy cập các biến của hàm ngoài. displayName () có thể truy cập tên biến được khai báo trong hàm cha, init (). Tuy nhiên, các biến cục bộ tương tự trong displayName () sẽ được sử dụng nếu chúng tồn tại.

Đóng 1: hàm init + (tên biến + hàm displayName ()) -> phạm vi từ vựng

Đóng 2: hàm displayName + (biến tên) -> phạm vi từ vựng


0

Đóng cửa cung cấp JavaScript với trạng thái.

Nhà nước trong lập trình chỉ đơn giản là ghi nhớ mọi thứ.

Thí dụ

var a = 0;

a = a + 1; // => 1
a = a + 1; // => 2
a = a + 1; // => 3

Trong trường hợp trên, trạng thái được lưu trữ trong biến "a". Chúng tôi làm theo bằng cách thêm 1 đến "a" nhiều lần. Chúng tôi chỉ có thể làm điều đó bởi vì chúng tôi có thể "nhớ" giá trị. Chủ sở hữu trạng thái, "a", giữ giá trị đó trong bộ nhớ.

Thông thường, trong các ngôn ngữ lập trình, bạn muốn theo dõi mọi thứ, ghi nhớ thông tin và truy cập nó sau.

Điều này, trong các ngôn ngữ khác , thường được thực hiện thông qua việc sử dụng các lớp. Một lớp, giống như các biến, theo dõi trạng thái của nó. Và các thể hiện của lớp đó, lần lượt, cũng có trạng thái bên trong chúng. Bang chỉ đơn giản là thông tin mà bạn có thể lưu trữ và truy xuất sau này.

Thí dụ

class Bread {
  constructor (weight) {
    this.weight = weight;
  }

  render () {
    return `My weight is ${this.weight}!`;
  }
}

Làm thế nào chúng ta có thể truy cập "trọng lượng" từ bên trong phương thức "kết xuất"? Vâng, cảm ơn nhà nước. Mỗi phiên bản của lớp Bánh mì có thể hiển thị trọng lượng riêng của nó bằng cách đọc nó từ "trạng thái", một vị trí trong bộ nhớ nơi chúng ta có thể lưu trữ thông tin đó.

Bây giờ, JavaScript là một ngôn ngữ rất độc đáo mà trước đây không có các lớp (hiện tại nó có, nhưng dưới vỏ bọc chỉ có các hàm và biến) nên Closures cung cấp một cách để JavaScript ghi nhớ mọi thứ và truy cập chúng sau này.

Thí dụ

var n = 0;
var count = function () {
  n = n + 1;
  return n;
};

count(); // # 1
count(); // # 2
count(); // # 3

Ví dụ trên đã đạt được mục tiêu "giữ trạng thái" với một biến. Điều đó thật tuyệt! Tuy nhiên, điều này có nhược điểm là biến (chủ sở hữu "trạng thái") hiện bị lộ. Chúng ta có thể làm tốt hơn. Chúng ta có thể sử dụng Closures.

Thí dụ

var countGenerator = function () {
  var n = 0;
  var count = function () {
    n = n + 1;
    return n;
  };

  return count;
};

var count = countGenerator();
count(); // # 1
count(); // # 2
count(); // # 3

Cái này thật tuyệt.

Bây giờ chức năng "đếm" của chúng tôi có thể đếm. Nó chỉ có thể làm như vậy bởi vì nó có thể "giữ" trạng thái. Trạng thái trong trường hợp này là biến "n". Biến này hiện đang đóng. Đóng cửa trong thời gian và không gian. Trong thời gian bởi vì bạn sẽ không bao giờ có thể khôi phục nó, thay đổi nó, gán cho nó một giá trị hoặc tương tác trực tiếp với nó. Trong không gian vì nó được lồng vào nhau về mặt địa lý trong chức năng "CountGenerator".

Tại sao điều này là tuyệt vời? Bởi vì không liên quan đến bất kỳ công cụ phức tạp và phức tạp nào khác (ví dụ: các lớp, phương thức, thể hiện, v.v.), chúng tôi có thể 1. che giấu 2. kiểm soát từ xa

Chúng tôi che giấu trạng thái, biến "n", biến nó thành biến riêng! Chúng tôi cũng đã tạo một API có thể kiểm soát biến này theo cách được xác định trước. Cụ thể, chúng ta có thể gọi API như vậy là "Count ()" và thêm 1 đến "n" từ "khoảng cách". Không có cách nào, hình dạng hoặc hình thức bất kỳ ai cũng có thể truy cập "n" ngoại trừ thông qua API.

JavaScript thực sự tuyệt vời trong sự đơn giản của nó.

Đóng cửa là một phần lớn của lý do tại sao điều này là.


0

Một ví dụ đơn giản trong Groovy để bạn tham khảo:

def outer() {
    def x = 1
    return { -> println(x)} // inner
}
def innerObj = outer()
innerObj() // prints 1
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.