Advance Javascript - Call Stack & Memory Heap
Là một lập trình viên, việc khai báo các biến, khởi tạo chúng và gán chúng cho các giá trị mới là điều mà hầu hết chúng ta sẽ làm hàng ngày.
Nhưng những gì thực sự xảy ra khi chúng ta thực hiện điều này ? Làm thến nào để qua mỗi lần chạy dữ liệu về những lần chạy trong quá khứ được phân tích, hiểu được những điều này giúp chúng ta làm việc với Javascrpit một cách tự tin hơn rất nhiều.
Trong khi chúng ta thường nói myNumer = 23 nhưng về mặt bản chất myNumber trỏ tới (hay có thể nói là giá trị của myNumber) địa chỉ (0012CCGWH80) nắm giữ giá trị (23) đây là một điều quan trọng bạn cần phải nhớ.
Bây giờ thực hiện gán một biến bằng với myNumer : let newVar = myNumber , lúc này về mặt bản chất biến newVar cũng có giá trị chính là địa chỉ bộ nhớ (0012CGWH80) nắm giữ giá trị 23.
Điều này xảy ra là do myNumber có giá trị là một địa chỉ bộ nhớ nên gán 2 biến này bằng nhau dẫn đến newVar có giá trị tương đương.
Bây giờ nếu tôi làm thế này : myNumber = myNumber + 1
Bây giờ myNumber = 24 điều này là rõ ràng nhưng newVar thì sao ? biến newVar = 24 bởi vì 2 biến này cùng trỏ đến một vùng nhớ ?
=> Câu trả lời là không. Do kiểu dữ liệu nguyên thủy không bao giờ thay đổi nên biến myNumber sẽ trỏ đến một vùng nhớ khác => Biến newVar không thay đổi giá trị.
Đây là một ví dụ khác :
Ban đầu mình luôn suy nghĩ một cách đơn giản khi thực hiện nối chuỗi như thế này thì đơn giản chỉ việc thêm chữ d để gắn vào chuỗi 'abc' vào vùng nhớ mà biến abc trỏ => Đơn giản là thay đổi giá trị mà vùng nhớ đó nắm giữ.
=> Nhưng rõ ràng về mặt kĩ thuật điều này sai hoàn toàn String trong JS là kiểu dữ liệu nguyên thủy nên khi thực hiện thay đổi thì biến sẽ được cấp phát một địa chỉ mới và giá trị 'abcd' sẽ được lưu trữ ở địa chỉ này.
Nhưng những gì thực sự xảy ra khi chúng ta thực hiện điều này ? Làm thến nào để qua mỗi lần chạy dữ liệu về những lần chạy trong quá khứ được phân tích, hiểu được những điều này giúp chúng ta làm việc với Javascrpit một cách tự tin hơn rất nhiều.
Điều gì thực sự xảy ra khi khai báo một biến
Bắt đầu với một ví dụ đơn giản, chúng ta khởi tạo một biến let myNumber = 23
Khi đoạn code này được thực thi, V8 sẽ
+ Tạo một định danh (Identifier) cho biến (myNumber)
+ Phân bổ một địa chỉ trong bộ nhớ để lưu trữ giá tị của biến (điều này diễn ra trong thời gian thực thi , để hiểu vì sao điều này diễn ra trong thời gian chạy cần hiểu được các giai đoạn khi một hàm được invoked).
+ Lưu trữ một giá trị (ở đây là 23) tại địa chỉ được cấp phát.
Bây giờ thực hiện gán một biến bằng với myNumer : let newVar = myNumber , lúc này về mặt bản chất biến newVar cũng có giá trị chính là địa chỉ bộ nhớ (0012CGWH80) nắm giữ giá trị 23.
Điều này xảy ra là do myNumber có giá trị là một địa chỉ bộ nhớ nên gán 2 biến này bằng nhau dẫn đến newVar có giá trị tương đương.
Bây giờ nếu tôi làm thế này : myNumber = myNumber + 1
Bây giờ myNumber = 24 điều này là rõ ràng nhưng newVar thì sao ? biến newVar = 24 bởi vì 2 biến này cùng trỏ đến một vùng nhớ ?
=> Câu trả lời là không. Do kiểu dữ liệu nguyên thủy không bao giờ thay đổi nên biến myNumber sẽ trỏ đến một vùng nhớ khác => Biến newVar không thay đổi giá trị.
Đây là một ví dụ khác :
Ban đầu mình luôn suy nghĩ một cách đơn giản khi thực hiện nối chuỗi như thế này thì đơn giản chỉ việc thêm chữ d để gắn vào chuỗi 'abc' vào vùng nhớ mà biến abc trỏ => Đơn giản là thay đổi giá trị mà vùng nhớ đó nắm giữ.
=> Nhưng rõ ràng về mặt kĩ thuật điều này sai hoàn toàn String trong JS là kiểu dữ liệu nguyên thủy nên khi thực hiện thay đổi thì biến sẽ được cấp phát một địa chỉ mới và giá trị 'abcd' sẽ được lưu trữ ở địa chỉ này.
Kết luận: Primitive are imutable ( Giá trị nguyên thủy là bất biến) điều này có nghĩa rằng thay vì thay đổi giá trị ban đầu, Javascript sẽ tạo ra một giá trị mới ở một vùng nhớ mới.
Chúng ta sẽ làm quen với mô hình bộ nhớ trong JS có thể được hiểu là có hai khu vực riêng biệt : stack và heap.
=> Stack (static memory allocation) :
+ Nói một cách hàn lâm thì là một cấu trúc dữ liệu dùng để lưu trữ dữ liệu tĩnh (static data), tức là các giá trị mà V8 biết trước tại thời điểm biên dịch, vì V8 nó biết trước kích thước nên nó có thể cấp phát một vùng nhớ cố định cho biến đó.
Tìm hiểu về nơi lưu trữ giá trị kiểu nguyên thủy
Mô hình bộ nhớ của Javascript : CallStack và Memory HeapChúng ta sẽ làm quen với mô hình bộ nhớ trong JS có thể được hiểu là có hai khu vực riêng biệt : stack và heap.
=> Stack (static memory allocation) :
+ Nói một cách hàn lâm thì là một cấu trúc dữ liệu dùng để lưu trữ dữ liệu tĩnh (static data), tức là các giá trị mà V8 biết trước tại thời điểm biên dịch, vì V8 nó biết trước kích thước nên nó có thể cấp phát một vùng nhớ cố định cho biến đó.
+ Quá trình cấp phát vùng nhớ trước khi thực thi gọi là cấp phát bộ nhớ tĩnh (allocate static memory)
=> Heap (dynamic memory allocation)
+ Heap có vai trò cũng giống như stack cũng là để lưu trữ dữ liệu nhưng có một điểm khác biệt, heap chỉ lưu trữ giá trị có kiểu object hoặc function.
+ Không như stack được phân bố cố định một lượng ô nhớ cố định thì heap vùng nhớ trên heap sẽ được cấp phát không cố định còn được gọi là dynamic memory allocation.
Trong ảnh trên đã trừu tượng hóa giá trị các biến bởi vì về bản chất giá trị biến là địa chỉ nắm giữ giá trị chúng ta (lập trình viên) muốn gán cho biến => Đây cũng là chìa khóa quan trọng để phân biệt let vs const.
CallStack là nơi lưu trữ các biến có kiểu nguyên thủy và có phạm vi toàn cục (không nằm trong hàm nào => toàn cục).
Memory Heap là nơi lưu trữ các biến có kiểu không phải nguyên thủy, sự khác biệt là Heap có thể lưu trữ không có thứ tự có thể phát triển linh hoạt với array, object, function... (các kiểu dữ liệu không phải nguyên thủy).
Những gì khi khai báo biến kiểu không nguyên thủy trong JS.
Những gì thực sự xảy ra khác với khi bạn khai báo biến kiểu nguyên thủy.
Giả sử khai báo : let myArray = []
Đây là những gì thực sự diễn ra
1 : Tạo một định danh cho biến (myArray)
2 : Phân bổ một địa chỉ trong bộ nhớ để nắm giữ giá trị chúng ta muốn gán cho biến (sẽ được chỉ định trong thời gian chạy)
3 : Lưu trữ một giá trị của một địa chỉ bộ nhớ được cấp phát trên Heap (điều này diễn ra trong thời gian chạy) , trong bước này giá trị của địa chỉ bộ nhớ cấp phát có kiểu string và được gán cho biến(về mặt kĩ thuật) như vậy biến myArray và giá trị của nó sẽ được lưu trên CallStack vì biến này kiểu string (nguyên thủy)
4 : Địa chỉ bộ nhớ trên heap lưu trữ giá trị chúng ta muốn gán cho biến. (một mảng trống [])
Bức ảnh này sẽ trực quan hơn
Nói đơn giản biến myString nắm giữ địa chỉ vùng nhớ A mà giá trị của nó là địa chỉ vùng nhớ B nắm giữ giá trị C mà chúng ta muốn gán cho biến, vấn đề là B có kiểu string nên A sẽ được lưu trữ trên callStach .
Và từ đây chúng ta có thể làm bất cứ điều gì chúng ta muốn.
Ở đoạn code trên mình đã đúng khi sử dụng let với biến sum , sử dụng let với biến sum rõ ràng phù hợp với vì biến sum thực sự đã "thay đổi" , hiểu rõ ràng ở đây là thay đổi địa chỉ mà nó lưu trữ.
Nhưng với numbers mình sử dụng let là không phù hợp bởi vì kể cả push bao nhiêu lần địa chỉ mà numbers nắm giữ không bao giờ thay đổi, nhưng thay đổi về mảng được thực hiện trên Heap nó không dẫn đến sự thay đổi về địa chỉ mà biến numbers nắm giữ.
=> Một cách chính xác để giải thích cho sự "thay đổi" đó là thay đổi về địa chỉ bộ nhớ mà biến nắm giữ. Let cho phép thay đổi địa chỉ mà biến nắm giữ nhưng const thì không.
Chúng ta đã được học rằng JavaScriptEngine đã thực hiện rất nhiều công việc nhưng quan trọng nhất rằng nó đã đọc code và thực thi code cho chúng ta.
Hãy đi sâu vào những điều cần chú ý trong 2 bước này
- Nơi chúng ta dùng để lưu trũ thông tin, lưu trữ các biến của chúng ta, dữ liệu về ứng dụng
- Một nơi thực sự chạy các dòng lệnh, theo dõi những gì đang xảy ra, từng dòng một
Đó chính là CallStack và MemoryHeap
- Chúng ta cần Memory Heap chính là nơi đọc và viết thông tin vì về mặt bản chất hoạt động của chương trình cũng chỉ dựa trên đọc và ghi mà thôi, bằng cách đó chúng ta có một nơi để phân bổ bộ nhớ sử dụng bộ nhớ và giải phóng bộ nhớ .
- Với Call Stack chúng ta có thể chạy mã theo thứ tự, theo dõi những gì đang chạy, kiểm soát luồng của chương trình.
Memory Heap
- Nơi cấp phát bộ nhớ cho hoạt động thực thi của chương trình, về bản chất cho là một vùng nhớ rộng lớn mà JSE cung cấp cho chúng ta có thể sử dụng để lưu trữ bất kỳ loại dữ liệu tùy ý không cần có thứ tự, cho phép chúng ta sử dụng các biến để trỏ đến các khu vực lưu trữ dữ liệu.
Call Stack
- Call Stack lưu trữ biến và hàm trong quá trình thực thi(chỉ biến và hàm mà nó đang thực thi thôi) ở StackFrame, mỗi khi hàm được invoked thì nó được đẩy lên đỉnh của Call Stack , mỗi lần một hàm được invoked thì trong callStack nó tạo ra một stackFrame đẩy lên đỉnh của CallStack
Như các bạn nhìn trên hình, biểu thị mỗi gạch màu đỏ là một stackFrame cho phép chúng ta biến chúng ta đang ở đâu trong code và chúng ta sử dụng MemoryHeap để trỏ (tham chiến) đến các biến và object mà chúng ta lưu trữ ở bộ nhớ.
Trong ảnh trên đã trừu tượng hóa giá trị các biến bởi vì về bản chất giá trị biến là địa chỉ nắm giữ giá trị chúng ta (lập trình viên) muốn gán cho biến => Đây cũng là chìa khóa quan trọng để phân biệt let vs const.
CallStack là nơi lưu trữ các biến có kiểu nguyên thủy và có phạm vi toàn cục (không nằm trong hàm nào => toàn cục).
Memory Heap là nơi lưu trữ các biến có kiểu không phải nguyên thủy, sự khác biệt là Heap có thể lưu trữ không có thứ tự có thể phát triển linh hoạt với array, object, function... (các kiểu dữ liệu không phải nguyên thủy).
Những gì khi khai báo biến kiểu không nguyên thủy trong JS.
Những gì thực sự xảy ra khác với khi bạn khai báo biến kiểu nguyên thủy.
Giả sử khai báo : let myArray = []
Đây là những gì thực sự diễn ra
1 : Tạo một định danh cho biến (myArray)
2 : Phân bổ một địa chỉ trong bộ nhớ để nắm giữ giá trị chúng ta muốn gán cho biến (sẽ được chỉ định trong thời gian chạy)
3 : Lưu trữ một giá trị của một địa chỉ bộ nhớ được cấp phát trên Heap (điều này diễn ra trong thời gian chạy) , trong bước này giá trị của địa chỉ bộ nhớ cấp phát có kiểu string và được gán cho biến(về mặt kĩ thuật) như vậy biến myArray và giá trị của nó sẽ được lưu trên CallStack vì biến này kiểu string (nguyên thủy)
4 : Địa chỉ bộ nhớ trên heap lưu trữ giá trị chúng ta muốn gán cho biến. (một mảng trống [])
Bức ảnh này sẽ trực quan hơn
Nói đơn giản biến myString nắm giữ địa chỉ vùng nhớ A mà giá trị của nó là địa chỉ vùng nhớ B nắm giữ giá trị C mà chúng ta muốn gán cho biến, vấn đề là B có kiểu string nên A sẽ được lưu trữ trên callStach .
Và từ đây chúng ta có thể làm bất cứ điều gì chúng ta muốn.
Let vs Const
Nhìn chung chúng ta nên sử dụng const càng nhiều càng tốt, chúng ta chỉ sử dụng let khi biết chắc chắn biến đó sẽ thay đổi.
Hãy thực sự rõ ràng về những gì chúng ta gọi là "thay đổi" , một sai lần để giải thích "thay đổi" hiểu là thay đổi giá trị (giá trị mà chúng ta muốn gán khác với giá trị mà thực sự biến đó được gán cho).
Nhưng với numbers mình sử dụng let là không phù hợp bởi vì kể cả push bao nhiêu lần địa chỉ mà numbers nắm giữ không bao giờ thay đổi, nhưng thay đổi về mảng được thực hiện trên Heap nó không dẫn đến sự thay đổi về địa chỉ mà biến numbers nắm giữ.
=> Một cách chính xác để giải thích cho sự "thay đổi" đó là thay đổi về địa chỉ bộ nhớ mà biến nắm giữ. Let cho phép thay đổi địa chỉ mà biến nắm giữ nhưng const thì không.
Chúng ta đã được học rằng JavaScriptEngine đã thực hiện rất nhiều công việc nhưng quan trọng nhất rằng nó đã đọc code và thực thi code cho chúng ta.
Hãy đi sâu vào những điều cần chú ý trong 2 bước này
- Nơi chúng ta dùng để lưu trũ thông tin, lưu trữ các biến của chúng ta, dữ liệu về ứng dụng
- Một nơi thực sự chạy các dòng lệnh, theo dõi những gì đang xảy ra, từng dòng một
Đó chính là CallStack và MemoryHeap
- Chúng ta cần Memory Heap chính là nơi đọc và viết thông tin vì về mặt bản chất hoạt động của chương trình cũng chỉ dựa trên đọc và ghi mà thôi, bằng cách đó chúng ta có một nơi để phân bổ bộ nhớ sử dụng bộ nhớ và giải phóng bộ nhớ .
- Với Call Stack chúng ta có thể chạy mã theo thứ tự, theo dõi những gì đang chạy, kiểm soát luồng của chương trình.
Memory Heap
- Nơi cấp phát bộ nhớ cho hoạt động thực thi của chương trình, về bản chất cho là một vùng nhớ rộng lớn mà JSE cung cấp cho chúng ta có thể sử dụng để lưu trữ bất kỳ loại dữ liệu tùy ý không cần có thứ tự, cho phép chúng ta sử dụng các biến để trỏ đến các khu vực lưu trữ dữ liệu.
Call Stack
- Call Stack lưu trữ biến và hàm trong quá trình thực thi(chỉ biến và hàm mà nó đang thực thi thôi) ở StackFrame, mỗi khi hàm được invoked thì nó được đẩy lên đỉnh của Call Stack , mỗi lần một hàm được invoked thì trong callStack nó tạo ra một stackFrame đẩy lên đỉnh của CallStack
Nhận xét
Đăng nhận xét