نکات جاوااسکریپت همیشه جزو موضوعاتی است که حتی توسعه‌دهندگان باتجربه را شگفت‌زده می‌کند. زبان جاوااسکریپت یکی از پرکاربردترین زبان‌های برنامه‌نویسی در دنیای وب است، اما در عین حال رفتاری دارد که گاهی حتی برای متخصصان هم پیش‌بینی‌ناپذیر به نظر می‌رسد.

برای مثال، قطعه‌کد زیر را در نظر بگیرید:

function returnSomething()
{
  return
    {
      name: 'JavaScript Expert'
      contactMethod: 'Shine batsign at sky'
    }
}

در بیشتر زبان‌های برنامه‌نویسی مانند C# یا Java، خروجی این تابع یک آبجکت با مقادیر مشخص خواهد بود. اما در جاوااسکریپت، اجرای همین کد در کنسول مرورگر مقدار undefined را برمی‌گرداند. این رفتار نشان می‌دهد چرا جاوااسکریپت همچنان زبانی شگفت‌انگیز و در عین حال پیچیده باقی می‌ماند.

در این مقاله به بررسی نکات جاوااسکریپت می‌پردازیم که شناخت آن‌ها می‌تواند در نوشتن کدی تمیزتر و قابل‌پیش‌بینی‌تر نقش مهمی داشته باشد.

زمانی که همه‌چیز طبق برنامه پیش نمی‌رود

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

چه قصد ساخت اپلیکیشن موبایل با React Native را داشته باشیم، چه برنامه دسکتاپ یا تابع سمت سرور با Node.js، جاوااسکریپت در همه‌جا حضور دارد.

اما همین قدمت و گستردگی باعث شده تا طراحان زبان جاوااسکریپت تصمیم‌های پیچیده و گاهی اشتباهی اتخاذ کنند؛ تصمیم‌هایی که تغییرشان امروز عملکرد بسیاری از وب‌سایت‌ها را مختل می‌کند. به همین دلیل، درک این رفتارهای غیرمنتظره، یعنی همان نکات مهم جاوااسکریپت، به ما کمک می‌کند تا در طراحی و نگه‌داری کدهای خود، انتخاب‌های آگاهانه‌تری داشته باشیم.

تزریق خودکار Semicolon

نمونه ابتدایی مقاله، یکی از رفتارهای مفسر جاوااسکریپت را نشان می‌دهد که توسعه‌دهندگان آن را با عنوان Automatic Semicolon Insertion یا «تزریق خودکار Semicolon» می‌شناسند. این ویژگی یکی از نکات جاوااسکریپت است که اغلب توسعه‌دهندگان در برخورد اول آن را اشتباه تفسیر می‌کنند.

در برخی زبان‌ها مانند C#، استفاده از ; در انتهای هر دستور الزامی است. اما در جاوااسکریپت، این علامت اختیاری است و مفسر مجموعه‌ای از قوانین را برای تشخیص محل قرارگیری خودکار آن به کار می‌برد.

در مثال بالا، چون کروشه باز { در خطی جدا از دستور return قرار دارد، مفسر جاوااسکریپت به‌صورت خودکار در انتهای return یک ; اضافه می‌کند و همین باعث می‌شود تابع مقدار undefined برگرداند. بنابراین از دید مفسر، کد ما عملاً به شکل زیر تفسیر می‌شود:

function returnSomething()
{
  return ; // <-- semicolon inserted by ASI, remainder of function not evaluated.
    {
      name: 'JavaScript Expert'
      contactMethod: 'Shine batsign at sky'
    }
}

راه‌حل ساده است: باید کروشه باز { را در همان خطی قرار دهیم که دستور return نوشته شده است. اگرچه استفاده از ; در جاوااسکریپت اختیاری است، اما در عمل کنار گذاشتن آن می‌تواند منجر به خطاهای دشوار برای شناسایی شود. پس بهتر است همیشه در پایان دستورات از ; استفاده کنیم تا از بروز رفتارهای غیرمنتظره جلوگیری شود.

آرایه‌ها با کلیدهای غیر‌پیوسته

در جاوااسکریپت می‌توان آرایه‌هایی با ایندکس‌های ناپیوسته ایجاد کرد. برای مثال:

const arr = [];
arr[0] = 'A';
arr[100] = 'B';
arr[200] = 'C';

در این حالت، مقدار arr.length برابر با 201 خواهد بود، نه ۳. به‌عبارت دیگر، جاوااسکریپت عناصر میانی را با مقدار undefined پر می‌کند تا ایندکس‌های استفاده شده را پوشش دهد. این رفتار، یکی از نکات کم‌تر شناخته شده جاوااسکریپت است که می‌تواند در حلقه‌های تودرتو یا محاسبات ایندکس‌ها باعث نتایج غیرمنتظره شود و یافتن منبع خطا را سخت کند.

در زبان‌هایی مانند C#، آرایه‌ها طول ثابتی دارند و اگر به ایندکسی خارج از محدوده مقدار دهیم، برنامه بلافاصله خطا می‌دهد. اما در جاوااسکریپت، این رفتار به‌صورت پویا و بدون هشدار انجام می‌شود، که اگرچه انعطاف‌پذیری را افزایش می‌دهد، اما احتمال خطا را نیز بالا می‌برد.

در نتیجه، بهتر است در زمان کار با آرایه‌ها، از متدهایی مانند push یا splice برای افزودن عناصر استفاده کنیم تا از ایجاد آرایه‌های «ناپیوسته» جلوگیری شود.

افزودن ویژگی به تایپ‌های Primitive  

در جاوااسکریپت می‌توانیم توابع جدیدی را به prototype انواع داده‌ای اضافه کنیم. به‌عنوان مثال، توسعه‌دهنده می‌تواند ویژگی جدیدی به String.prototype یا Array.prototype بیفزاید و رفتار آن‌ها را تغییر دهد. اما این کار یک اشتباه رایج و خطرناک است و در میان نکات جاوااسکریپت جایگاه خاصی دارد، زیرا باعث می‌شود رفتار آبجکت‌های پایه‌ای در بخش‌های مختلف پروژه متفاوت باشد.

به‌عنوان مثال، اگر تابعی دلخواه به String.prototype اضافه کنیم، ممکن است در کدهای دیگر یا کتابخانه‌های خارجی تعارض ایجاد کند و باگ‌هایی غیرقابل‌پیش‌بینی به وجود آورد:

String.prototype.alwaysReturnsFalse = () => false;

const itsAString = "hooray";
console.log(itsAString.alwaysReturnsFalse()); // false

در این مثال، ما تابع جدیدی به String.prototype اضافه کرده‌ایم و تمام رشته‌ها اکنون این تابع را در اختیار دارند. شاید در نگاه اول جالب به نظر برسد، اما واقعیت این است که چنین تغییری می‌تواند در پروژه‌های بزرگ، باعث بروز مشکلات و تداخل در کدهای دیگر شود.

تیم TC39 سال‌ها برای تعریف دقیق رفتار جاوااسکریپت زمان صرف کرده است؛ بنابراین تغییر یا افزودن متدهای دلخواه به آبجکت‌های پایه‌ای، نه‌تنها فایده‌ای ندارد، بلکه می‌تواند کل برنامه را بی‌ثبات کند.

در حالی که می‌توان به آبجکت‌های سفارشی ویژگی یا تابع جدیدی افزود، جاوااسکریپت اجازه نمی‌دهد به تایپ‌های Primitive مانند string یا number ویژگی جدید اضافه کنیم. اگر چنین تلاشی انجام دهیم، مفسر آن را می‌پذیرد، اما در زمان اجرا با خطای TypeError مواجه می‌شویم:

const testString = "Hello world!";
testString.onlyFalse = () => false;

console.log(testString.onlyFalse); // [Function: onlyFalse]
console.log(testString.onlyFalse()); // ❌ TypeError: testString.onlyFalse is not a function

در نگاه اول فکر می‌کنیم تابع به رشته افزوده شده چون در کنسول نمایش داده می‌شود، اما در زمان اجرا جاوااسکریپت هیچ ویژگی‌ای به رشته اضافه نمی‌کند.

به بیان ساده، تایپ‌های Primitive تغییرناپذیر (immutable) هستند و جاوااسکریپت ویژگی‌های جدید آن‌ها را نادیده می‌گیرد.

چرا این موضوع اهمیت دارد؟

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

در زبان‌هایی با ساختار سخت‌گیرانه‌تر، اگر دستوری را نادرست بنویسیم، خطا دریافت می‌کنیم. جاوااسکریپت اغلب به‌جای خطا دادن، به‌نرمی از کنار اشتباه ما می‌گذرد و آن را نادیده می‌گیرد، همین رفتار گاهی انتظارات ما از منطق زبان را تغییر می‌دهد.

تبدیل تایپ

در زبان‌های strongly typed مانند C#، نوع داده‌ها پیش از استفاده باید مشخص شود. اما جاوااسکریپت چنین محدودیتی ندارد و به‌طور خودکار نوع داده‌ها را برای سازگاری با یکدیگر تغییر می‌دهد.

از یک جنبه، این ویژگی مفید است؛ زیرا نیاز ما به تبدیل دستی نوع‌ها (Casting) را کاهش می‌دهد، مزیتی که در بسیاری از مواقع باعث ساده‌تر شدن کدنویسی می‌شود:

let num = 5;
let str = "10";

console.log(num + Number(str)); // 15
console.log(num + str); // "510" → because JavaScript prefers string concatenation

همین رفتار در مورد نوع‌های boolean نیز صدق می‌کند:

console.log(true + 1); // 2 → true is converted to number 1
console.log(false + 1); // 1 → false is converted to number 0
console.log(true + "1"); // "true1" → because string type takes precedence

تا اینجا، همه چیز منطقی و کاربردی به نظر می‌رسد. اما هنگامی که به عملیات جمع می‌رسیم، رفتار زبان کمی غیرمنتظره می‌شود. اگر یک رشته "1" را با عدد 1 جمع کنیم، چه نتیجه‌ای خواهیم گرفت؟ جاوااسکریپت در این حالت رشته را ترجیح می‌دهد یا عدد؟

console.log("1" + 1); // "11" → the number is converted to a string
console.log(1 + "1"); // "11" → order doesn’t matter

ماجرا زمانی جالب‌تر می‌شود که مقدار boolean را هم وارد معادله کنیم:

console.log(true + "1");  // "true1"
console.log("1" + false); // "1false"
console.log(true + 1);    // 2

ممکن است تصور کنیم که دلیل این رفتار آن است که مقادیر boolean در واقع نوعی عدد هستند، اما چنین نیست. مقدار true یا false همان boolean باقی می‌مانند. جاوااسکریپت صرفاً برای حفظ سازگاری، مقادیر را به‌گونه‌ای تغییر می‌دهد تا در کنار هم کار کنند؛ حتی اگر منطق روشنی پشت آن وجود نداشته باشد.

چرا این موضوع اهمیت دارد؟

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

Function Hoisting

در بسیاری از زبان‌های برنامه‌نویسی امروزی، مفهومی به نام Function Hoisting وجود دارد. به‌طور خلاصه، این ویژگی به ما امکان می‌دهد که توابع خود را در هر جای فایل بنویسیم و حتی پیش از تعریف آن‌ها، فراخوانی‌شان کنیم:

sayHello(); // ✅ Works because the function is hoisted

function sayHello() {
console.log("Hello, world!");
}

این قابلیت بسیار مفید است، زیرا نیازی نداریم ترتیب نوشتن کدها را به‌صورت دستی تنظیم کنیم تا کد به‌درستی اجرا شود. اما نکته کلیدی این است که در جاوااسکریپت بیش از یک روش برای تعریف تابع وجود دارد.

در مثال بالا از Function Declaration استفاده کردیم، اما می‌توانیم به‌جای آن از Function Expression هم استفاده کنیم.

sayHi(); // ❌ Error: Cannot access 'sayHi' before initialization

const sayHi = function () {
console.log("Hi there!");
};

sayHi(); // ✅ Works only after the definition

چرا این موضوع اهمیت دارد؟

در ظاهر، تفاوت چندانی بین این دو روش وجود ندارد. اما در عمل، انتخاب اشتباه می‌تواند منجر به خطا شود.

در روش Function Declaration، تابع قبل از تعریف هم در دسترس است، در حالی‌که در Function Expression تنها پس از تعریف قابل فراخوانی است.

بنابراین، در طراحی ساختار پروژه باید آگاهانه تصمیم بگیریم که از کدام روش استفاده کنیم تا از خطاهای غیرمنتظره جلوگیری شود.

مقدار null یک آبجکت است

در اکثر زبان‌های برنامه‌نویسی، ویژگی‌های آبجکت‌ها یا مقداردهی می‌شوند یا مقدار آن‌ها null است؛ به این معنا که هیچ مقداری به آن‌ها تخصیص داده نشده است. اما جاوااسکریپت این مفهوم را پیچیده‌تر کرده و علاوه بر null، مقداری دیگر به نام undefined نیز دارد.

در نگاه اول ممکن است تصور کنیم این دو یکی هستند، اما تفاوت مهمی میان آن‌ها وجود دارد:

در نتیجه، هرچند هر دو بیانگر نبود مقدارند، اما مفهوم آن‌ها متفاوت است:

let a = null;
let b;

console.log(a); // null
console.log(b); // undefined

و حالا اگر نوع (typeof) آن‌ها را بررسی کنیم، با نتیجه‌ای شگفت‌انگیز روبه‌رو می‌شویم:

console.log(typeof undefined); // "undefined"
console.log(typeof null);      // "object"

در جاوااسکریپت، نوع null برابر "object" است! به عبارت دیگر، هم یک آبجکت واقعی و هم مقدار null از نظر نوع، یکسان در نظر گرفته می‌شوند:

let obj = { name: "Sevda" };
let empty = null;

console.log(typeof obj);   // "object"
console.log(typeof empty); // "object"

چرا این موضوع اهمیت دارد؟

جاوااسکریپت سیستم بررسی تایپ قوی و صریحی ندارد و فقط چند نوع داده اولیه را تعریف می‌کند. به همین دلیل، استفاده از typeof همیشه نتیجه دقیقی به ما نمی‌دهد.

برای مثال، اگر متغیری واقعاً شامل یک آبجکت باشد، typeof مقدار "object" را برمی‌گرداند. اما اگر مقدارش null باشد، باز هم "object" برگردانده می‌شود!

این رفتار خلاف منطق به نظر می‌رسد، زیرا انتظار داریم null نشان‌دهنده نبود آبجکت باشد، نه خود آن.

جمع‌بندی

هیچ تردیدی در محبوبیت بی‌نظیر جاوااسکریپت وجود ندارد. با رشد مداوم اکوسیستم‌هایی مانند npm و افزایش تعداد کتابخانه‌ها، توسعه‌دهندگان بیش از هر زمان دیگری به این زبان توجه نشان می‌دهند.

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

اینکه مقدار null نوعش object است، یا مفسر جاوااسکریپت خودکار ; اضافه می‌کند، ویژگی‌هایی هستند که احتمالاً هرگز تغییر نخواهند کرد.

در نهایت، ایمن‌تر و منطقی‌تر است که به‌جای تغییر در ساختار زبان، توسعه‌دهندگان را از این رفتارهای خاص آگاه کنیم تا بتوانند آگاهانه تصمیم بگیرند، و فراموش نکنند از ; استفاده کنند.