Closure là một khái niệm quan trọng trong ngôn ngữ lập trình JavaScript, mang lại tính linh hoạt và mạnh mẽ trong quản lý biến và hàm. Hãy đi sâu vào cơ bản và xem cách closure hoạt động cùng với một số ví dụ chi tiết.

1. Closure là gì?

Closure xảy ra khi một hàm được định nghĩa bên trong một hàm khác và có thể truy cập các biến của hàm bên ngoài nó. Điều này tạo ra một môi trường đóng, giữ cho các biến vẫn tồn tại ngay cả sau khi hàm bên ngoài đã hoàn thành.

Trong JavaScript, các hàm là các đối tượng đầu tiên (first-class objects), có thể được truyền qua như là tham số, được gán cho biến, và được trả về từ một hàm khác. Khi một hàm được định nghĩa trong một hàm khác, closure xuất hiện.

Anh em cũng có thể đọc định nghĩa chính xác ở đây: Closures - JavaScript | MDN (mozilla.org)


2. Lexical Scoping

Đây là một khái niệm anh em cũng cần phải biết và rất quan trọng khi nói về closure. Vậy lexical scope là gì?

Lexical Scoping xác định phạm vi của biến bằng cách nói về nơi mà biến được khai báo trong code. Nếu một biến được khai báo trong một hàm, nó chỉ có thể truy cập được trong phạm vi của hàm đó. Điều này tạo ra sự nhất quán và dễ dàng trong việc đọc code.


3. Cấu trúc của Closure

Một closure bao gồm hai phần chính: hàm (inner function) và biến môi trường (environment variable). Hàm này được định nghĩa bên trong một hàm khác, và mình có thể truy cập các biến của hàm bên ngoài mình, tạo ra một môi trường đóng (enclosed environment).

Anh em cùng xem ví dụ dưới đây để hiểu rõ hơn về cấu trúc của nó:


function outerFunction() {
  let outerVariable = 'Mình ở ngoài này!';

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

const closureExample = outerFunction();
closureExample(); // Output: Mình ở ngoài này!


Trong ví dụ trên, innerFunction là một closure và giữ được Lexical Scope của outerFunction, nơi biến outerVariable được khai báo. Do đó, khi innerFunction được gọi, nó có thể truy cập và in ra giá trị của outerVariable.


4. Sử dụng Closure để Quản lý Trạng Thái

Closure có thể được sử dụng để giữ trạng thái và tạo ra các hàm hoặc phương thức có thể truy cập vào biến môi trường của chúng ngay cả sau khi hàm cha đã hoàn thành.

function counter() {
  let count = 0;


  function increment() {
    count++;
    console.log(count);
  }


  return increment;
}

const counter1 = counter();
counter1(); // Output: 1
counter1(); // Output: 2

Trong ví dụ này, counter tạo ra một closure với biến count, giúp duy trì trạng thái và tính toán kết quả mỗi khi gọi counter1.


5. Bảo vệ Dữ Liệu với Closure

Closure giúp bảo vệ dữ liệu bằng cách giữ nó trong phạm vi của hàm, ngăn chặn truy cập trực tiếp từ bên ngoài:

function privateData() {
  let secret = 'This is a secret!';


  return function() {
    console.log(secret);
  };
}


const revealSecret = privateData();
revealSecret(); // Output: This is a secret!

Trong ví dụ trên, biến secret chỉ có thể được truy cập thông qua closure, bảo vệ dữ liệu khỏi sự can thiệp từ bên ngoài.


6. Closure và Asynchronous JavaScript

Closure cũng có thể sử dụng trong xử lý bất đồng bộ, như trong trường hợp của setTimeout:

function delayedGreeting(message, delay) {
  setTimeout(function() {
    console.log(message);
  }, delay);
}

delayedGreeting('Hello after 2 seconds!', 2000);

Trong ví dụ này, hàm setTimeout tạo ra một closure để giữ lại giá trị của message, ngay cả khi delayedGreeting đã kết thúc thực thi.


7. Một số lỗi về closure bạn cần tránh

Sử dụng closure có thể mang lại sự tiện lợi và tính linh hoạt trong việc quản lý biến và hàm trong JavaScript, nhưng cũng có thể dẫn đến một số lỗi nếu không được sử dụng đúng cách. Dưới đây là một số vấn đề thường gặp và cách giải quyết chúng:


Lỗi về Reference:

Ví dụ:

function outer() {
  let outerVar = 'I am outer!';
  function inner() {
    console.log(outerVar);
    console.log(innerVar); // Lỗi: innerVar is not defined
  }
  inner();
}

outer();

Giải thích: Lỗi này xuất hiện khi biến không được khai báo đúng trong closure. Trong ví dụ, innerVar không tồn tại trong phạm vi của inner.


Lỗi về Closure và Loop:

Ví dụ:

for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

Giải thích: Sử dụng var thay vì let tạo ra một biến i chia sẻ giữa các closure, dẫn đến việc in ra giá trị 6 cho mọi hàm setTimeout.

Giải quyết: Sử dụng let để tạo biến mới trong mỗi vòng lặp, đảm bảo mỗi closure giữ một giá trị riêng biệt của i.

for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}


Lỗi về Quản lý Bộ Nhớ (Memory Management):

Ví dụ:

function outer() {
  let bigData = new Array(1000000).fill('Big Data');
  return function inner() {
    console.log(bigData[0]);
  };
}

const closureExample = outer();
closureExample();

Giải thích: Closure giữ lại tham chiếu đến bigData, làm cho nó không thể được giải phóng bộ nhớ khi hàm inner không còn được sử dụng nữa.

Giải quyết: Hạn chế việc giữ các tham chiếu lớn trong closure, đặc biệt là khi chúng không còn cần thiết. Cân nhắc việc giải phóng bộ nhớ thủ công nếu cần thiết.


Lỗi về Performance:

Ví dụ:

function unnecessaryClosure() {
  let data = 'Some data';
  return function() {
    console.log(data);
  };
}

const closure1 = unnecessaryClosure();
const closure2 = unnecessaryClosure();

Giải thích: Tạo các closure không cần thiết với cùng một tham chiếu đến data có thể dẫn đến tốn tài nguyên và làm giảm hiệu suất.

Giải quyết: Tạo closure chỉ khi thực sự cần thiết và đảm bảo rằng chúng không giữ các tham chiếu không cần thiết. Điều này giúp cải thiện hiệu suất và giảm tải cho trình duyệt.


8. Kết Luận

Closure là một khái niệm khá khó hiểu để vận dụng hiệu quả đúng đắn khi làm việc với Javascript. Hy vọng sau bài viết này giúp anh em có cái nhìn rõ hơn về closure nhé.