[Advanced JS] Closure là gì? Ứng dụng nâng cao của Closure trong JavaScript

Nếu bạn đã từng dạo quanh các diễn đàn lập trình hay tham gia các buổi phỏng vấn về JavaScript, chắc chắn bạn đã nghe đến từ khóa "Closure". Nó thường được miêu tả như một khái niệm khó nhằn, trừu tượng, nhưng cũng là chìa khóa để mở ra cánh cửa của một lập trình viên JavaScript chuyên nghiệp.

Closure là gì? Ứng dụng nâng cao của Closure trong JavaScript

Vậy Closure thực sự là gì? Tại sao nó lại quan trọng đến vậy? Và làm thế nào để chúng ta không chỉ hiểu mà còn có thể sử dụng nó một cách thành thạo? Hãy cùng nhau vén bức màn bí ẩn này nhé!

1. Closure là gì? Một định nghĩa dễ hiểu 💡

Hãy quên đi những định nghĩa học thuật phức tạp trong giây lát. Hãy tưởng tượng thế này:

Một hàm (function) giống như một người công nhân. Khi được tạo ra, người công nhân này được phát cho một chiếc ba lô. Bên trong chiếc ba lô đó chứa tất cả những "dụng cụ" (biến, hằng số) mà người đó có thể cần, lấy từ chính nơi anh ta được "sinh ra". Ngay cả khi người công nhân này được cử đi làm ở một nơi hoàn toàn khác, anh ta vẫn luôn mang theo chiếc ba lô đó và có thể sử dụng các dụng cụ bên trong.

Closure chính là sự kết hợp của hàm đó và chiếc ba lô (phạm vi từ vựng) mà nó mang theo.

Giải thích khái niệm Closure trong JavaScript

Bây giờ, hãy đến với định nghĩa chính thức hơn:

Closure là sự kết hợp giữa một hàm và môi trường từ vựng (lexical environment) nơi hàm đó được khai báo. Closure cho phép một hàm con truy cập và thao tác với các biến của hàm cha, ngay cả khi hàm cha đã thực thi xong.

Nghe vẫn hơi khó hiểu? Đừng lo, hãy xem ví dụ kinh điển sau đây.

function createCharacter() {
  let characterName = 'Luffy' // Biến được định nghĩa trong hàm cha

  function showName() {
    // Hàm con
    console.log(characterName) // Có thể truy cập biến của hàm cha
  }

  return showName // Trả về chính hàm con
}

// Gọi hàm createCharacter, nó thực thi và trả về hàm showName
// Chúng ta gán hàm được trả về vào biến callName
const callName = createCharacter()

// Bây giờ, hàm createCharacter đã chạy xong.
// Về lý thuyết, biến 'characterName' phải bị xóa khỏi bộ nhớ.

// NHƯNG...
callName() // Output: "Luffy"

Điều kỳ diệu gì đã xảy ra? Khi createCharacter được gọi, nó đã trả về hàm showName. Hàm showName này khi được tạo ra đã "đóng gói" (closed over) môi trường của nó, bao gồm cả biến characterName. Nó mang theo "chiếc ba lô" chứa characterName bên mình. Vì vậy, dù createCharacter đã kết thúc, callName (chính là showName) vẫn nhớ và truy cập được characterName. Đó chính là Closure!

2. Tại sao Closures lại quan trọng? Sức mạnh thực sự 🧠

Closures không chỉ là một câu đố lý thuyết. Chúng là nền tảng cho rất nhiều mẫu thiết kế (design patterns) và tính năng mạnh mẽ trong JavaScript.

🔐 Data Encapsulation - Che giấu dữ liệu và tạo biến riêng tư

Trong nhiều ngôn ngữ lập trình hướng đối tượng, bạn có các từ khóa như private để bảo vệ dữ liệu. JavaScript (trước đây) không có khái niệm này một cách chính thức, và Closures chính là cứu cánh.

Hãy xem ví dụ về một bộ đếm (counter):

function createCounter() {
  let count = 0 // Biến 'count' này là "riêng tư"

  return {
    increment: function () {
      count++
      console.log(count)
    },
    decrement: function () {
      count--
      console.log(count)
    },
    getValue: function () {
      return count
    },
  }
}

const counter = createCounter()

counter.increment() // Output: 1
counter.increment() // Output: 2
counter.decrement() // Output: 1

// Bạn không thể truy cập trực tiếp vào 'count' từ bên ngoài
console.log(counter.count) // Output: undefined

Ở đây, biến count sống bên trong createCounter. Chúng ta không thể truy cập hay thay đổi nó từ bên ngoài. Cách duy nhất để tương tác với count là thông qua các phương thức increment, decrement, và getValue được trả về. Các phương thức này tạo thành một closure, "nhớ" và chia sẻ cùng một biến count. Đây chính là nguyên tắc che giấu dữ liệu.

🏭 Function Factories - Nhà máy sản xuất hàm

Closures cho phép bạn tạo ra các hàm đã được "cấu hình sẵn".

function makeGreeter(greeting) {
  return function (name) {
    console.log(`${greeting}, ${name}!`)
  }
}

const sayHello = makeGreeter('Hello')
const sayXinChao = makeGreeter('Xin chào')

sayHello('John') // Output: Hello, John!
sayXinChao('Sơn') // Output: Xin chào, Sơn!

Hàm makeGreeter là một "nhà máy". Mỗi lần gọi nó, bạn tạo ra một hàm mới (sayHello, sayXinChao) đã được "ghi nhớ" sẵn giá trị greeting khác nhau.

⏳ Callbacks và Asynchronous programming

Đây là một trong những ứng dụng phổ biến nhất của closures. Khi bạn làm việc với setTimeout, event listeners, hay Promises, bạn đang sử dụng closures mọi lúc mà có thể không nhận ra.

function waitAndSay(message, delay) {
  setTimeout(function () {
    // Hàm callback này là một closure
    // Nó "nhớ" biến 'message' từ môi trường bên ngoài
    console.log(message)
  }, delay)
}

waitAndSay('Đợi 3 giây và tôi xuất hiện.', 3000)

Hàm callback được truyền vào setTimeout sẽ được thực thi sau 3 giây. Tại thời điểm đó, hàm waitAndSay đã chạy xong từ lâu. Nhưng nhờ có closure, hàm callback vẫn "nhớ" được giá trị của message khi nó được tạo ra.

3. Cạm bẫy thường gặp: Vòng lặp và Closures ⚠️

Đây là một ví dụ kinh điển thường xuất hiện trong các buổi phỏng vấn, làm bối rối rất nhiều lập trình viên.

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

Nhiều người sẽ kỳ vọng kết quả là 1, 2, 3, 4, 5 được in ra sau mỗi giây. Nhưng kết quả thực tế lại là: 6 6 6 6 6

Tại sao?

  • var có phạm vi hàm (function scope): Chỉ có một biến i duy nhất được chia sẻ cho tất cả các lần lặp.
  • Bất đồng bộ: Vòng lặp for chạy rất nhanh và hoàn thành gần như ngay lập tức. Nó xếp 5 lệnh setTimeout vào hàng đợi. Khi vòng lặp kết thúc, giá trị của i6.
  • Closure: Sau 1 giây, 2 giây,... khi các hàm callback trong setTimeout được thực thi, chúng truy cập vào biến i. Vì tất cả chúng đều tham chiếu đến cùng một biến i, chúng đều thấy giá trị cuối cùng của nó là 6.

Cách giải quyết?

Sử dụng let: Đây là cách hiện đại và đơn giản nhất. let có phạm vi khối (block scope), nghĩa là mỗi lần lặp của vòng for, một biến i mới sẽ được tạo ra.

for (let i = 1; i <= 5; i++) {
  setTimeout(function () {
    // Mỗi callback bây giờ có một closure với biến 'i' riêng của nó
    console.log(i)
  }, i * 1000)
}
// Kết quả: 1, 2, 3, 4, 5 (đúng như mong đợi)

Kết luận: Closures không phải là "ma thuật" 🔮

Closures không phải là thứ gì đó quá khó hiểu. Đó là một hệ quả tự nhiên của cách JavaScript xử lý phạm vi biến (cụ thể là Lexical Scoping - phạm vi được xác định tại thời điểm viết code, không phải lúc chạy code).

Việc nắm vững Closures sẽ giúp bạn:

  • Viết code sạch hơn, module hóa tốt hơn.
  • Hiểu sâu hơn về các khái niệm cốt lõi như scope, context.
  • Tự tin chinh phục các mẫu thiết kế nâng cao và các framework JavaScript hiện đại.

Hy vọng rằng qua bài viết này, "Closure" không còn là một từ khóa đáng sợ, mà đã trở thành một công cụ mạnh mẽ trong bộ kỹ năng JavaScript của bạn. Hãy thực hành, thử nghiệm với các ví dụ, và bạn sẽ thấy sức mạnh của nó thật đáng kinh ngạc.

Chúc bạn thành công!

Bài viết liên quan

[Advanced JS] IIFE là gì? Ứng dụng nâng cao của IIFE trong JavaScript

Tìm hiểu về IIFE (Immediately Invoked Function Expression) và cách nó hoạt động trong JavaScript. Khám phá các ứng dụng thực tế của IIFE để tạo scope và bảo vệ biến.

[JS Basics] Phân biệt Shallow Copy và Deep Copy trong JavaScript

Bạn có chắc đã sao chép đối tượng đúng cách? Tìm hiểu sâu về Shallow copy và Deep copy trong JavaScript qua ví dụ minh họa, từ đó lựa chọn phương pháp phù hợp cho ứng dụng của mình.

[JS Basics] Cách thiết lập môi trường chạy mã JavaScript

Bạn là người mới học lập trình và muốn bắt đầu với JavaScript? Bài viết này sẽ hướng dẫn bạn từng bước cách thiết lập môi trường chạy JavaScript trên máy tính một cách dễ dàng và nhanh chóng nhất.

[JS Basics] ES6+ Features: Những điều cần biết để viết code JavaScript hiện đại

Bài viết này là cẩm nang chi tiết nhất về JavaScript Events. Tìm hiểu cách lắng nghe và phản ứng với các hành động của người dùng trên website, giúp bạn xây dựng ứng dụng web tương tác và mượt mà hơn.