تابلوی نرخ یک صرافی در تهران باید طلا و سکه و دلار را همان لحظه که میرسند نشان بدهد. چهار سطر، و ترتیبی که چاپ میشوند ترتیبی نیست که نوشته شدهاند:
console.log("1 gheymat khande shod")setTimeout(() => console.log("4 timer, dor-e badi"), 0)queueMicrotask(() => console.log("3 promise, akhar-e hamin dor"))console.log("2 jadval rasm shod")
خروجی ۱ و ۲ و ۳ و ۴ است. setTimeout با صفر بعد از هر سطر همگام اجرا میشود، نه همان لحظه، و یک وعده حلشده در پایان همین دور اجرا میشود، پیش از هر تایمری. صفر یعنی «به محض اینکه کار دیگری نمانده» و یعنی «الان» نیست. هر چیز غافلگیرکنندهای که پایینتر از نحو در این زبان هست از همین یک چیدمان بیرون میآید، و کالبک، پرامیس و await است.
نیمه عملیاش قیمت اشتباه گرفتنش است. سه نرخ که بهترتیب ۱۸۰ و ۲۴۰ و ۴۲۰ میلیثانیه طول میکشند: سه await پشت سر هم ۸۴۴ میلیثانیه گرفت و Promise.all عدد ۴۲۱ میلیثانیه، اندازهگیریشده روی Node 25 در سوم سپتامبر ۲۰۲۶. await Promise.all([a, b, c]) هر سه را شروع میکند و فقط منتظر کندترین میماند، و نسخه پشت سر هم کار بیشتری نمیکرد، سه بار منتظر چیزهایی میماند که میتوانستند با هم منتظر بمانند. دلیل اینکه اصلاً میتواند این کار را بکند همان چیدمان است: await رشته اجرا را قفل نمیکند، کنترل را به حلقه پس میدهد، و حلقه آزاد است درخواست دوم را وقتی اولی هنوز در هواست شروع کند.
عددها و تاریخها، که در فارسی نصف کار همینجاست
تابلو باید نرخ را فارسی نشان بدهد و اینجا زبان خودش جواب را دارد. (1234567.5).toLocaleString("fa-IR") رشته «۱٬۲۳۴٬۵۶۷٫۵» را میدهد، با ارقام فارسی و جداکننده و ممیز درست، بدون هیچ کتابخانهای. تاریخ از این هم بهتر است: Intl.DateTimeFormat با fa-IR تقویم شمسی را پیشفرض میگیرد، پس همان تاریخ بهشکل «۱۲ شهریور ۱۴۰۵» درمیآید و برای میلادی باید صریح fa-IR-u-ca-gregory خواست. کسی که این را نداند، معمولاً یک کتابخانه تبدیل تاریخ به پروژه اضافه میکند که کارش را مرورگر از قبل بلد بوده.
و اینجا همان تلهای است که یک روز از تیم میگیرد: Number روی رشتهای با ارقام فارسی مقدار NaN میدهد. رشتهای که خودتان با toLocaleString ساختهاید، از همان راه برنمیگردد. اگر نرخ ویرایششده در یک فیلد ورودی دوباره خوانده شود، جمع تابلو NaN میشود و هیچ خطایی هم بالا نمیآید. قاعدهاش ساده است: عدد را عدد نگه دارید و فارسیسازی را فقط در آخرین لحظه، سر چاپ کردن، انجام بدهید.
بخشهایی که یک بار سر همه را میبرند
نرخها که مرتب میشوند اشتباه برمیگردند. [10, 9, 1].sort() جواب [1, 10, 9] میدهد چون مرتبسازی پیشفرض مقایسه را رشتهای انجام میدهد، و بهشکل رشته «10» پیش از «9» میآید. راهش این است که ترتیب را بگویید: [10, 9, 1].sort((a, b) => a - b) جواب [1, 9, 10] میدهد. هیچ چیز این ماجرا اشکال نیست و از اول همین رفتار تعریفشده بوده، که شکل کلی غافلگیریهای این زبان است.
دومی مقایسه است. === بدون تبدیل مقایسه میکند و == اول تبدیل میکند، پس "2" == 2 درست است. قاعدهای که دوام میآورد این است که همهجا === بنویسید و دیگر به آن فکر نکنید، که هر بازرسی نوشتهشده پس از سال ۲۰۱۰ رویش پافشاری میکند. اینها و بقیهشان نوعها، توابع، و بخشهایی که آدمها را غافلگیر میکنند هستند.
دو جای اجرا، و اشتراکی کوچکتر از آنچه انتظار میرود
یک فایل در هر دو جا اجرا نمیشود. document در Node وجود ندارد و process در مرورگر وجود ندارد، پس ماژولی که به هرکدام دست بزند به یکی از آن دو گره خورده است. آنچه مشترک است از سال ۲۰۱۵ به این طرف بزرگتر شده: fetch و URL و setTimeout و Promise در هر دو هستند، یعنی بیشتر منطق واقعاً جابهجا میشود، و بیشتر آنچه جابهجا نمیشود همان بخشی است که با بیرون حرف میزند. Intl هم در هر دو هست، که برای یک تیم ایرانی یعنی قالببندی فارسی روی سرور و در مرورگر یک جواب میدهد.
داخل تب، آن بیرون خودِ صفحه است. document.querySelector اولین تطبیق را برمیگرداند و querySelectorAll همه را، و هر کاری که تابلو با صفحه میکند از همین دو و همسایههایشان رد میشود، که خواندن و عوض کردن صفحه از داخل اسکریپت است. تقسیم کد میان فایلها و برگرداندنش به یک چیزی که مرورگر بار میکند تکه کردن کد، و دوباره کنار هم گذاشتنش است.
جای اجرا
چه چیزی میان یک تب مرورگر و یک سرور جابهجا میشود و چه چیزی نه
بخش مشترک از سال ۲۰۱۵ بزرگتر شده و هنوز آن بخشی نیست که با چیزی حرف میزند.
- داخل تب مرورگر
- داخل Node
- داخل تب مرورگرdocument و window و alert، و آدمی که دارد تماشا میکند
- داخل Nodeprocess و Buffer و require، و یک سیستم فایل
روی Node 25 بررسی شد، با پرسیدن هر نام و دیدن اینکه هست یا نه. اشتراک هر سال بزرگتر میشود و دو فهرست بیرونی تقریباً تکان نخوردهاند.
ابزار، و سالی که این حرف درست است
TypeScript همان است که روز کاری را عوض میکند. رایگان و متنباز است، از حدود سال ۲۰۱۹ پیشفرض هر پروژه تازه با اندازه جدی بوده، و جای نوشتن نوعها داخل یک توضیح و امیدوار بودن را گرفت. همان مرتبسازی بالا را میگرفت، و اشتباه بخش بعد را صریح میگیرد، چون یک وعده و یک عدد دو نوع متفاوتاند و پیش از بار شدن صفحه این را میگوید. اینکه چیست و چه چیزی را نمیگیرد نوعها روی جاوااسکریپت، و آنچه میگیرند است.
Node زمان اجرای بیرون از مرورگر است، رایگان، و جای این را گرفت که برای اجرای هر جاوااسکریپتی به یک مرورگر نیاز باشد؛ به همین دلیل هم ابزارهای کد مرورگر خودشان با جاوااسکریپت نوشته شدهاند. Prettier قالببندی میکند و جای بحث سر قالببندی در بازبینی را گرفت. از ایران، نصب خود اینها معمولاً از مخزن npm جواب میدهد و چیزی که گیر میکند بستههایی است که موقع نصب یک فایل باینری از جای دیگری دانلود میکنند؛ آن یکی را باید تکتک حل کرد و راه عمومی ندارد.
اشتباه: مقداری که یک وعده است و لباس مقدار پوشیده
این همان است که آدمها را متوقف میکند، و متوقفشان میکند چون هیچ چیز نمیشکند. تابعی که نرخ را میگیرد صدا زده میشود و await جا میافتد. تابلو Promise { 2.42 } چاپ میکند، که شبیه خطاست و نیست، و حساب کردن رویش [object Promise]1 میدهد بهجای ۳٫۴۲. نه استثنایی، نه ردپای خطایی، نه شماره سطری.
قاعدهای که تمامش میکند: هر تابعی که async جلویش باشد یک وعده برمیگرداند، همیشه، حتی وقتی بدنهاش یک عدد برمیگرداند. صدا زدنش وعده را به شما میدهد و await کردنش عدد را. وقتی مقداری بهشکل Promise چاپ شد، کلمه گمشده await است، و روی سطری گم شده که مقدار را ساخته نه سطری که چاپش کرده.
چیزی که این صفحه به آن دست نزد نیمهای از زبان است که وجود دارد چون مرورگر یک نمایشگر سند است نه نمایشگر برنامه، و فریمورکهایی که ساخته شدند تا آن نیمه دستی نوشته نشود، زیر وب نشستهاند. حلقهای هم که زیر تمام ترتیب بالاست یک صف است و یک رشته اجرا، که کد چطور اجرا میشود از سمت دیگر است، و زبان همسایهاش که همان کار را روی سرور میکند پایتون است، که مثل این یکی زیر زبانهای برنامهنویسی نشسته است.