Kiến thức lập trình

Closures trong JavaScript

  • Tác giả NIIT - ICT HANOI

  • Ngày đăng 17/ 10/ 2020

  • Bình luận 0 Bình luận

Trong bài hướng dẫn tự học JavaScript này bạn sẽ được tìm hiểu Closures trong JavaScript là gì và nó hoạt động như thế nào.

 

JavaScript Closures là gì?

 

Closures trong JavaScript

 

Ở bài học hàm trong JavaScript, bạn đã biết rằng trong JavaScript, phạm vi của biến có thể là toàn cục hoặc cục bộ.

 

Và kể từ phiên bản ES6, bạn cũng có thể tạo các biến phạm vi khối (block scope) bằng từ khóa let.

 

Một biến toàn cục có thể được truy cập và thao tác ở bất kỳ đâu trong chương trình JS.

 

Trong khi một biến cục bộ chỉ có thể được truy cập và thao tác bởi hàm (hoặc phạm vi) mà chúng được khai báo.

 

Tuy nhiên, có những trường hợp nhất định khi bạn muốn một biến có sẵn trong toàn bộ tập lệnh, nhưng bạn không muốn bất kỳ phần nào trong mã của mình có thể thay đổi giá trị của nó một cách tình cờ.

 

Hãy xem điều gì sẽ xảy ra nếu bạn cố gắng đạt được điều này bằng cách sử dụng biến toàn cục:

 

// Tạo một biến toàn cục
var counter  = 0;

// Một hàm rành riêng để thao tác với bộ đếm counter
function makeCounter() {
    return counter += 1;
}

// Gọi hàm
makeCounter();
console.log(counter); // Kết quả: 1

makeCounter();
console.log(counter); // Kết quả: 2

// Thử thay đổi biến counter từ bên ngoài
counter = 10;
console.log(counter); // Kết quả: 10

 

Như bạn thấy trong ví dụ trên, giá trị của biến counter có thể được thay đổi từ bất kỳ đâu trong chương trình mà không cần gọi hàm makeCounter() (như ta đã làm ở dòng số 17).

 

Bây giờ, hãy cố gắng đạt được điều tương tự với biến cục bộ và xem điều gì sẽ xảy ra:

 

function makeCounter() {
    // Tạo biến cục bộ
    var counter  = 0;
	
    // Thao tác với biến counter
    return counter += 1;
}

// Gọi hàm
console.log(makeCounter()); // Kết quả: 1
console.log(makeCounter()); // Kết quả: 1

 

Trong trường hợp này, biến counter không thể được thao tác từ bên ngoài, vì nó là cục bộ của hàm makeCounter().

 

Giá trị của nó cũng sẽ không tăng sau lần gọi hàm tiếp theo, vì mỗi khi chúng ta gọi hàm, nó sẽ đặt lại giá trị biến counter, mà bạn có thể thấy rõ trong ví dụ trên (dòng code số 11).

 

Vậy nếu chúng ta vẫn muốn đạt kết quả tương tự như ví dụ 1 mà lại không thay đổi biến counter từ bên ngoài thì làm thế nào?

 

Closure trong JavaScript ở đây để giải quyết vấn đề này.

 

Về cơ bản, Closure là một hàm bên trong (inner function) có quyền truy cập vào phạm vi của hàm mẹ, ngay cả sau khi hàm mẹ đã thực thi xong.

 

Điều này được thực hiện bằng cách tạo một hàm bên trong một hàm khác. Hãy xem ví dụ sau để xem nó hoạt động như thế nào:

 

function makeCounter() {
    var counter = 0;
	
    // Hàm bên trong hàm
    function make() {
        counter += 1;
        return counter;
    }
    return make;
}

/* Thực thi hàm makeCounter() và lưu trữ giá trị
được trả về bằng biến myCounter */
var myCounter = makeCounter();

console.log(myCounter()); // Kết quả: 1
console.log(myCounter()); // Kết quả: 2

 

Như bạn có thể thấy trong ví dụ trên, hàm bên trong make() được trả về từ hàm bên ngoài makeCounter().

 

Vì vậy, giá trị của myCounter bản chất là hàm make() bên trong (dòng code số 14).

 

Vì thế, việc gọi myCounter sẽ gọi hàm make().

 

Trong JavaScript, các hàm có thể được gán cho các biến, được truyền dưới dạng đối số cho các hàm khác, có thể được lồng vào bên trong các hàm khác và hơn thế nữa.

 

Bạn cũng sẽ nhận thấy rằng hàm bên trong make() vẫn có thể truy cập giá trị của biến counter được định nghĩa trong makeCounter().

 

Theo như hiểu biết cơ bản thì hàm đã thực thi xong thì các biến trong hàm sẽ bị xóa đi. Không thể truy cập nữa.

 

Thế nhưng, mặc dù hàm makeCounter() đã hoàn thành việc thực thi (ở dòng code số 14). Mà điều này vẫn xảy ra bởi bạn đang chương trình đang ở hình thức closure.

 

Closures lưu trữ nội bộ các tham chiếu đến các biến bên ngoài của chúng và có thể truy cập và cập nhật các giá trị của chúng.

 

Trong ví dụ trên, hàm make() là một closure có tham chiếu đến biến counter bên ngoài nó (vẫn ở trong makeCounter() nha).

 

Điều này ngụ ý rằng bất cứ khi nào hàm make() được gọi, mã bên trong nó có thể truy cập và cập nhật biến counter vì nó được lưu trữ trong closure.

 

Cuối cùng, vì hàm bên ngoài đã hoàn thành việc thực thi, không phần nào khác của mã có thể truy cập hoặc thao tác với biến bộ đếm. Chỉ có chức năng bên trong mới có quyền truy cập độc quyền vào nó.

 

Ví dụ trước cũng có thể được viết bằng cách sử dụng biểu thức hàm ẩn danh (anonymous function expression), như sau:

 

// Anonymous function expression
var myCounter = (function() {
    var counter = 0;
	
    // Nested anonymous function
    return function() {
        counter += 1;
        return counter;
    }
})();

console.log(myCounter()); // Kết quả: 1
console.log(myCounter()); // Kết quả: 2

 

Ở trên đây là chúng ta đã gán một hàm tự thực thi (Self-Invoking Fucntion) cho một biến.

 

Và trong hàm tự thực thi thì sử dụng nested function và trả về một hàm.

 

Mẹo: Trong JavaScript, tất cả các hàm đều có quyền truy cập vào phạm vi toàn cục, cũng như phạm vi phía trên chúng. Vì JavaScript hỗ trợ các hàm lồng nhau, điều này thường có nghĩa là các hàm lồng nhau có quyền truy cập vào bất kỳ giá trị nào được khai báo trong phạm vi cao hơn bao gồm phạm vi của hàm mẹ của nó.

 

Lưu ý: Các biến toàn cục tồn tại miễn là ứng dụng của bạn (tức là trang web của bạn) còn sống. Trong khi đó, các biến cục bộ có tuổi thọ ngắn, chúng được tạo ra khi hàm được gọi và bị hủy ngay sau khi hàm được thực thi xong.

 

Tạo các hàm Getter và Setter

 

Ở đây chúng ta sẽ tạo một biến secret và bảo vệ nó khỏi bị thao túng trực tiếp từ chương trình bên ngoài bằng cách sử dụng hàm closure.

 

Chúng ta cũng sẽ tạo hàm getterhàm setter để lấy và thiết lập giá trị của secret.

 

Ngoài ra, hàm setter cũng sẽ thực hiện kiểm tra nhanh xem giá trị được chỉ định có phải là một số hay không và nếu không, nó sẽ không thay đổi giá trị biến.

 

var getValue, setValue;

// Self-executing function
(function() {
    var secret = 0;
    
    // Hàm Getter
    getValue = function() {
        return secret;
    };
    
    // Hàm Setter
    setValue = function(x) {
        if(typeof x === "number") {
            secret = x;
        }
    };
}());

// Gọi hàm
getValue(); // Kết quả: 0
setValue(10);
getValue(); // Kết quả: 10
setValue(null);
getValue(); // Kết quả: 10

 

Lưu ý: Các hàm tự thực thi (Self-Executing Functions) còn được gọi là biểu thức hàm được gọi ngay lập tức (Immediately Invoked Function Expression ) (IIFE), hàm được thực thi ngay lập tức (immediately executed function ) hoặc hàm ẩn danh tự thực thi (self-executing anonymous function ).

 

Như vậy là qua bài viết này bạn đã hiểu thêm về một khái niệm nâng cao trong JavaScript đó chính là Closure.

 

Sau này khi lập trình web thực tế bạn sẽ gặp nó khá là thường xuyên. Vì thế, hãy làm lại ví dụ ở trên nhiều lần để thực sự nắm được Closure là gì bạn nhé.

 

> Và nếu bạn đang tích cực học tập để lập trình web thì tham khảo ngay KHÓA HỌC LẬP TRÌNH WEB (Full Stack) tại NIIT - ICT Hà Nội để được học một cách bài bản, đầy đủ bộ công nghệ hiện đại nhất.