موتور جاوااسکریپت (که در یک محیط هاستینگ مانند مرورگر یافت می‌شود) یک مفسر single threaded است که از یک heap و یک call stack منفرد تشکیل شده است. مرورگر APIهای وب مانند DOM، AJAX و Timerها را ارائه می‌دهد. هدف ما در این مقاله این است که با call stack آشنا شویم و بدانیم چرا به آن نیاز داریم. درک call stack نحوه عملکرد «سلسله مراتب توابع و ترتیب اجرا آن‌ها» در موتور جاوااسکریپت را بیان می‌کند.

call stack چیست؟

call stack در درجه اول برای فراخوانی (call) تابع استفاده می‌شود. از آنجایی که call stack منفرد است، اجرای تابع(ها) یک به یک از بالا به پایین انجام می‌گردد یعنی این که فرآیند synchronous می‌باشد. درک مفهوم call stack برای برنامه نویسی Asynchronous نیز بسیار مفید و حیاتی است.

در جاوااسکریپت Asynchronous ما یک تابع callback، یک حلقه event و یک صف task داریم. پس از اینکه تابع callback توسط حلقه event به داخل پشته push می‌شود، این تابع callback توسط call stack در طول اجرا انجام می‌شود.

در ابتدایی‌ترین سطح، call stack یک ساختار داده است که از اصل Last In First Out (LIFO) برای ذخیره و مدیریت موقت فراخوانی (call) تابع استفاده می‌کند.

بررسی مفاهیم پایه‌ای

در این بخش قصد داریم تا برخی از مفاهیم پایه‌ای پرتکرار را باهم بررسی کنیم.

LIFO: وقتی می‌گوییم که call stack بر اساس اصل ساختار داده Last In First Out عمل می‌کند به این معنی است که آخرین تابعی که به پشته push می‌شود، اولین تابعی است که هنگام بازگشت تابع از پشته خارج می‌گردد. در ادامه یک قطعه کد داریم که LIFO را با چاپ خطای stack trace در کنسول نشان می‌دهد.

function firstFunction(){
  throw new Error('Stack Trace Error');
}

function secondFunction(){
  firstFunction();
}

function thirdFunction(){
  secondFunction();
}

thirdFunction();

زمانی که کد اجرا می‌شود، با خطا مواجه می‌شویم. پشته‌ای چاپ می‌شود که نشان می‌دهد توابع چگونه روی هم قرار گرفته‌اند.

Uncaught Error: Stack Trace Error
    at firstfunction (<anonymous>:3:7>)
    at secondfunction (<anonymous>:7:1>)
    at thirdfunction (<anonymous>:11:1>)
    at <anonymous>:14:1

متوجه می‌شویم که ترتیب چیدمان توابع به صورت پشته با firstFunction()(آخرین تابعی که وارد پشته شده و برای ایجاد ارور خارج می‌شود) شروع می‌گردد به دنبال آنsecondFunction()و در نهایت thirdFunction()(اولین تابعی که هنگام اجرای کد به پشته push شده است) ادامه پیدا می‌کند.

ذخیره موقت: هنگامی که یک تابع فراخوانی (called) می‌شود تابع، پارامترها و متغیرهای آن داخل call stack پوش می‌شوند تا یک stack frame تشکیل دهند. این stack frame یک مکان حافظه در پشته است. هنگامی که تابع return شود حافظه پاک می‌گردد، زیرا از پشته خارج می‌شود.

مدیریت فراخوانی تابع (call): call stack یک رکورد از موقعیت هر stack frame را حفظ می‌کند. همچنین تابع بعدی که باید اجرا شود را می‌داند و پس از اجرا آن را حذف می‌کند. این همان چیزی است که اجرای کد در جاوااسکریپت را synchronous می‌کند. منظور ما از “مدیریت فراخوانی تابع” همین است.

call stack چگونه فراخوانی‌ توابع را مدیریت می‌کند؟

در ادامه قطعه کدی داریم که در آن تابعی یک تابع دیگر را فراخوانی می‌کند. با‌ بررسی آن به این سوال پاسخ خواهیم داد.

jafunction firstFunction(){
  console.log("Hello from firstFunction");
}

function secondFunction(){
  firstFunction();
  console.log("The end from secondFunction");
}

secondFunction();

خروجی به صورت زیر می‌باشد:

Hello from firstFunction
The end from secondFunction

زمانی که کد اجرا می‌شود اتفاقات زیر رخ می‌دهد:

  1. هنگامی که secondFunction()اجرا می‌شود، یک stack frame خالی ایجاد می‌شود. این قسمت نقطه اصلی برنامه ما به حساب می‌آید.
  2. سپس secondFunction()تابع firstFunction()را فراخوانی می‌کند که به پشته push می‌شود.
  3. firstFunction()مقدار Hello from firstFunctionرا برمی‌گرداند و در کنسول چاپ می‌کند.
  4. firstFunction()از پشته خارج می‌شود.
  5. سپس دستور اجرا به secondFunction()می‌رسد.
  6. secondFunction()مقدار The end from secondFunctionرا برمی‌گرداند و در کنسول چاپ می‌کند.
  7. secondFunction()از پشته خارج شده و حافظه را پاک می‌کند.

چه چیزی باعث ایجاد سرریز در پشته می‌شود؟

سرریز پشته زمانی اتفاق می‌افتد که یک تابع بازگشتی (تابعی که خود را فراخوانی می‌کند) بدون نقطه خروج وجود داشته باشد. مرورگر (محیط هاستینگ) دارای یک مقدار حداکثر call stack می‌باشد که می‌تواند تا قبل از ایجاد خطا آن را در خود جای دهد. به عنوان مثال:

function callMyself(){
  callMyself();
}

callMyself();

تابع callMyself()تا زمانی که مرورگر خطای Maximum call size exceededرا شناسایی کند اجرا خواهد شد. و این یعنی سرریز پشته اتفاق می‌افتد.

Uncaught RangeError: Maximum call stack size exceeded
    at callMyself(<anonymous>:2:20>)
    at callMyself(<anonymous>:3:3>)
    at callMyself(<anonymous>:3:3>)
    at callMyself(<anonymous>:3:3>)
    at callMyself(<anonymous>:3:3>)
    at callMyself(<anonymous>:3:3>)
    at callMyself(<anonymous>:3:3>)
    at callMyself(<anonymous>:3:3>)
    at callMyself(<anonymous>:3:3>)
    at callMyself(<anonymous>:3:3>)

جمع‌بندی

در این مقاله با مفهوم call stack در جاوااسکریپت آشنا شدیم. نکات کلیدی که می‌توانیم به عنوان چکیده به آن‌ها اشاره کنیم عبارتند از:

  1. single threaded است. به این معنی که در هر زمان فقط می‌تواند یک کار را انجام دهد.
  2. اجرای کد synchronous است.
  3. فراخوانی تابع یک stack frame ایجاد می‌کند که یک حافظه موقت را به خود تخصیص می‌دهد.
  4. به عنوان یک ساختار داده به صورت (LIFO) Last In First Out کار می‌کند.