1. Giới thiệu 👋
🙂 Khi bạn bắt đầu học lập trình JavaScript, một trong những khái niệm cơ bản mà bạn cần hiểu là cách JavaScript quản lý bộ nhớ. Điều này liên quan đến việc JavaScript lưu trữ và truy cập dữ liệu trong bộ nhớ máy tính. Hai khái niệm quan trọng trong việc này là Stack và Heap.
👉️ Trong bài viết này, chúng ta sẽ cùng tìm hiểu về Stack và Heap, cách chúng hoạt động trong JavaScript, và cách sử dụng chúng một cách hiệu quả. Bằng cách hiểu rõ các khái niệm này, bạn sẽ có thể viết code JavaScript tối ưu hơn, tránh các vấn đề về bộ nhớ và hiệu suất.
2. Tổng quan về Stack và Heap ✨️
Trong JavaScript, bộ nhớ được chia thành hai phần chính: Stack và Heap.
🧱 Stack
Stack là một cấu trúc dữ liệu đơn giản, hoạt động theo nguyên tắc LIFO (Last-In-First-Out). Điều này có nghĩa là phần tử được thêm vào cuối cùng sẽ là phần tử được lấy ra đầu tiên.
Stack được sử dụng để lưu trữ các biến cục bộ (local variables) và các tham số của các hàm (function parameters) . Khi bạn gọi một hàm, một khung ngăn xếp (stack frame) mới được tạo để lưu trữ các biến và tham số của hàm đó. Khi hàm kết thúc, khung ngăn xếp sẽ bị xóa và các biến trong đó sẽ không còn tồn tại nữa.
Ví dụ, hãy xem đoạn code sau:
function multiply(a, b) { return a * b;
} function square(a) { return multiply(a, a);
} let result = square(5);
console.log(result); // Output: 25
Khi đoạn code này được thực thi, Stack sẽ hoạt động như sau:
- Khi gọi hàm square(5), một khung ngăn xếp mới được tạo và các tham số a được đẩy vào Stack.
- Bên trong hàm square(), hàm multiply() được gọi với các tham số a và a. Một khung ngăn xếp mới được tạo cho hàm multiply() và các tham số được đẩy vào.
- Hàm multiply() thực hiện phép tính và trả về kết quả, sau đó khung ngăn xếp của nó bị xóa khỏi Stack.
- Hàm square() nhận được kết quả từ multiply() và trả về kết quả, sau đó khung ngăn xếp của nó cũng bị xóa khỏi Stack.
- Kết quả cuối cùng 25 được lưu vào biến result và được in ra màn hình.
Việc sử dụng Stack để lưu trữ các biến cục bộ và tham số hàm giúp JavaScript quản lý bộ nhớ một cách hiệu quả. Khi một hàm kết thúc, các biến trong khung ngăn xếp của nó sẽ tự động bị xóa, giải phóng bộ nhớ.
🧮 Heap
Trong khi Stack được sử dụng để lưu trữ các biến cục bộ và tham số hàm, Heap được sử dụng để lưu trữ các đối tượng (objects) và các giá trị phức tạp khác.
Heap là một vùng bộ nhớ không có cấu trúc cụ thể, được sử dụng để lưu trữ các giá trị động, như đối tượng, mảng, hàm và các kiểu dữ liệu phức tạp khác. Các giá trị này không được lưu trữ theo một trật tự cụ thể như trong Stack, mà được lưu trữ ở bất kỳ vị trí nào còn trống trong Heap.
Khi bạn tạo một đối tượng hoặc một giá trị phức tạp khác, nó sẽ được lưu trữ trong Heap. Các biến ở trong stack chỉ lưu trữ một tham chiếu (reference) đến vị trí của giá trị đó trong Heap.
Ví dụ, hãy xem đoạn code sau:
let person = { name: "John Doe", age: 30
}; let anotherPerson = person;
Trong ví dụ này :
- khi chúng ta tạo đối tượng person, giá trị JSON của person sẽ được lưu trữ trong Heap. Biến person (trong stack) chỉ giữ một tham chiếu đến vị trí của đối tượng JSON này trong Heap.
- Khi chúng ta gán person cho anotherPerson, anotherPerson cũng sẽ lưu cùng một tham chiếu đến đối tượng trong Heap. Cả hai biến person và anotherPerson đều trỏ đến cùng một đối tượng JSON trong Heap.
Việc sử dụng Heap để lưu trữ các đối tượng và giá trị phức tạp giúp JavaScript quản lý bộ nhớ một cách linh hoạt hơn. Các giá trị trong Heap có thể được tạo, sửa đổi và xóa mà không ảnh hưởng đến các biến khác.
🤝 Sự tương tác giữa Stack và Heap
Mặc dù Stack và Heap là hai phần riêng biệt của bộ nhớ, chúng tương tác chặt chẽ với nhau trong quá trình thực thi JavaScript.
Khi bạn gọi một hàm, một khung ngăn xếp mới được tạo trong Stack để lưu trữ các biến cục bộ và tham số của hàm đó. Nếu hàm sử dụng các đối tượng hoặc giá trị phức tạp, chúng sẽ được lưu trữ trong Heap, và các biến trong Stack sẽ giữ các tham chiếu đến chúng.
Ví dụ, hãy xem đoạn code sau:
function updatePersonAge(person, newAge) { person.age = newAge; return person;
} let john = { name: "John Doe", age: 30
}; let updatedJohn = updatePersonAge(john, 35);
console.log(updatedJohn); // Output: { name: 'John Doe', age: 35 }
console.log(john); // Output: { name: 'John Doe', age: 35 }
Trong ví dụ này :
- khi gọi hàm updatePersonAge(), một khung ngăn xếp mới được tạo trong Stack. Tham số person là một tham chiếu đến đối tượng JSON là john trong Heap. Khi chúng ta thay đổi person.age, chúng ta thực sự đang thay đổi thuộc tính age của đối tượng john trong Heap.
- Sau khi hàm updatePersonAge() trả về, khung ngăn xếp của nó bị xóa khỏi Stack. Tuy nhiên, biến john vẫn giữ tham chiếu đến cùng đối tượng JSON trong Heap, vì vậy cả john và updatedJohn đều trỏ đến cùng một đối tượng và có cùng giá trị age.
Hiểu rõ sự tương tác giữa Stack và Heap là rất quan trọng, đặc biệt khi làm việc với các đối tượng và giá trị phức tạp. Điều này giúp bạn viết code JavaScript hiệu quả hơn, tránh các vấn đề về bộ nhớ và hiệu suất.
3. Quản lý bộ nhớ với Stack và Heap 🪄
Việc quản lý bộ nhớ hiệu quả là một trong những nhiệm vụ quan trọng của ngôn ngữ lập trình. JavaScript cung cấp một số cơ chế để giúp quản lý bộ nhớ, bao gồm việc sử dụng Stack và Heap.
🎯 Quản lý bộ nhớ trong Stack
Như đã đề cập, Stack được sử dụng để lưu trữ các biến cục bộ và tham số hàm. Khi một hàm được gọi, một khung ngăn xếp mới được tạo trong Stack để lưu trữ các biến và tham số của hàm đó.
Khi hàm kết thúc, khung ngăn xếp của nó sẽ bị xóa khỏi Stack, và các biến trong đó sẽ không còn tồn tại nữa. Điều này giúp JavaScript tự động quản lý bộ nhớ cho các biến cục bộ, giải phóng bộ nhớ khi không còn sử dụng chúng.
Ví dụ:
function calculateArea(width, height) { let area = width * height; return area;
} let result = calculateArea(5, 10);
console.log(result); // Output: 50
Trong ví dụ này, khi hàm calculateArea() được gọi, các biến width, height và area được lưu trữ trong một khung ngăn xếp mới trong Stack. Khi hàm kết thúc, khung ngăn xếp này bị xóa và các biến trong đó không còn tồn tại nữa.
🎯 Quản lý bộ nhớ trong Heap
Trong khi Stack được sử dụng để quản lý bộ nhớ cho các biến cục bộ, Heap được sử dụng để quản lý bộ nhớ cho các đối tượng và giá trị phức tạp.
Khi bạn tạo một đối tượng hoặc một giá trị phức tạp khác, nó sẽ được lưu trữ trong Heap. Các biến chỉ giữ các tham chiếu đến vị trí của giá trị đó trong Heap.
Việc quản lý bộ nhớ trong Heap phức tạp hơn so với Stack, vì các giá trị trong Heap có thể được tạo, sửa đổi và xóa liên tục. JavaScript sử dụng một quá trình gọi là Garbage Collection để tự động quản lý bộ nhớ trong Heap.
Garbage Collection là một cơ chế tự động giải phóng bộ nhớ cho các đối tượng và giá trị không còn được sử dụng. Khi một giá trị không còn được tham chiếu bởi bất kỳ biến nào, Garbage Collector sẽ xác định rằng giá trị đó không còn cần thiết và tự động xóa nó khỏi Heap.
Ví dụ:
let person = { name: "John Doe", age: 30
}; person = null; // Giải phóng bộ nhớ cho đối tượng person
Trong ví dụ này, khi chúng ta gán null cho biến person, chúng ta đang loại bỏ tất cả các tham chiếu đến đối tượng JSON của person trong Heap. Khi đó, Garbage Collector sẽ tự động xóa đối tượng này khỏi Heap, giải phóng bộ nhớ.
Hiểu rõ cách quản lý bộ nhớ trong Stack và Heap là rất quan trọng để viết code JavaScript hiệu quả và tránh các vấn đề về bộ nhớ.
4. Thêm vài ví dụ về sử dụng Stack và Heap 👍️
Để hiểu rõ hơn về cách Stack và Heap hoạt động trong JavaScript, hãy xem qua một số ví dụ cụ thể.
👉️ Ví dụ 1: Gán giá trị cho biến
let x = 5;
let y = x;
y = 10;
console.log(x); // Output: 5
console.log(y); // Output: 10
Trong ví dụ này:
- Biến x được tạo trong Stack và được gán giá trị 5.
- Khi chúng ta gán x cho y, y cũng được tạo trong Stack và giữ cùng một giá trị 5 như x.
- Khi chúng ta gán 10 cho y, giá trị của y thay đổi, nhưng giá trị của x vẫn giữ nguyên 5.
Điều này là do các biến nguyên thủy (primitive types) như số nguyên, boolean, string, v.v. được lưu trữ trực tiếp trong Stack. Khi chúng ta gán giá trị cho một biến, giá trị đó được sao chép vào biến mới.
👉️ Ví dụ 2: Sử dụng đối tượng
let person = { name: "John Doe", age: 30
}; let anotherPerson = person;
anotherPerson.age = 35; console.log(person.age); // Output: 35
console.log(anotherPerson.age); // Output: 35
Trong ví dụ này:
- Khi chúng ta tạo đối tượng person, nó được lưu trữ trong Heap, và biến person giữ một tham chiếu đến vị trí của đối tượng này trong Heap.
- Khi chúng ta gán person cho anotherPerson, vậy anotherPerson cũng giữ cùng một tham chiếu đến đối tượng person trong Heap. Điều này có nghĩa là cả hai biến đều trỏ đến cùng một đối tượng.
- Khi chúng ta thay đổi thuộc tính age của anotherPerson, chúng ta thực sự đang thay đổi thuộc tính age của đối tượng mà cả hai biến đều tham chiếu đến. Vì vậy, khi chúng ta in ra giá trị của age từ cả person và anotherPerson, kết quả sẽ là 35, cho thấy cả hai biến đều phản ánh sự thay đổi của cùng một đối tượng.
5. Lời Kết 🚀
😀 Hiểu biết về Stack và Heap trong JavaScript không chỉ giúp bạn nắm bắt cách ngôn ngữ này quản lý bộ nhớ mà còn giúp bạn viết mã hiệu quả hơn. Qua các ví dụ và phân tích mà chúng ta đã thảo luận, bạn có thể thấy rằng việc phân biệt giữa các biến cục bộ và đối tượng, cũng như cách thức hoạt động của Stack và Heap là rất quan trọng trong lập trình.
Khi bạn làm việc với các giá trị phức tạp như đối tượng và mảng, việc biết được cách mà JavaScript lưu trữ và quản lý bộ nhớ có thể giúp bạn tránh được những lỗi không đáng có. Hơn nữa, hiểu rõ về Garbage Collection sẽ cho phép bạn tối ưu hóa mã của mình và giảm thiểu khả năng gặp phải các vấn đề liên quan đến bộ nhớ, như rò rỉ bộ nhớ (memory leaks).
Cuối cùng, việc nắm vững các khái niệm này không chỉ là một lợi thế trong lập trình mà còn là một bước tiến lớn trong sự nghiệp phát triển phần mềm của bạn. Hãy tiếp tục tìm hiểu và thực hành để củng cố kiến thức của mình.
Nếu bạn có bất kỳ câu hỏi nào hoặc muốn thảo luận thêm về chủ đề này, đừng ngần ngại để lại ý kiến dưới bài viết để cùng nhau trao đổi nhé 😉.