Đóng gói hàm (Closures) trong JavaScript
Javascript nâng cao | by
Trong JavaScript, Closure là một khái niệm quan trọng giúp các hàm có thể ghi nhớ và truy cập biến từ phạm vi (scope) mà chúng được tạo ra, ngay cả khi phạm vi đó đã kết thúc. Closure đóng vai trò quan trọng trong lập trình vì nó cho phép bảo vệ dữ liệu, tạo ra các hàm có trạng thái riêng và hỗ trợ lập trình hàm (Functional Programming).
Nhờ vào cơ chế ghi nhớ phạm vi, Closure giúp tránh ô nhiễm biến toàn cục, cung cấp cách thức để tạo biến private trong JavaScript (mô phỏng tính đóng gói của lập trình hướng đối tượng). Ngoài ra, nó còn được sử dụng rộng rãi trong các tình huống thực tế như xử lý bất đồng bộ, tạo hàm callback, module pattern, và tối ưu hiệu suất ứng dụng.
Hiểu rõ về Closure không chỉ giúp lập trình viên viết mã hiệu quả hơn mà còn là nền tảng để làm chủ JavaScript, đặc biệt trong các ứng dụng web phức tạp.
Closure là gì?
Closure là một tính năng của JavaScript cho phép một hàm có thể ghi nhớ phạm vi (scope) nơi nó được tạo ra và có thể truy cập các biến trong phạm vi đó, ngay cả khi hàm được gọi bên ngoài phạm vi ban đầu.
Nói cách khác, Closure giúp một hàm "nhớ" được các biến mà nó đã truy cập khi được định nghĩa, ngay cả khi phạm vi chứa nó đã kết thúc.
Cấu trúc của một Closure
Một Closure thường bao gồm:
-
Hàm cha (outer function): Định nghĩa các biến cục bộ.
-
Hàm con (inner function): Truy cập vào biến của hàm cha, ngay cả khi hàm cha đã kết thúc.
-
Một tham chiếu từ bên ngoài để duy trì trạng thái của các biến trong phạm vi của hàm cha.
Ví dụ minh họa cơ bản về Closure
function outerFunction() { let counter = 0; // Biến cục bộ trong outerFunction function innerFunction() { counter++; // innerFunction có thể truy cập biến counter của outerFunction console.log(counter); } return innerFunction; // Trả về innerFunction mà không thực thi ngay } const increment = outerFunction(); // Gọi outerFunction và lưu kết quả vào biến increment increment(); // Output: 1 increment(); // Output: 2 increment(); // Output: 3
Giải thích:
-
Khi
outerFunction()
được gọi, nó tạo ra một biếncounter
và một hàm coninnerFunction
có thể truy cậpcounter
. -
Hàm
innerFunction
được trả về nhưng chưa thực thi ngay, thay vào đó nó được gán vào biếnincrement
. -
Khi
increment()
được gọi lần đầu tiên, nó tăngcounter
lên1
và in ra1
. -
Do Closure giữ lại tham chiếu đến
counter
, nên mỗi lần gọiincrement()
,counter
vẫn tồn tại và tiếp tục tăng.
Lợi ích của Closure trong ví dụ này
-
Ghi nhớ trạng thái của biến (
counter
) ngay cả khi hàm cha (outerFunction
) đã hoàn thành. -
Tạo ra biến "private" vì
counter
không thể bị thay đổi từ bên ngoàiinnerFunction
.
Closure là một trong những khái niệm quan trọng nhất trong JavaScript, giúp tăng cường tính bảo mật và hiệu quả của mã nguồn.
Cách hoạt động của Closure trong JavaScript
Phạm vi (Scope) và Chuỗi phạm vi (Scope Chain) trong JavaScript
Để hiểu Closure hoạt động như thế nào, trước tiên chúng ta cần hiểu về phạm vi (Scope) và chuỗi phạm vi (Scope Chain) trong JavaScript.
Phạm vi (Scope) là vùng mà một biến có thể được truy cập. Trong JavaScript, có ba loại phạm vi chính:
-
Global Scope (Phạm vi toàn cục): Biến được khai báo ngoài mọi hàm có thể được truy cập từ bất kỳ đâu trong chương trình.
-
Function Scope (Phạm vi hàm): Biến khai báo bên trong một hàm chỉ có thể được truy cập trong hàm đó.
-
Block Scope (Phạm vi khối): Biến khai báo với
let
hoặcconst
bên trong một khối{}
chỉ tồn tại trong khối đó.
Chuỗi phạm vi (Scope Chain): Khi JavaScript cần truy cập một biến, nó tìm kiếm biến đó trong phạm vi hiện tại. Nếu không tìm thấy, nó sẽ tìm trong phạm vi bên ngoài, tiếp tục tìm lên cấp cao hơn cho đến khi đạt phạm vi toàn cục.
Cách Closure ghi nhớ biến ngay cả khi phạm vi bên ngoài đã kết thúc
Thông thường, khi một hàm kết thúc, tất cả các biến cục bộ bên trong nó sẽ bị xóa khỏi bộ nhớ do cơ chế Garbage Collection. Tuy nhiên, nếu một hàm con bên trong vẫn tham chiếu đến các biến này, JavaScript sẽ không xóa chúng mà giữ lại để phục vụ hàm con. Đây chính là cách Closure ghi nhớ biến ngay cả khi hàm cha đã kết thúc.
Cụ thể, khi một hàm con được trả về từ một hàm cha, nó vẫn giữ quyền truy cập vào các biến của hàm cha, ngay cả khi hàm cha đã hoàn thành thực thi.
Ví dụ 1: Closure ghi nhớ biến của hàm cha
function outerFunction() { let message = "Hello, Closure!"; // Biến cục bộ trong outerFunction function innerFunction() { console.log(message); // Truy cập biến của outerFunction } return innerFunction; // Trả về hàm innerFunction nhưng chưa gọi ngay } const myClosure = outerFunction(); // outerFunction() thực thi và trả về innerFunction myClosure(); // Output: "Hello, Closure!"
Giải thích:
-
outerFunction()
chạy và tạo ra biếnmessage
, sau đó trả vềinnerFunction
. -
myClosure
nhận giá trị làinnerFunction
, nhưng không gọi ngay. -
Khi
myClosure()
được gọi sau này,innerFunction
vẫn nhớ biếnmessage
củaouterFunction
, dùouterFunction
đã hoàn thành.
Ví dụ 2: Closure duy trì trạng thái biến sau nhiều lần gọi
function counterFunction() { let count = 0; // Biến cục bộ của counterFunction return function() { count++; // Mỗi lần gọi sẽ tăng biến count console.log(count); }; } const counter = counterFunction(); // counterFunction() trả về hàm và gán vào counter counter(); // Output: 1 counter(); // Output: 2 counter(); // Output: 3
Giải thích:
-
counterFunction()
chạy một lần và trả về một hàm, nhưng không bị xóa khỏi bộ nhớ vì biếncount
vẫn được Closure tham chiếu. -
Mỗi lần gọi
counter()
, biếncount
vẫn tồn tại và tiếp tục tăng. -
Closure giúp biến
count
tồn tại sau khi hàm cha đã kết thúc, thay vì bị xóa như các biến thông thường.
Ứng dụng của Closure trong JavaScript
Closure là một trong những tính năng mạnh mẽ và quan trọng nhất của JavaScript. Dưới đây là một số ứng dụng thực tế của Closure giúp tận dụng tối đa khả năng của nó trong lập trình.
Bảo vệ biến khỏi bị thay đổi từ bên ngoài (Data Encapsulation)
Closure giúp ẩn dữ liệu và ngăn chặn việc truy cập hoặc thay đổi biến từ bên ngoài, tạo ra một dạng biến private trong JavaScript.
Ví dụ: Tạo biến private bằng Closure
function createCounter() { let count = 0; // Biến private return { increment: function() { count++; console.log(count); }, decrement: function() { count--; console.log(count); }, getCount: function() { return count; } }; } const counter = createCounter(); counter.increment(); // Output: 1 counter.increment(); // Output: 2 counter.decrement(); // Output: 1 console.log(counter.getCount()); // Output: 1 console.log(counter.count); // Undefined (không thể truy cập trực tiếp)
Giải thích:
-
Biến
count
được khai báo bên trongcreateCounter()
nên không thể truy cập từ bên ngoài. -
Closure giữ
count
sống sau khicreateCounter()
hoàn thành. -
Chỉ có thể thay đổi giá trị của
count
thông qua các phương thứcincrement()
,decrement()
,getCount()
.
Tạo hàm có trạng thái riêng biệt (Stateful Functions)
Closure giúp tạo các hàm có trạng thái riêng biệt, giúp duy trì dữ liệu mà không cần biến toàn cục.
Ví dụ: Tạo nhiều bộ đếm độc lập
function createCounter(start = 0) { let count = start; return function() { count++; console.log(count); }; } const counter1 = createCounter(5); const counter2 = createCounter(10); counter1(); // Output: 6 counter1(); // Output: 7 counter2(); // Output: 11 counter2(); // Output: 12
Giải thích:
-
counter1
vàcounter2
có biếncount
độc lập, không ảnh hưởng lẫn nhau. -
Closure giúp duy trì giá trị
count
riêng biệt cho từng instance củacreateCounter()
.
Dùng trong lập trình bất đồng bộ (Asynchronous Programming)
Closure giúp lưu trữ dữ liệu trong các callback, rất hữu ích khi làm việc với setTimeout(), setInterval() hoặc Promise.
Ví dụ: Dùng Closure trong setTimeout()
function delayedMessage(message, delay) { setTimeout(function() { console.log(message); }, delay); } delayedMessage("Hello after 2 seconds", 2000);
Giải thích:
-
Hàm
setTimeout()
chạy sau một khoảng thời gian, nhưng closure giúp nó vẫn nhớ được biếnmessage
. -
Nếu không có closure,
message
có thể bị mất trước khisetTimeout()
thực thi.
Tạo hàm Callback linh hoạt
Closure giúp tạo ra các hàm callback có tham số động, giúp code trở nên linh hoạt hơn.
Ví dụ: Closure với event listener
function createLogger(prefix) { return function(message) { console.log(`[${prefix}] ${message}`); }; } const infoLogger = createLogger("INFO"); const errorLogger = createLogger("ERROR"); infoLogger("User logged in"); // Output: [INFO] User logged in errorLogger("Server not responding"); // Output: [ERROR] Server not responding
Giải thích:
-
createLogger()
tạo ra các hàm có tiền tố (prefix
) riêng. -
Closure giúp
infoLogger()
vàerrorLogger()
lưu trạng thái mà không dùng biến toàn cục.
Ứng dụng Closure trong Module Pattern trong JavaScript
Closure giúp tạo module riêng biệt, giúp mã nguồn dễ bảo trì hơn bằng cách ẩn đi các biến và chỉ cung cấp các phương thức cần thiết.
Ví dụ: Module Pattern với Closure
const CounterModule = (function() { let count = 0; // Biến private return { increment: function() { count++; console.log(count); }, decrement: function() { count--; console.log(count); }, getCount: function() { return count; } }; })(); CounterModule.increment(); // Output: 1 CounterModule.increment(); // Output: 2 CounterModule.decrement(); // Output: 1 console.log(CounterModule.getCount()); // Output: 1
Giải thích:
-
CounterModule
là một module đóng gói với biếncount
private. -
Chỉ có thể thao tác
count
thông qua các phương thứcincrement()
,decrement()
,getCount()
. -
Tránh ô nhiễm biến toàn cục, giúp code dễ bảo trì hơn.
Các tình huống thường gặp với Closure trong JavaScript
Closure là một tính năng mạnh mẽ trong JavaScript, nhưng nó cũng có thể gây ra một số vấn đề phổ biến, đặc biệt là trong vòng lặp, hàm setTimeout(), và lập trình hướng đối tượng. Dưới đây là những tình huống hay gặp phải và cách khắc phục.
Closure trong vòng lặp (Loop and Closure problem) và cách khắc phục
Một vấn đề phổ biến khi sử dụng closure trong vòng lặp là biến trong closure có thể không giữ được giá trị mong muốn do JavaScript sử dụng biến tham chiếu khi chạy vòng lặp.
Ví dụ lỗi với var trong vòng lặp
for (var i = 1; i <= 3; i++) { setTimeout(function() { console.log(i); }, 1000); }
Kết quả mong đợi:
1 2 3
Kết quả thực tế:
4 4 4
Lý do:
-
Biến
i
được khai báo bằngvar
nên nó có phạm vi function scope, nghĩa là sau khi vòng lặp kết thúc,i = 4
(vìi
tăng đến 4 thì vòng lặp dừng). -
Tất cả các hàm
setTimeout()
đều tham chiếu đến cùng một biếni
, nên khi chúng chạy sau 1 giây, giá trị củai
là 4.
Sử dụng let để tạo block scope
for (let i = 1; i <= 3; i++) { setTimeout(function() { console.log(i); }, 1000); }
Kết quả đúng:
1 2 3
Lý do:
-
let
có phạm vi block scope, mỗi lần lặp tạo một bản sao riêng củai
, nên mỗisetTimeout()
ghi nhớ giá trịi
khác nhau.
Dùng IIFE để tạo closure riêng
for (var i = 1; i <= 3; i++) { (function(i) { setTimeout(function() { console.log(i); }, 1000); })(i); }
Kết quả đúng:
1 2 3
Lý do:
-
Mỗi lần lặp, một hàm IIFE (
function(i){}
) được tạo mới vài
được truyền vào, tạo một closure riêng biệt giữ giá trị củai
.
Closure trong setTimeout() và setInterval()
Closure thường gây lỗi khi sử dụng với setTimeout()
hoặc setInterval()
nếu không xử lý đúng cách.
Ví dụ lỗi với setTimeout()
function startTimer() { var count = 1; setTimeout(function() { console.log(count); count++; }, 2000); } startTimer(); console.log(count); // Lỗi: count không được định nghĩa
Lỗi: count
chỉ tồn tại trong phạm vi của startTimer()
, nhưng nó không được cập nhật sau mỗi lần chạy setTimeout()
.
Dùng closure để duy trì trạng thái
function startTimer() { var count = 1; function printCount() { console.log(count); count++; setTimeout(printCount, 2000); // Gọi lại chính nó để lặp vô hạn } printCount(); } startTimer();
Kết quả đúng:
1 2 (sau 2 giây) 3 (sau 2 giây) ...
Lý do:
-
printCount()
tạo một closure giữcount
trong phạm vi củastartTimer()
, giúp duy trì trạng tháicount
cho mỗi lần gọi.
Closure trong lập trình hướng đối tượng (OOP)
Trong lập trình hướng đối tượng, closure giúp bảo vệ dữ liệu private và tạo các phương thức có trạng thái riêng biệt.
Ví dụ sử dụng closure trong OOP để tạo biến private
function Counter() { let count = 0; // Biến private this.increment = function() { count++; console.log(count); }; this.getCount = function() { return count; }; } const myCounter = new Counter(); myCounter.increment(); // Output: 1 myCounter.increment(); // Output: 2 console.log(myCounter.getCount()); // Output: 2 console.log(myCounter.count); // Undefined (không thể truy cập trực tiếp)