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?
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?
Câu trả lờ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 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.
Đâ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 a
là 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 outer
thoá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 a
là 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 a
sẽ là rác được thu thập và vứt đi khi hàm outer
thoát. Gọi fnc sẽ có lỗi vì a
không còn tồn tại.
Trong JavaScript, biến a
vẫ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.
a
thuộc phạm vi của outer
. Phạm vi của inner
có một con trỏ cha mẹ đến phạm vi outer
. fnc
là một biến mà trỏ đến inner
. a
Vẫn tồn tại miễn là fnc
vẫn tồn tại. a
là trong đóng cửa.
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.
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.
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ó là 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 y
nghĩ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 y
trong 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 y
phả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ải là bao đó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ế.
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
Để 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.
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?
Đâ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 delay
nó và nó đóng cửa:
startPlayback.delay(5000, someTrack);
// Keep going, do other things
Khi bạn gọi delay
bằng 5000
ms, đ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 setTimeout
gọ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.
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
}
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.
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)
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, startAt
trả về một hàm ( ẩn danh ) mà hàm được gán cho closure1
và closure2
. 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).
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:
y
bị ràng buộc 3
.startAt
phạm vi, x
bị ràng buộc 1
hoặ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, x
khô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 startAt
phạm vi ( s) trê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:
Để giải thích thêm, hãy xem câu hỏi này và xem 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 startAt
phạ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.
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 closure1
và closure2
do đó một bao đóng được tạo, bởi vì các biến được truyền 1
và 5
sẽ được lưu trong startAt
phạ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 closure1
và closure2
với cùng một đối số ( 3
), giá trị của y
sẽ được tìm thấy ngay lập tức (vì đó là tham số của hàm đó), nhưng x
khô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 1
hoặ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.
Ồ, 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ố.
Đó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.
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.
• Đó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 />″);
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".
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 đó.
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 g
này là một bao đóng: g
đóng a
vào. Vì vậy, g
có thể so sánh với một hàm thành viên, a
có thể được so sánh với một trường lớp và hàm f
với một lớp.
Đó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
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,4
không phải là 5,5,5,5,5
vì đó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 setTimeout
chứ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 setTimeout
nă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.
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
Đó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
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
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.
Đóng cửa là một phần lớn của lý do tại sao điều này là.