جاوااسکریپت واقعا جالبه!
ولی چطور یک ماشین واقعا میتونه کدهایی که شما نوشتید رو درک کنه؟
به عنوان یک توسعه دهنده جاوااسکریپت ما معمولاً مجبور نیستیم خودمون با کامپایلرها سر و کار داشته باشیم
با این حال، مطمئناً خوبه که اصول اولیه موتور جاوا اسکریپت رو بدونیم و ببینیم که چگونه با کد جاوااسکریپتی "human-friendly" ما رفتار میکنه و اون رو به چیزی تبدیل می کنه که ماشین ها می فهمند.
نکته : این مقاله عمدتا بر اساس موتور v8است که توسط Node.js و موتور های مبتنی بر Chromium استفاده می شود.
(بیاید تصور کنیم که در سند html یک فایل جاوااسکریپت رو فراخوانی کردیم)
تجزیه کننده اچ تی ام ال (html parser) با یک تگ “script” مواجه میشه که به یک منبع (source) اشاره میکنه.کدها طبق آدرس از این منبع (که میتواند هر کدام از network یا cache و یا service worker باشد) بارگزاری میشود. در نتیجه پاسخ اسکریپت درخواستی به صورت جریان یا استریمی از بایت ها (UTF-16 bytes stream) است که رمزگشای جریان بایت(byte stream decoder) از اون نگهداری میکنه.
در این مرحله رمزگشای بایت ها یا همان byte stream decoder هنگام بارگزاری و دریافت بایت ها ، آنها را دیکود یا رمزگشایی میکند.(تصویر اول)
رمزگشای جریان بایت (stream decoder) توکن هایی رو از جریان رمزگشایی شده بایت ها ایجاد می کند.
مثلا 0066 به f و 0075 به u و 006e به n و 0063 به c و 0074 به t و 0069 به i و 006f به o و 006e به n به همراه یک فاصله (white space) تبدیل شده و کلمه کلیدی function را که یک کلمه کلیدی رزو شده در جاوااسکریپت است، از این جریان بایت، نشانه گزاری یا توکنایز کرده و در نهایت به تجزیه کننده جاوااسکریپت(parser یا pre-parser) که در ادامه شرح میدهیم، ارسال میکند.
همین اتفاق برای بقیه جریان بایت ها نیز می افتد. (تصویر دوم)
موتور جاوااسکریپت v8از دو نوع تجزیه کننده (parser) استفاده میکند: parser و pre-parser
برای کاهش زمان لازم برای بارگذاری یک وب سایت، موتور سعی می کند از تجزیه کدهایی که فوراً و درابتدا ضروری نیستند جلوگیری کند . pre-parser کدهایی را کنترل میکند که ممکن است بعداً مورد استفاده قرار گیرند، در حالی که parser کدهایی را که بلافاصله مورد نیاز است را کنترل میکند!
اگر یک تابع خاص فقط زمانی فراخوانی میشود که کاربر روی یک دکمه کلیک کند، الزام آورنیست که این کد بلافاصله پس از بارگزاری وب سایت کامپایل (compile) شود.
سرانجام اگر کاربر در نهایت روی دکمه کلیک کند و به آن قطعه کد نیاز داشته باشد، اینبار کدها به parserارسال می شود.
تجزیه کننده (parser) نودهایی (node) را بر اساس توکن هایی که از رمزگشای جریان بایت دریافت می کند ایجاد می کند و با این نودها یک ساختار انتزاعی درختی (Abstract Syntax Tree) میسازد که اختصارا به آن AST میگوییم. (تصویر سوم)
برای مشاهده نحوه تبدیل کدها به AST میتوانید به این سایت مراجعه کنید.
و بعد نوبت به مترجم یا مفسر(interpreter) میرسد. مفسر AST ها را براساس اطلاعاتی که هر کدام درخودشان دارند به بایت کد تبدیل میکند. هنگامی که هر کدام از ASTها به طور کامل به بایت کد تبدیل شد، AST نیز حذف شده و فضای حافظه رم نیز پاک سازی می شود.
در نهایت، ما چیزی داریم که برای یک ماشین قابل فهم است و می تواند با آن کار کند.(تصویر چهارم)
اگر چه بایت کد ها سریع هستند اما میتواند سریع تر هم باشد. با اجرای این بایت کد اطلاعاتی نیزایجاد می شود که از آن میتوان تشخیص داد که آیا رفتار خاصی اغلب و به تکرار اتفاق می افتد و یا چه نوع (type) داده ای غالبا مورد استفاده قرار گرفته است. شاید شما ده ها بار یک تابع را فراخوانی کرده باشید بنابراین وقت آن است که این کد را بهینه کنید تا حتی سریعتر از این اجرا شود.
بایت کد به همراه بازخورد یا فیدبک نوع داده (type) به کامپایلر بهینه گر (optimizing compiler) ارسال میشود. کامپایلر بهینه گر، بایت کد را به همراه تایپ فیدبک (بازخوردهای نوع داده) میگیرد و تبدیل به کد بسیار بهینه تر شده ماشین (optimized machine code) تبدیل میکند.
جاوا اسکریپت یک زبان با نوع داده پویا (dynamic type) است، به این معنی که انواع داده ها می توانند دائما تغییر کنند. اگر موتور جاوا اسکریپت مجبور باشد هر بار بررسی کند که یک مقدار خاص کدام نوع داده را دارد، بسیار کند خواهد بود.
به منظور کاهش زمان لازم برای تفسیر کد، ماشین کد بهینه سازی شده ، فقط مواردی را که موتور قبلاً در حین اجرای بایت کد دیده و با آن برخورد داشته را کنترل می کند. اگر به طور مکرر از یک قطعه کد خاص استفاده کنیم که نوع داده یکسانی را بارها و بارها برمی گرداند، ماشین کد بهینه شده به سادگی می تواند مجدداً برای سرعت بخشیدن به کارها مورد استفاده قرار گیرد.
با این حال، از آنجایی که جاوا اسکریپت به صورت داینامیک تایپ است، ممکن است اتفاق بیفتد که همان قطعه کد به طور ناگهانی نوع متفاوتی از داده را گرفته یا برگرداند.اگر این اتفاق بیفتد ماشین کد بهینه شده ناچارا از حالت بهینه خارج شده (اصطلاحا de-optimizeمیشود) و موتور به بایت کد تفسیر شده اولیه باز میگردد.
فرض کنید یک تابع خاص 100 بار فراخوانی شده است و تا کنون همیشه همان مقداریکسانی را برگردانده است. لذا موتور فرض میکند که این مقدار را در صد و یکمین باری که آن را فراخوانی میکنید، نیز برمیگرداند.
مثلا فرض کنید که تابع جمع زیر را داریم، که (تا کنون) همیشه با مقادیر عددی به عنوان آرگومان هر بار فراخوانی می شود:
در اینجا نتیجتا عدد 3 را برمیگردانیم، دفعه بعد که اون را فراخوانی میکنیم، فرض میکند که قرار است باز هم در نتیجه دو مقدار عددی (numerical) به عنوان ورودی تابع بگیرد.
اگر تغییری رخ ندهد لذا نیازی به مشاهده حالت های پویا (dynamically type) نیست. اما درغیر اینصورت اگر فرض نادرست بود، به جای ماشین کد بهینه شده، به بایت کد اصلی باز می گردد.
به عنوان مثال، دفعه بعد که آن را فراخوانی می کنیم، یک رشته را به جای عدد ارسال می کنیم. از آنجایی که جاوا اسکریپت به صورت پویا تایپ می شود، می توانیم این کار را بدون هیچ خطایی انجام دهیم!
این بدین معنیست که عدد 2 با یک مقدار متنی ادغام شود و در نتیجه ما در این مثال خروجی متنی "12" را خواهیم داشت. لذا موتور به اجرای بایت کد تفسیر شده برمی گردد و تایپ فیدبک را نیز به روز می کند.
امیدوارم تا اینجا مورد استفاده شما قرار گرفته باشد. البته قطعا من در اینجا توضیح بخش های زیادی از موتور رو پوشش ندادم مثل (js heap و call stack و...) که حتما اون ها رو هم در ادامه خواهیم داشت.
قطعاً به شما توصیه میکنم که اگر به قسمتهای داخلی جاوا اسکریپت هستید، تحقیقات خود را در این زمینه شروع کنید. موتور V8 یک پروژه متن باز است که مستندات عالی در اختیار شما قرار داده برای اینکه درک کنید پشت صحنه (یا به اصطلاح زیر کاپوت، under the hood) جاوا اسکریپت چه اتفاقات و رفتاری رخ می دهد.
مترجم : میلاد کاظمی
مطالب بیشتر در آکادمی ذهن افزار
منبع : مقاله خانم Lydia Hallie