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?
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?
Câu trả lời:
(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à một hàm có thể được lưu trữ dưới dạng một 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 mà nó được tạo.
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();
Các chức năng 1 được gán cho document.onclick
và displayValOfBlack
là đóng cửa. 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
là cục bộ của phạm vi nơi hàm được xác định , con trỏ tới biến này được giữ nguyên.
Nếu bạn đặt nó trong một trang HTML:
Điều này chứng tỏ rằng cả hai đều có quyền truy cập như nhau black
và có thể được sử dụng để lưu trữ trạng thái mà không có bất kỳ đối tượng trình bao bọc nào.
Cuộc gọi đến setKeyPress
là để chứng minh làm thế nào một chức năng có thể được thông qua giống như bất kỳ biến nào. Các phạm vi bảo quản trong việc đóng cửa vẫn là một trong những nơi hàm được xác định.
Các bao đóng thường được sử dụng như các 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.
black
được khai báo bên trong một hàm, liệu nó có bị phá hủy khi ngăn xếp thư giãn không ...?
black
được khai báo bên trong một hàm, nên nó sẽ không bị phá hủy". Cũng nhớ rằng nếu bạn khai báo một đối tượng trong một hàm và sau đó gán nó cho một biến sống ở một nơi khác, thì đối tượng đó được giữ nguyên vì có các tham chiếu khác đến nó.
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 ràng buộc 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 phát hiện điều đó 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.
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í được đó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ụ về định nghĩa hàm Scala:
def addConstant(v: Int): Int = v + k
Trong thân hàm có hai tên (biến) v
và k
chỉ ra hai giá trị nguyên. Tên v
bị ràng buộc bởi vì nó được khai báo như là một đố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
này là hàm wrt miễn phí addConstant
vì hàm này không chứa manh mối nào về giá trị nào k
được ràng buộc 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 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 một bối cảnh k
được xác định, addConstant
đã trở thành một bao đóng vì tất cả các biến tự do của nó hiện đang bị đóng (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 hàm. Lưu ý biến tự do k
bị ràng buộc với một giá trị khi đóng được xác định , trong khi đó biến đối số v
bị ràng buộc khi đóng đượ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ể ẩ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à một 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 ẩn danh không có biến miễn phí.
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 alertValue
gian tên của hàm được trả về sẽ được kết nối với không gian tên trong đó closure
biến nằm trong đó . Khi bạn xóa toàn bộ hàm, giá trị của closure
biến sẽ bị xóa, nhưng cho đến lúc đó, alertValue
hàm 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 closure
biến và viết lại hàm cho:
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 closure_example
hàm, nó sẽ ghi giá trị tăng của closure
biến vì alert(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.
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 trả về 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 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. "
static
biến cục bộ có thể được coi là đóng không? Do đóng cửa trong Haskell liên quan đến nhà nước?
static
biến cục bộ, bạn có chính xác một).
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à còn tham chiếu trạng tháifoo
còn 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 rõ hơn trong phạm vi của nó; nó giống như môi trường nhỏ của chính 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?).
Đâ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 myclosure
hàm / trạng thái (thay vì thông qua hàm ẩn danh mà nó trả về), như pvalue
sẽ được đặt lại về 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 nó pvalue
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 về 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?
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");
}
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 trong đó 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 ra 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
ở đâ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:
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 định nghĩa, chúng ta sẽ có một chuỗi phạm vi cho nó chỉ chứa đối tượng toàn cục.
Bây giờ khi f1
được gọi, chuỗi phạm vi của f1
đố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 một chuỗi phạm vi mới cho f2
. Chuỗi phạm vi được lưu cho chức năng 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à của f1
. Do đó f2
's chuỗi phạm vi là f1
của hiện tại chuỗi phạm vi - trong đó có các đối tượng hoạt hóa f1
và đối tượng toàn cầu.
Khi f2
được gọi, nó sẽ chứa đối tượng kích hoạt của riêng nó y
, được thêm vào chuỗi phạm vi của nó đã chứa đối tượng kích hoạt f1
và đối tượng toàn cục.
Nếu có một hàm lồng nhau khác được xác định bên 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à thực sự không có vấn đề gì về việc đóng cửa có liên quan. Việc đóng cửa trở nên thú vị khi chúng được gọi theo một chuỗi phạm vi khác với 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 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ó sẽ không được thu gom rác và đối tượng kích hoạt mà nó đề cập đến sẽ không phải là rác được thu thập.
Trong ví dụ trên của chúng tôi, chúng tôi không trở về f2
từ f1
đó, do đó, khi có lệnh gọi f1
trở lại, đố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 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, trả về f2
sẽ có một chuỗi phạm vi sẽ chứa đối tượng kích hoạt f1
và do đó nó sẽ không được thu gom rác. Tại thời điểm này, nếu chúng tôi gọi f2
, nó sẽ có thể truy cập vào f1
biến của x
chúng ngay cả khi chúng tôi không tham gia 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" , nghĩa là chúng lưu phạm vi hoạt động khi chúng được định nghĩa trái ngược với phạm vi hoạt động khi chúng được gọi.
Có một số kỹ thuật lập trình mạnh mẽ liên quan đến việc đóng cửa 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ả đ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.
Một bao đóng là một tối ưu hóa trình biên dịch (còn gọi là cú pháp đường?). Một số người đã gọi nó là Đối tượng của Người nghèo .
Xem câu trả lời của Eric Lippert : (trích đoạn dưới đây)
Trình biên dịch sẽ tạo mã như thế này:
private class Locals
{
public int count;
public void Anonymous()
{
this.count++;
}
}
public Action Counter()
{
Locals locals = new Locals();
locals.count = 0;
Action counter = new Action(locals.Anonymous);
return counter;
}
Có lý?
Ngoài ra, bạn yêu cầu so sánh. VB và JScript đều tạo ra các bao đóng theo cách khá giống nhau.