JavaScript - Call Stack

Xin chào các bạn tương lai trở thành các phù thủy JavaScript! Hôm nay, chúng ta sẽ cùng nhau lặn sâu vào một trong những khái niệm cơ bản nhất trong JavaScript: Call Stack. Đừng lo lắng nếu bạn chưa từng nghe đến nó trước đây - đến cuối bài hướng dẫn này, bạn sẽ trở thành một chuyên gia về Call Stack! Vậy, hãy lấy饮料 yêu thích của bạn, thoải mái ngồi xuống, và cùng nhau bắt đầu hành trình thú vị này.

JavaScript - Call Stack

Call Stack là gì?

Trước khi chúng ta đi vào chi tiết, hãy bắt đầu với một ví dụ đơn giản. Hãy tưởng tượng bạn đang đọc một cuốn sách phiêu lưu tự chọn. Khi bạn đọc, bạn sẽ để lại một dấu trang tại mỗi điểm quyết định. Khi bạn đến cuối một con đường, bạn sẽ quay lại dấu trang cuối cùng và thử một con đường khác. Call Stack trong JavaScript hoạt động tương tự - nó theo dõi nơi chương trình nên quay lại sau khi hoàn thành việc thực thi một hàm.

Trong thuật ngữ kỹ thuật, Call Stack là một cấu trúc dữ liệu sử dụng nguyên tắc Last In, First Out (LIFO) để lưu trữ và quản lý tạm thời các cuộc gọi hàm (call) trong JavaScript.

Call Stack trong JavaScript hoạt động như thế nào?

Bây giờ, hãy cùng xem Call Stack thực sự hoạt động như thế nào trong JavaScript. Chúng ta sẽ bắt đầu với một ví dụ đơn giản và dần tăng độ phức tạp.

Ví dụ 1: Một cuộc gọi hàm đơn giản

function greet(name) {
console.log("Hello, " + name + "!");
}

greet("Alice");

Khi mã này chạy, đây là những gì xảy ra trong Call Stack:

  1. Hàm greet được đẩy lên stack.
  2. Hàm thực thi và ghi thông báo chào mừng vào console.
  3. Hàm hoàn thành và bị loại bỏ khỏi stack.

Cực kỳ đơn giản, phải không? Bây giờ, hãy xem một ví dụ phức tạp hơn một chút.

Ví dụ 2: Các cuộc gọi hàm lồng nhau

function multiply(a, b) {
return a * b;
}

function square(n) {
return multiply(n, n);
}

function printSquare(n) {
var squared = square(n);
console.log(n + " squared is " + squared);
}

printSquare(4);

Khi chúng ta chạy printSquare(4), Call Stack hoạt động như sau:

  1. printSquare(4) được đẩy lên stack.
  2. Trong printSquare, square(4) được gọi và đẩy lên stack.
  3. Trong square, multiply(4, 4) được gọi và đẩy lên stack.
  4. multiply hoàn thành và bị loại bỏ khỏi stack.
  5. square hoàn thành và bị loại bỏ khỏi stack.
  6. printSquare ghi kết quả và hoàn thành, sau đó bị loại bỏ khỏi stack.

Bạn có thấy stack tăng và giảm khi các hàm được gọi và hoàn thành không? Nó giống như một tháp Legos được xây dựng và tháo dỡ!

Ví dụ 3: Các hàm đệ quy

Các hàm đệ quy là một cách tuyệt vời để minh họa cách Call Stack có thể tăng lên. Hãy xem một ví dụ kinh điển: tính giai thừa.

function factorial(n) {
if (n === 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}

console.log(factorial(5));

Khi chúng ta gọi factorial(5), Call Stack sẽ trông như sau:

  1. factorial(5) được đẩy lên.
  2. factorial(4) được đẩy lên.
  3. factorial(3) được đẩy lên.
  4. factorial(2) được đẩy lên.
  5. factorial(1) được đẩy lên.
  6. factorial(1) trả về 1 và bị loại bỏ.
  7. factorial(2) tính 2 * 1, trả về 2 và bị loại bỏ.
  8. factorial(3) tính 3 * 2, trả về 6 và bị loại bỏ.
  9. factorial(4) tính 4 * 6, trả về 24 và bị loại bỏ.
  10. factorial(5) tính 5 * 24, trả về 120 và bị loại bỏ.

Woo! Đó là rất nhiều lần đẩy và loại bỏ, phải không? Nhưng đó chính xác là cách JavaScript theo dõi tất cả các cuộc gọi hàm lồng nhau.

Tràn stack

Bây giờ chúng ta đã hiểu cách Call Stack hoạt động, hãy nói về điều gì sẽ xảy ra khi mọi thứ出错. Bạn có từng nghe đến thuật ngữ "stack overflow"? Nó không chỉ là một trang web cho các lập trình viên tuyệt vọng (mặc dù nó cũng là như vậy) - nó là một lỗi thực tế có thể xảy ra trong mã của bạn.

Một stack overflow xảy ra khi có quá nhiều cuộc gọi hàm và Call Stack vượt quá giới hạn kích thước của nó. Nguyên nhân phổ biến nhất? Vô hạn đệ quy!

Ví dụ 4: Stack Overflow

function causeStackOverflow() {
causeStackOverflow();
}

causeStackOverflow();

Nếu bạn chạy mã này, bạn sẽ nhận được thông báo lỗi như "Maximum call stack size exceeded". Nó giống như cố gắng xây một tháp Legos lên đến mặt trăng - cuối cùng, bạn sẽ cạn kiệt blocs (hoặc trong trường hợp này, bộ nhớ)!

Để tránh stack overflows, hãy luôn đảm bảo rằng các hàm đệ quy của bạn có một trường hợp cơ bản để kết thúc đệ quy.

Các phương thức Call Stack

JavaScript không cung cấp các phương thức trực tiếp để manipulates Call Stack, nhưng có một số hàm liên quan có thể hữu ích cho việc gỡ lỗi và hiểu Call Stack:

Phương thức Mô tả
console.trace() Xuất một vết stack ra console
Error.stack Một thuộc tính không chuẩn xác trả về một vết stack

Dưới đây là một ví dụ nhanh về cách sử dụng console.trace():

function func1() {
func2();
}

function func2() {
func3();
}

function func3() {
console.trace();
}

func1();

Điều này sẽ xuất một vết stack hiển thị chuỗi cuộc gọi: func3 -> func2 -> func1.

Kết luận

Và thế là bạn đã có nó, các bạn! Chúng ta đã cùng nhau hành trình qua thế giới kỳ diệu của Call Stack trong JavaScript. Từ các cuộc gọi hàm đơn giản đến các đệ quy phức tạp, bạn bây giờ đã hiểu cách JavaScript theo dõi vị trí của nó trong mã của bạn.

Nhớ rằng, Call Stack giống như một trợ lý hữu ích, luôn giữ cho bạn không bị lạc trong cuốn sách cốt truyện JavaScript. Nhưng như bất kỳ trợ lý nào, nó cũng có giới hạn - vì vậy hãy đối xử tốt với nó và tránh những stack overflows khó chịu!

Khi bạn tiếp tục hành trình JavaScript của mình, hãy luôn nhớ đến Call Stack. Hiểu nó sẽ không chỉ giúp bạn viết mã tốt hơn mà còn làm cho việc gỡ lỗi dễ dàng hơn rất nhiều. Chúc bạn may mắn và stack của bạn luôn cân bằng!

Credits: Image by storyset