it-swarm-vi.com

Đóng cửa là gì?

Thỉnh thoảng tôi thấy "đóng cửa" được đề cập và tôi đã cố gắng tìm kiếm nhưng Wiki không đưa ra lời giải thích mà tôi hiểu. ai đó có thể giúp tôi ra ở đây?

157
gablin

(Tuyên bố miễn trừ trách nhiệm: đây là một lời giải thích cơ bản; theo như định nghĩa, tôi đơn giản hóa một chút)

Cách đơn giản nhất để nghĩ về việc đóng là hàm có thể được lưu dưới dạng biến (được gọi là "hàm hạng nhất"), có khả năng đặc biệt để truy cập các biến khác cục bộ vào phạm vi nó được tạo ra.

Ví dụ (JavaScript):

var setKeyPress = function(callback) {
    document.onkeypress = callback;
};

var initialize = function() {
    var black = false;

    document.onclick = function() {
        black = !black;
        document.body.style.backgroundColor = black ? "#000000" : "transparent";
    }

    var displayValOfBlack = function() {
        alert(black);
    }

    setKeyPress(displayValOfBlack);
};

initialize();

Chức năng1 phân công document.onclickdisplayValOfBlack là các bao đóng. Bạn có thể thấy rằng cả hai đều tham chiếu biến boolean black, nhưng biến đó được gán bên ngoài hàm. Vì black cục bộ trong phạm vi nơi hàm được xác định , nên con trỏ tới biến này được giữ nguyên.

Nếu bạn đặt nó trong một trang HTML:

  1. Nhấn vào đây để đổi thành màu đen
  2. Nhấn [enter] để xem "true"
  3. Nhấp lại, thay đổi trở lại màu trắng
  4. Nhấn [enter] để xem "false"

Điều này chứng tỏ rằng cả hai đều có quyền truy cập vào giống nhau black và có thể được sử dụng để lưu trữ trạng thái không có bất kỳ đối tượng trình bao bọc nào.

Cuộc gọi đến setKeyPress là để giải thích cách một hàm có thể được truyền giống như bất kỳ biến nào. scope được bảo toàn trong bao đóng vẫn là một trong đó hàm được xác định.

Các bao đóng thường được sử dụng làm trình xử lý sự kiện, đặc biệt là trong JavaScript và ActionScript. Việc sử dụng tốt các bao đóng sẽ giúp bạn ngầm liên kết các biến với các trình xử lý sự kiện mà không phải tạo một trình bao bọc đối tượng. Tuy nhiên, việc sử dụng bất cẩn sẽ dẫn đến rò rỉ bộ nhớ (chẳng hạn như khi một trình xử lý sự kiện không được sử dụng nhưng được bảo quản là điều duy nhất để giữ các đối tượng lớn trong bộ nhớ, đặc biệt là các đối tượng DOM, ngăn chặn việc thu gom rác).


1: Trên thực tế, tất cả các chức năng trong JavaScript là đóng cửa.

141
Nicole

Một đóng cửa về cơ bản chỉ là một cách khác nhau để nhìn vào một đối tượng. Một đối tượng là dữ liệu có một hoặc nhiều chức năng ràng buộc với nó. Một bao đóng là một hàm có một hoặc nhiều biến liên kết với nó. Hai cái cơ bản là giống hệt nhau, ở mức độ thực hiện ít nhất. Sự khác biệt thực sự là nơi họ đến từ.

Trong lập trình hướng đối tượng, bạn khai báo một lớp đối tượng bằng cách xác định các biến thành viên của nó và các phương thức của nó (các hàm thành viên) ở phía trước, và sau đó bạn tạo các thể hiện của lớp đó. Mỗi phiên bản đi kèm với một bản sao của dữ liệu thành viên, được khởi tạo bởi hàm tạo. Sau đó, bạn có một biến của một loại đối tượng và chuyển nó xung quanh dưới dạng một phần dữ liệu, vì trọng tâm là bản chất của nó là dữ liệu.

Mặt khác, trong một bao đóng, đối tượng không được xác định từ trước như một lớp đối tượng hoặc được khởi tạo thông qua một lệnh gọi hàm tạo trong mã của bạn. Thay vào đó, bạn viết bao đóng như là một hàm bên trong của hàm khác. Việc đóng có thể tham chiếu đến bất kỳ biến cục bộ nào của hàm ngoài và trình biên dịch sẽ phát hiện ra và di chuyển các biến này từ không gian ngăn xếp của hàm ngoài sang khai báo đối tượng ẩn của bao đóng. Sau đó, bạn có một biến của kiểu đóng, và mặc dù về cơ bản nó là một đối tượng dưới mui xe, bạn chuyển nó xung quanh như một tham chiếu hàm, bởi vì trọng tâm là bản chất của nó như là một hàm.

69
Mason Wheeler

Thuật ngữ đóng xuất phát từ thực tế là một đoạn mã (khối, hàm) có thể có các biến miễn phí là đã đóng (tức là bị ràng buộc với một giá trị) bởi môi trường trong đó khối mã được xác định.

Lấy ví dụ định nghĩa hàm Scala:

def addConstant(v: Int): Int = v + k

Trong thân hàm có hai tên (biến) vk chỉ ra hai giá trị nguyên. Tên v bị ràng buộc bởi vì nó được khai báo là đối số của hàm addConstant (bằng cách nhìn vào khai báo hàm chúng ta biết rằng v sẽ được gán một giá trị khi hàm Được gọi). Tên k là hàm wrt miễn phí addConstant vì hàm này không chứa đầu mối nào về giá trị k được liên kết với (và làm thế nào).

Để đánh giá một cuộc gọi như:

val n = addConstant(10)

chúng ta phải gán k một giá trị, điều này chỉ có thể xảy ra nếu tên k được xác định trong ngữ cảnh trong đó addConstant được xác định. Ví dụ:

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  def addConstant(v: Int): Int = v + k

  values.map(addConstant)
}

Bây giờ chúng ta đã xác định addConstant trong ngữ cảnh trong đó k được xác định, addConstant đã trở thành một đóng bởi vì tất cả các biến miễn phí của nó hiện đang đã đóng (bị ràng buộc với một giá trị): addConstant có thể được gọi và chuyển xung quanh như thể nó là một chức năng. Lưu ý biến tự do k bị ràng buộc với một giá trị khi bao đóng được xác định , trong khi biến đối số v bị ràng buộc khi đóng là được gọi .

Vì vậy, một bao đóng về cơ bản là một hàm hoặc khối mã có thể truy cập các giá trị không cục bộ thông qua các biến miễn phí của nó sau khi các giá trị này bị ràng buộc bởi bối cảnh.

Trong nhiều ngôn ngữ, nếu bạn chỉ sử dụng bao đóng một lần, bạn có thể làm cho nó ẩn danh , ví dụ:.

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  values.map(v => v + k)
}

Lưu ý rằng một hàm không có biến miễn phí là trường hợp đặc biệt của bao đóng (với một tập hợp các biến miễn phí trống). Tương tự, một hàm ẩn danh là trường hợp đặc biệt của một bao đóng ẩn danh , tức là một hàm ẩn danh là một bao đóng ẩn danh không có biến miễn phí.

29
Giorgio

Một lời giải thích đơn giản trong JavaScript:

var closure_example = function() {
    var closure = 0;
    // after first iteration the value will not be erased from the memory
    // because it is bound with the returned alertValue function.
    return {
        alertValue : function() {
            closure++;
            alert(closure);
        }
    };
};
closure_example();

alert(closure) sẽ sử dụng giá trị được tạo trước đó của closure. Không gian tên của hàm alertValue được trả về sẽ được kết nối với không gian tên trong đó biến closure nằm trong đó. Khi bạn xóa toàn bộ hàm, giá trị của biến closure sẽ bị xóa, nhưng cho đến lúc đó, hàm alertValue sẽ luôn có thể đọc/ghi giá trị của biến closure.

Nếu bạn chạy mã này, lần lặp đầu tiên sẽ gán giá trị 0 cho biến closure và viết lại hàm thành:

var closure_example = function(){
    alertValue : function(){
        closure++;
        alert(closure);
    }       
}

Và bởi vì alertValue cần biến cục bộ closure để thực thi hàm, nó liên kết chính nó với giá trị của biến cục bộ được gán trước đó closure.

Và bây giờ mỗi khi bạn gọi hàm closure_example, Nó sẽ ghi ra giá trị tăng của biến closurealert(closure) bị ràng buộc.

closure_example.alertValue()//alerts value 1 
closure_example.alertValue()//alerts value 2 
closure_example.alertValue()//alerts value 3
//etc. 
9
Muha

Một "đóng cửa", về bản chất, một số trạng thái cục bộ và một số mã, được kết hợp thành một gói. Thông thường, trạng thái cục bộ xuất phát từ một phạm vi (từ vựng) xung quanh và mã (về cơ bản) là một hàm bên trong sau đó được đưa trở lại bên ngoài. Việc đóng sau đó là sự kết hợp của các biến được bắt mà hàm bên trong nhìn thấy và mã của hàm bên trong.

Thật không may, một trong những điều đó, thật không may, hơi khó để giải thích, do không quen thuộc.

Một từ tương tự tôi đã sử dụng thành công trong quá khứ là "hãy tưởng tượng chúng ta có một thứ mà chúng ta gọi là" cuốn sách ", trong phòng đóng cửa," cuốn sách "là bản sao ở đó, ở góc, của TAOCP, nhưng trên cái bàn đóng , đó là bản sao của một cuốn sách của Tập tin Dresden. Vì vậy, tùy thuộc vào việc bạn đóng cửa ở đâu, mã 'đưa cho tôi cuốn sách' dẫn đến những điều khác nhau xảy ra. "

5
Vatine

Thật khó để định nghĩa đóng cửa là gì mà không xác định khái niệm 'trạng thái'.

Về cơ bản, trong một ngôn ngữ có phạm vi từ vựng đầy đủ coi các chức năng là giá trị hạng nhất, điều gì đó đặc biệt xảy ra. Nếu tôi phải làm một cái gì đó như:

function foo(x)
return x
end

x = foo

Biến x không chỉ tham chiếu function foo() mà nó còn tham chiếu trạng thái foo được để lại trong lần cuối cùng trả về. Phép thuật thực sự xảy ra khi foo có các chức năng khác được xác định thêm trong phạm vi của nó; nó giống như môi trường mini của riêng nó (giống như 'thông thường', chúng tôi xác định các chức năng trong môi trường toàn cầu).

Về mặt chức năng, nó có thể giải quyết nhiều vấn đề tương tự như từ khóa 'tĩnh' của C++ (C?), Giữ lại trạng thái của một biến cục bộ trong nhiều lệnh gọi hàm; tuy nhiên, nó giống như áp dụng cùng một nguyên tắc (biến tĩnh) cho một hàm, vì các hàm là các giá trị hạng nhất; bao đóng thêm hỗ trợ cho toàn bộ trạng thái của chức năng được lưu (không liên quan gì đến các hàm tĩnh của C++).

Việc coi các hàm là các giá trị của lớp đầu tiên và thêm hỗ trợ cho các bao đóng cũng có nghĩa là bạn có thể có nhiều hơn một thể hiện của cùng một hàm trong bộ nhớ (tương tự như các lớp). Điều này có nghĩa là bạn có thể sử dụng lại cùng một mã mà không phải đặt lại trạng thái của hàm, như được yêu cầu khi xử lý các biến tĩnh C++ bên trong một hàm (có thể sai về điều này?).

Dưới đây là một số thử nghiệm về hỗ trợ đóng cửa của Lua.

--Closure testing
--By Trae Barlow
--

function myclosure()
    print(pvalue)--nil
    local pvalue = pvalue or 10
    return function()
        pvalue = pvalue + 10 --20, 31, 42, 53(53 never printed)
        print(pvalue)
        pvalue = pvalue + 1 --21, 32, 43(pvalue state saved through multiple calls)
        return pvalue
    end
end

x = myclosure() --x now references anonymous function inside myclosure()

x()--nil, 20
x() --21, 31
x() --32, 42
    --43, 53 -- if we iterated x() again

các kết quả:

nil
20
31
42

Nó có thể trở nên khó khăn và có thể thay đổi từ ngôn ngữ này sang ngôn ngữ khác, nhưng dường như trong Lua rằng bất cứ khi nào một chức năng được thực thi, trạng thái của nó được đặt lại. Tôi nói điều này bởi vì các kết quả từ mã ở trên sẽ khác nếu chúng ta truy cập trực tiếp vào hàm myclosure (thay vì thông qua hàm ẩn danh mà nó trả về), vì pvalue sẽ được đặt lại đến 10; nhưng nếu chúng ta truy cập trạng thái của myclenses thông qua x (hàm ẩn danh), bạn có thể thấy rằng pvalue vẫn còn sống và ở đâu đó trong bộ nhớ. Tôi nghi ngờ có một chút nữa cho nó, có lẽ ai đó có thể giải thích rõ hơn bản chất của việc thực hiện.

Tái bút: Tôi không biết một chút về C++ 11 (khác với những gì trong các phiên bản trước) vì vậy hãy lưu ý rằng đây không phải là sự so sánh giữa các lần đóng trong C++ 11 và Lua. Ngoài ra, tất cả các 'đường được vẽ' từ Lua đến C++ đều giống nhau vì các biến tĩnh và các bao đóng không giống nhau 100%; ngay cả khi đôi khi chúng được sử dụng để giải quyết các vấn đề tương tự.

Điều tôi không chắc chắn là, trong ví dụ mã ở trên, liệu hàm ẩn danh hay hàm bậc cao hơn có được coi là đóng không?

5
Trae Barlow

Một bao đóng là một chức năng có trạng thái liên quan:

Trong Perl, bạn tạo các bao đóng như thế này:

#!/usr/bin/Perl

# This function creates a closure.
sub getHelloPrint
{
    # Bind state for the function we are returning.
    my ($first) = @_;a

    # The function returned will have access to the variable $first
    return sub { my ($second) = @_; print  "$first $second\n"; };
}

my $hw = getHelloPrint("Hello");
my $gw = getHelloPrint("Goodby");

&$hw("World"); // Print Hello World
&$gw("World"); // PRint Goodby World

Nếu chúng ta nhìn vào chức năng mới được cung cấp với C++.
[.__.] Nó cũng cho phép bạn liên kết trạng thái hiện tại với đối tượng:

#include <string>
#include <iostream>
#include <functional>


std::function<void(std::string const&)> getLambda(std::string const& first)
{
    // Here we bind `first` to the function
    // The second parameter will be passed when we call the function
    return [first](std::string const& second) -> void
    {   std::cout << first << " " << second << "\n";
    };
}

int main(int argc, char* argv[])
{
    auto hw = getLambda("Hello");
    auto gw = getLambda("GoodBye");

    hw("World");
    gw("World");
}
4
Martin York

Hãy xem xét một chức năng đơn giản:

function f1(x) {
    // ... something
}

Hàm này được gọi là hàm cấp cao nhất vì nó không được lồng trong bất kỳ hàm nào khác. Mỗi ​​hàm JavaScript liên kết với chính nó một danh sách các đối tượng được gọi là "Chuỗi phạm vi" . Chuỗi phạm vi này là một danh sách các đối tượng được sắp xếp. Mỗi đối tượng này định nghĩa một số biến.

Trong các hàm cấp cao nhất, chuỗi phạm vi bao gồm một đối tượng duy nhất là đối tượng toàn cục. Ví dụ: hàm f1 Ở trên có một chuỗi phạm vi có một đối tượng duy nhất xác định tất cả các biến toàn cục. (lưu ý rằng thuật ngữ "đối tượng" ở đây không có nghĩa là đối tượng JavaScript, nó chỉ là một đối tượng được xác định triển khai hoạt động như một thùng chứa biến, trong đó JavaScript có thể "tra cứu" các biến.)

Khi hàm này được gọi, JavaScript sẽ tạo một thứ gọi là "Đối tượng kích hoạt" và đặt nó ở đầu chuỗi phạm vi. Đối tượng này chứa tất cả các biến cục bộ (ví dụ x tại đây). Do đó bây giờ chúng ta có hai đối tượng trong chuỗi phạm vi: đầu tiên là đối tượng kích hoạt và bên dưới nó là đối tượng toàn cầu.

Lưu ý rất cẩn thận rằng hai đối tượng được đưa vào chuỗi phạm vi tại thời điểm KHÁC BIỆT. Đối tượng toàn cục được đặt khi hàm được xác định (nghĩa là khi JavaScript phân tích hàm và tạo đối tượng hàm) và đối tượng kích hoạt sẽ nhập khi hàm được gọi.

Vì vậy, bây giờ chúng ta biết điều này:

  • Mỗi hàm có một chuỗi phạm vi liên quan đến nó
  • Khi chức năng được xác định (khi đối tượng chức năng được tạo), JavaScript sẽ lưu chuỗi phạm vi với chức năng đó
  • Đối với các hàm cấp cao nhất, chuỗi phạm vi chỉ chứa đối tượng toàn cục tại thời điểm xác định hàm và thêm một đối tượng kích hoạt bổ sung trên đầu tại thời điểm gọi

Tình huống trở nên thú vị khi chúng ta xử lý các hàm lồng nhau. Vì vậy, hãy tạo một:

function f1(x) {

    function f2(y) {
        // ... something
    }

}

Khi f1 Được xác định, chúng tôi sẽ nhận được một chuỗi phạm vi cho nó chỉ chứa đối tượng toàn cầu.

Bây giờ khi f1 Được gọi, chuỗi phạm vi của f1 Sẽ nhận được đối tượng kích hoạt. Đối tượng kích hoạt này chứa biến x và biến f2 Là một hàm. Và, lưu ý rằng f2 Đang được xác định. Do đó, tại thời điểm này, JavaScript cũng lưu chuỗi phạm vi mới cho f2. Chuỗi phạm vi được lưu cho hàm bên trong này là chuỗi phạm vi hiện tại có hiệu lực. Chuỗi phạm vi hiện tại có hiệu lực là đó là f1. Do đó, chuỗi phạm vi của f2f1 ' chuỗi phạm vi hiện tại - chứa đối tượng kích hoạt của f1 Và đối tượng toàn cầu.

Khi f2 Được gọi, nó sẽ có đối tượng kích hoạt riêng chứa y, được thêm vào chuỗi phạm vi đã chứa đối tượng kích hoạt của f1 Và đối tượng toàn cầu.

Nếu có một hàm lồng nhau khác được xác định trong f2, Chuỗi phạm vi của nó sẽ chứa ba đối tượng tại thời điểm xác định (2 đối tượng kích hoạt của hai hàm ngoài và đối tượng toàn cục) và 4 tại thời điểm gọi.

Vì vậy, bây giờ chúng tôi hiểu cách chuỗi phạm vi hoạt động nhưng chúng tôi chưa nói về việc đóng cửa.

Sự kết hợp giữa một đối tượng hàm và một phạm vi (một tập hợp các ràng buộc biến) trong đó các biến của hàm được giải quyết được gọi là một bao đóng trong tài liệu khoa học máy tính - JavaScript hướng dẫn dứt khoát của David Flanagan

Hầu hết các hàm được gọi bằng cách sử dụng cùng một chuỗi phạm vi có hiệu lực khi hàm được xác định, và nó thực sự không quan trọng rằng có một bao đóng có liên quan. Các bao đóng trở nên thú vị khi chúng được gọi theo một chuỗi phạm vi khác với một chuỗi có hiệu lực khi chúng được xác định. Điều này xảy ra phổ biến nhất khi một đối tượng hàm lồng nhau được được trả về từ hàm được xác định.

Khi hàm trả về, đối tượng kích hoạt đó sẽ bị xóa khỏi chuỗi phạm vi. Nếu không có các hàm lồng nhau, sẽ không có thêm các tham chiếu đến đối tượng kích hoạt và nó sẽ được thu gom rác. Nếu có các hàm lồng nhau được xác định, thì mỗi hàm đó có tham chiếu đến chuỗi phạm vi và chuỗi phạm vi đó đề cập đến đối tượng kích hoạt.

Tuy nhiên, nếu các đối tượng hàm lồng nhau đó vẫn nằm trong hàm ngoài của chúng, thì chính chúng sẽ là rác được thu thập, cùng với đối tượng kích hoạt mà chúng đã đề cập. Nhưng nếu hàm định nghĩa một hàm lồng nhau và trả về nó hoặc lưu trữ nó vào một thuộc tính ở đâu đó, thì sẽ có một tham chiếu bên ngoài đến hàm lồng nhau. Nó đã giành được thùng rác được thu gom và đối tượng kích hoạt mà nó đề cập đến won là rác được thu thập.

Trong ví dụ trên, chúng tôi không trả lại f2 Từ f1, Do đó, khi một cuộc gọi đến f1 Trả về, đối tượng kích hoạt của nó sẽ bị xóa khỏi chuỗi phạm vi và rác của nó thu thập. Nhưng nếu chúng ta có một cái gì đó như thế này:

function f1(x) {

    function f2(y) {
        // ... something
    }

    return f2;
}

Ở đây, f2 Trả về sẽ có một chuỗi phạm vi sẽ chứa đối tượng kích hoạt của f1, Và do đó nó sẽ không được thu gom rác. Tại thời điểm này, nếu chúng ta gọi f2, Nó sẽ có thể truy cập biến f1 Của x mặc dù chúng tôi đã hết f1.

Do đó, chúng ta có thể thấy rằng một hàm giữ chuỗi phạm vi của nó với nó và với chuỗi phạm vi có tất cả các đối tượng kích hoạt của các hàm bên ngoài. Đây là bản chất của đóng cửa. Chúng tôi nói rằng các hàm trong JavaScript là "phạm vi từ vựng" , có nghĩa là chúng lưu phạm vi đã hoạt động khi chúng được xác định trái ngược với phạm vi đã được xác định hoạt động khi họ được gọi.

Có một số kỹ thuật lập trình mạnh mẽ liên quan đến các bao đóng như xấp xỉ các biến riêng tư, lập trình hướng sự kiện, ứng dụng một phần , v.v.

Cũng lưu ý rằng tất cả những điều này áp dụng cho tất cả các ngôn ngữ hỗ trợ đóng cửa. Ví dụ PHP (5.3+), Python, Ruby, v.v.

2
treecoder