Phạm vi (scope) và đóng gói (closure) là hai nền tảng kiến trúc quan trọng nhất trong cách JavaScript quản lý bộ nhớ và giải quyết biến. Hiểu rõ cách trình thông dịch xử lý hai khái niệm này sẽ giúp lập trình viên kiểm soát được vòng đời của dữ liệu, tránh xung đột biến và tối ưu hóa hiệu năng ứng dụng.
1. Phân Cấp Phạm Vi Biến
Trong JavaScript, phạm vi quy định khu vực mà một biến có thể được truy cập hoặc sửa đổi. Ngôn ngữ này hỗ trợ ba loại phạm vi chính: toàn cục, hàm và khối.
Biến khai báo ngoài bất kỳ hàm nào thuộc về phạm vi toàn cục. Chúng tồn tại xuyên suốt thời gian chạy của ứng dụng và có thể được gọi từ bất kỳ vị trí nào. Ngược lại, biến bên trong hàm chỉ tồn tại trong ngữ cảnh đó. Từ ES6 trở đi, việc sử dụng let và const giúp giới hạn biến chặt chẽ hơn trong các khối mã.
function initializeModule() {
let internalConfig = { debug: false };
console.log(internalConfig); // Truy cập thành công bên trong hàm
}
initializeModule();
// console.log(internalConfig); // Lỗi ReferenceError: biến không tồn tại ở ngữ cảnh hiện tại
2. Chuỗi Phạm Vi (Scope Chain)
Khi trình thông dịch gặp một biến, nó sẽ thực hiện quy trình tìm kiếm tuần tự từ ngữ cảnh hiện tại, di chuyển dần lên các cấp cha cho đến khi tìm thấy hoặc chạm đến phạm vi toàn cục. Cơ chế này được gọi là chuỗi phạm vi. Mỗi lần một hàm được thực thi, một đối tượng ngữ cảnh mới được tạo ra và được liên kết với ngữ cảnh cha thông qua một con trỏ nội bộ. Nếu biến không tồn tại ở bất kỳ cấp nào, hệ thống sẽ ném lỗi ReferenceError.
3. Bản Chất Của Đóng Gói (Closure)
Đóng gói xảy ra khi một hàm con giữ lại tham chiếu đến các biến của hàm bao ngoài, ngay cả sau khi hàm ngoài đã kết thúc thực thi. Thay vì bị thu gom rác (garbage collection), các biến đó vẫn nằm trong bộ nhớ nhờ được đóng gói bởi hàm con.
Cơ chế này cho phép tạo ra các trạng thái riêng tư và duy trì dữ liệu qua nhiều lần gọi hàm mà không làm ô nhiễm môi trường toàn cục.
function createSessionManager() {
let activeTokens = [];
return {
addToken: function(id) {
activeTokens.push(id);
return activeTokens.length;
},
clearSession: function() {
activeTokens = [];
return 'Đã xóa hết token';
}
};
}
const session = createSessionManager();
console.log(session.addToken('user_1')); // 1
console.log(session.addToken('user_2')); // 2
console.log(session.clearSession()); // Đã xóa hết token
4. Mô Hình Áp Dụng Trong Phát Triển Thực Tế
4.1 Tạo Bộ Lọc Thực Thi Trễ (Debounce Pattern)
Đóng gói thường được dùng để xây dựng các hàm kiểm soát tần suất gọi, đặc biệt hữu ích với sự kiện cuộn trang hoặc gõ phím.
function setupDebouncer(callback, delayMs) {
let executionTimer = null;
return function(...args) {
if (executionTimer) clearTimeout(executionTimer);
executionTimer = setTimeout(() => {
callback.apply(this, args);
executionTimer = null;
}, delayMs);
};
}
const handleSearchInput = setupDebouncer((query) => {
console.log('Gửi yêu cầu tìm kiếm:', query);
}, 300);
handleSearchInput('java'); // Bị hoãn lại
handleSearchInput('javas'); // Ghi đè thời gian chờ trước
handleSearchInput('javascr'); // Chỉ thực thi sau 300ms nếu không có input mới
4.2 Quản Lý Sự Kiện Trong Vòng Lặp
Một lỗi phổ biến xảy ra khi đăng ký sự kiện cho nhiều phần tử trong vòng lặp mà không sử dụng cơ chế đóng gói đúng cách. Mỗi hàm xử lý sự kiện cần giữ lại giá trị riêng của biến lặp tại thời điểm đăng ký.
function attachGridListeners(elements) {
elements.forEach((item, index) => {
const cellData = `Ô số ${index + 1}`;
item.addEventListener('click', () => {
console.log(`Người dùng nhấn vào ${cellData}`);
});
});
}
// Giả sử đã lấy danh sách nút từ DOM
// attachGridListeners(document.querySelectorAll('.grid-item'));
4.3 Bao Wrapping Dữ Liệu Bất Đồng Bộ
Khi làm việc với API, đóng gói giúp duy trì bộ nhớ đệm (cache) và trạng thái tải dữ liệu mà không cần dùng biến toàn cục.
function createDataCache(endpoint) {
const memoryStore = new Map();
let isLoading = false;
return async function fetchData(query) {
if (memoryStore.has(query)) {
console.log('Trả về dữ liệu từ bộ đệm');
return memoryStore.get(query);
}
if (isLoading) return null;
isLoading = true;
try {
const response = await fetch(`${endpoint}?q=${query}`);
const result = await response.json();
memoryStore.set(query, result);
return result;
} catch (err) {
console.error('Lỗi kết nối:', err.message);
throw err;
} finally {
isLoading = false;
}
};
}
const apiClient = createDataCache('https://api.test.io/search');
// apiClient('react').then(data => console.log(data));
Việc nắm vững cách trình thông dịch quản lý chuỗi phạm vi và cơ chế giữ bộ nhớ của đóng gói sẽ giúp xây dựng các module độc lập, an toàn về dữ liệu và dễ dàng mở rộng trong các dự án JavaScript quy mô lớn.