Closure là gì?

1 Định nghĩa:

Closure là một hàm có khả năng truy cập các biến trong phạm vi của hàm cha nơi nó được tạo ra, ngay cả khi hàm cha đã hoàn tất việc thực thi.
Ví dụ:

function createCounter() {
    let count = 0; // Biến trong phạm vi hàm cha
    return function() {
        count++; // Hàm con sử dụng biến của hàm cha
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // Kết quả: 1
console.log(counter()); // Kết quả: 2

2. Một số tình huống phổ biến khi sử dụng Closure

Khi cần giữ lại trạng thái của một biến trong một hàm con.

  • Closure giúp bạn truy cập các biến bên ngoài hàm con, ngay cả khi hàm cha đã thực thi xong.
  • Ví dụ:
function counter() {
    let count = 0;
    return function() {
        count++;
        console.log(count);
    };
}
const increment = counter();
increment(); // 1
increment(); // 2
increment(); // 3

Khi cần sử dụng hàm callback với dữ liệu ngoài phạm vi hàm đó.

  • Closure thường dùng trong các hàm callback hoặc xử lý bất đồng bộ.

Ví dụ:

function greet(name) {
    return function() {
        console.log("Hello, " + name);
    };
}
const greetJohn = greet("John");
greetJohn(); // "Hello, John"

 

Khi cần bảo vệ các biến khỏi bị thay đổi trực tiếp từ bên ngoài.

  • Closure có thể tạo ra các hàm “setter” và “getter” giúp kiểm soát quyền truy cập vào các giá trị.

Ví dụ:

function createPerson(name) {
    let _name = name;  // _name không thể thay đổi trực tiếp từ bên ngoài
    return {
        getName: function() {
            return _name;
        },
        setName: function(newName) {
            _name = newName;
        }
    };
}
const person = createPerson("Alice");
console.log(person.getName()); // Alice
person.setName("Bob");
console.log(person.getName()); // Bob

Khi sử dụng với các sự kiện trong DOM (Event Listeners).

  • Closure giúp bạn giữ lại dữ liệu trong các hàm xử lý sự kiện.

Ví dụ:

function buttonClickHandler() {
    let count = 0;
    return function() {
        count++;
        console.log("Button clicked " + count + " times");
    };
}
const button = document.querySelector("button");
const handler = buttonClickHandler();
button.addEventListener("click", handler);

Khi cần tạo các hàm với đối số đặc biệt và kết quả thay đổi theo thời gian.

  • Closure có thể lưu lại các giá trị cần thiết và thay đổi theo yêu cầu.

Ví dụ:

function makeMultiplier(factor) {
    return function(number) {
        return number * factor;
    };
}
const multiplyBy2 = makeMultiplier(2);
console.log(multiplyBy2(5)); // 10
const multiplyBy3 = makeMultiplier(3);
console.log(multiplyBy3(5)); // 15

3. Nhược điểm

  • Tiêu tốn bộ nhớ (Memory consumption):
    • Closure có thể gây ra rò rỉ bộ nhớ nếu không được quản lý đúng cách. Bởi vì hàm con giữ tham chiếu đến các biến trong phạm vi hàm cha, các giá trị đó không thể bị thu hồi (garbage collected) cho đến khi hàm con không còn tham chiếu đến chúng.
    • Ví dụ: Nếu bạn tạo nhiều closure trong một vòng lặp mà không giải phóng tài nguyên, bộ nhớ sẽ tăng lên.
    function createClosure() {
        let largeData = new Array(1000000).fill('data');
        return function() {
    // Hàm này giữ tham chiếu đến largeData
            console.log(largeData[0]);
        };
    }
    const closure1 = createClosure(); // largeData không thể bị thu hồi
    const closure2 = createClosure(); // Tiếp tục giữ tham chiếu
    // Không giải phóng largeData, sẽ tiêu tốn bộ nhớ
    

    Ở ví dụ trên, nếu không giải phóng các closure sau khi sử dụng, bộ nhớ sẽ không được giải phóng đúng cách, gây ra sự tiêu tốn bộ nhớ.

  • Khó kiểm soát và dễ gây nhầm lẫn:
    • Việc các hàm con giữ tham chiếu đến các biến bên ngoài có thể gây nhầm lẫn về nơi và cách các giá trị được thay đổi, đặc biệt khi có nhiều closure hoạt động cùng lúc.
    • Điều này có thể làm mã trở nên khó hiểu, khó gỡ lỗi và bảo trì, đặc biệt khi các closure có tham chiếu đến nhiều biến và trạng thái khác nhau.
  • Không rõ ràng trong việc kiểm soát vòng đời của biến:
    • Vì closure giữ các biến bên ngoài, việc quản lý vòng đời của các biến này có thể phức tạp, nhất là khi có nhiều closure đang hoạt động. Nếu không cẩn thận, các biến có thể bị giữ lại trong bộ nhớ lâu hơn cần thiết.
  • Hiệu suất giảm khi sử dụng nhiều closure:
    • Nếu sử dụng quá nhiều closure trong mã, đặc biệt trong các tình huống vòng lặp hoặc xử lý hàng loạt, nó có thể làm giảm hiệu suất. Closure tạo ra các hàm mới, và mỗi hàm này có thể có chi phí tài nguyên và bộ nhớ riêng.
  • Khó kiểm tra và thử nghiệm (Testing):
    • Các closure có thể làm việc với các trạng thái và dữ liệu trong phạm vi riêng biệt, khiến việc kiểm tra và thử nghiệm mã trở nên khó khăn hơn. Khi một biến hoặc hàm được đóng gói trong một closure, bạn không thể truy cập trực tiếp hoặc kiểm soát nó ngoài phạm vi closure đó.

4. Cách giải phóng Closure

1. Giải phóng các tham chiếu không còn sử dụng

  • Một trong những cách đơn giản nhất để giải phóng bộ nhớ là đảm bảo rằng các tham chiếu đến các closure hoặc đối tượng không còn được sử dụng nữa, vì khi không có tham chiếu nào đến một đối tượng, Garbage Collector (GC) sẽ tự động thu hồi bộ nhớ.

Ví dụ:

function createClosure() {
    let largeData = new Array(1000000).fill('some data');
    return function() {
        console.log(largeData[0]);
    };
}

const closure1 = createClosure();
// Giải phóng closure1 khi không còn cần sử dụng nó nữa
closure1 = null;  // Xóa tham chiếu để GC có thể thu hồi bộ nhớ

2. Sử dụng WeakMap hoặc WeakSet

  • Các cấu trúc dữ liệu như WeakMap hoặc WeakSet cho phép bạn lưu trữ các đối tượng một cách “yếu”, nghĩa là nếu không còn tham chiếu nào đến đối tượng đó, thì đối tượng sẽ tự động bị xóa mà không cần phải làm gì thêm.

Ví dụ sử dụng WeakMap:

const cache = new WeakMap();

function createClosure() {
    let largeData = new Array(1000000).fill('some data');
    return function() {
        console.log(largeData[0]);
    };
}

const closure = createClosure();
cache.set(closure, "some value");

// Sau khi không còn tham chiếu đến closure, đối tượng closure sẽ được GC thu hồi

3. Xóa các sự kiện DOM hoặc listeners

  • Nếu bạn sử dụng closure trong các sự kiện DOM (event listeners), cần đảm bảo xóa các sự kiện này khi không cần thiết nữa, vì closure có thể giữ tham chiếu đến đối tượng DOM, gây rò rỉ bộ nhớ.
function createClickHandler() {
    let count = 0;
    return function() {
        count++;
        console.log(`Clicked ${count} times`);
    };
}

const button = document.querySelector("button");
const clickHandler = createClickHandler();
button.addEventListener("click", clickHandler);

// Khi không cần thiết nữa, xóa event listener
button.removeEventListener("click", clickHandler);