Closure trong JavaScript là gì?

Closure trong JavaScript là một tính năng mạnh mẽ cho phép một hàm truy cập các biến trong phạm vi của hàm bên ngoài nó, ngay cả sau khi hàm bên ngoài đã thực thi xong. Nói cách khác, closure xảy ra khi một hàm được “ghi nhớ” phạm vi mà nó đã được tạo ra, ngay cả khi phạm vi đó không còn tồn tại.

Đặc điểm chính của closure:

  • Một hàm có thể truy cập các biến trong phạm vi của hàm bao quanh (outer function), ngay cả sau khi hàm bao quanh đã hoàn thành việc thực thi.
  • Closure lưu trữ tham chiếu tới các biến trong phạm vi mà nó được tạo ra, không phải là giá trị của các biến đó.

Ví dụ đơn giản về closure

Dưới đây là ví dụ về một closure cơ bản:

function outerFunction() {
    let outerVariable = "Hello";

    function innerFunction() {
        console.log(outerVariable); // Truy cập được biến của outerFunction
    }

    return innerFunction;
}

const closure = outerFunction(); // outerFunction đã hoàn thành
closure(); // "Hello"

Trong ví dụ trên:

  • innerFunction được định nghĩa bên trong outerFunction.
  • Khi outerFunction thực thi xong, nó trả về innerFunction.
  • Mặc dù outerFunction đã hoàn thành và thoát khỏi phạm vi, innerFunction vẫn có thể truy cập biến outerVariable của nó. Đây chính là closure.

Một ví dụ khác với việc lưu trữ trạng thái qua closure

Closure có thể hữu ích trong việc lưu trữ và duy trì trạng thái của các biến.

function counter() {
    let count = 0; // Biến count chỉ tồn tại trong phạm vi của hàm counter

    return function() {
        count++; // Mỗi lần gọi, biến count sẽ được tăng lên
        console.log(count);
    };
}

const increment = counter();

increment(); // 1
increment(); // 2
increment(); // 3

Trong ví dụ này:

  • counter trả về một hàm ẩn danh.
  • Hàm ẩn danh này có quyền truy cập vào biến count của counter thông qua closure.
  • Mỗi lần gọi increment(), biến count được tăng lên, và giá trị mới được in ra.

Closure và vòng lặp

Một vấn đề phổ biến khi làm việc với closure trong các vòng lặp là biến trong closure có thể bị “chia sẻ” thay vì được “đóng gói” riêng lẻ cho mỗi lần lặp. Hãy xem ví dụ sau:

for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i); // Tất cả các hàm setTimeout sẽ in ra 4, không phải 1, 2, 3
    }, 1000);
}

Kết quả in ra sẽ là 4 cho cả ba lần, vì biến i được chia sẻ giữa các hàm callback của setTimeout. Để giải quyết vấn đề này, ta có thể sử dụng closure để tạo một phạm vi riêng cho mỗi lần lặp:

Giải pháp với closure:

for (var i = 1; i <= 3; i++) {
    (function(i) {
        setTimeout(function() {
            console.log(i); // In ra 1, 2, 3
        }, 1000);
    })(i); // Truyền i vào một hàm ẩn danh
}

Trong đoạn code trên, chúng ta đã tạo một hàm ẩn danh ngay lập tức (IIFE) để tạo ra một phạm vi riêng cho mỗi giá trị của i. Điều này đảm bảo rằng giá trị i trong mỗi lần gọi setTimeout là khác nhau và tương ứng với từng lần lặp.

Kết luận

Closure là một trong những tính năng quan trọng và mạnh mẽ của JavaScript, cho phép hàm truy cập và lưu giữ các biến từ phạm vi hàm bao quanh ngay cả khi hàm bao quanh đã thực thi xong. Closure rất hữu ích khi bạn cần lưu trữ trạng thái, tạo hàm có thể truy cập vào dữ liệu bên ngoài, và giải quyết các vấn đề liên quan đến phạm vi biến trong các vòng lặp.