چگونه خطای NaN در JavaScript را ریشهای رفع کنیم؟
چرا خطای NaN در جاوااسکریپت رخ میدهد، تفاوت آن با Infinity و undefined چیست و چگونه میتوان با Number.isNaN، اعتبارسنجی ورودی و TypeScript، این خطا را بهطور پایدار رفع کرد؟ راهنمای عملی مبتنی بر تجربه.
اولین بار که یک خطای NaN در JavaScript وقتم را گرفت، در یک پروژهی فروشگاهی بود. محاسبهی سبد خرید، در بعضی شرایط، بهجای مبلغ، عبارت NaN را نشان میداد. کاربر مبلغ کل را NaN میدید و از خرید منصرف میشد. تا آن روز، NaN را یک اصطلاح فنی میدانستم که بهندرت به کار میآید؛ آن روز فهمیدم که خطای NaN در JavaScript، در باطن، یک پنجره به سمت تفکر مرزی، کیفیت داده، و انضباط در عملیات عددی است. NaN یک شکایت از کیفیت داده است، نه فقط از عملیات ریاضی.
NaN در JavaScript دقیقاً چیست؟
NaN مخفف Not a Number است و یکی از مقادیر خاص در استاندارد IEEE 754 برای اعداد اعشاری (floating point numbers) است. در JavaScript، NaN یک مقدار از نوع number است که نشان میدهد یک عملیات ریاضی، نتیجهی معتبر عددی تولید نکرده است. برخلاف خطای null در جاوااسکریپت که باعث پرتاب استثنا میشود، NaN یک مقدار معتبر است که بیصدا در محاسبات پخش میشود. بههمین دلیل، تشخیص آن دشوارتر و پیامدهای آن میتواند بسیار سنگینتر باشد.
const result = parseInt("hello");
console.log(result); // NaN
const sum = 10 + NaN;
console.log(sum); // NaN
const check = NaN === NaN;
console.log(check); // false (!)
سه ویژگی منحصربهفرد NaN که آن را از هر مقدار دیگری در JavaScript جدا میکند:
- NaN با هیچ مقداری، حتی خودش، برابر نیست.
NaN === NaNمقدارfalseبرمیگرداند. - typeof NaN برابر
"number"است. یعنی NaN یک عدد است که عدد نیست. - NaN در محاسبات پخش میشود. هر عملیات ریاضی که NaN در آن شرکت کند، نتیجهی NaN خواهد داشت.
این سه ویژگی، علت اصلی شهرت NaN بهعنوان یکی از عجیبترین مقادیر در JavaScript است. اگر با مبانی پایتون آشنایی ندارید، ابتدا آموزش جاوااسکریپت از صفر را بخوانید تا مدل ذهنی درستی از انواع داده شکل بگیرد. NaN از آن مقادیری است که درک آن، پیشنیاز کار حرفهای با JavaScript است.
NaN یک شکایت از کیفیت داده است، نه از عملیات ریاضی. JavaScript میگوید عملیات را انجام دادم، ولی آنچه به من دادی، قابل تبدیل به عدد نبود. این خطا، یک پنجره به سمت کیفیت دادهی ورودی است.
چرا NaN یکی از عجیبترین مقادیر JavaScript است؟
NaN از جهات متعدد، با هر مقدار دیگری در JavaScript تفاوت دارد. این تفاوتها، در ظاهر پیچیده بهنظر میرسند، ولی درک آنها، کلید تشخیص و پیشگیری است:
| ویژگی | NaN | سایر مقادیر |
|---|---|---|
| برابری با خود | false | true |
| typeof | "number" | متناسب با نوع |
| تبدیل به رشته | "NaN" | متناسب با مقدار |
| تبدیل به بولی | false | بسته به مقدار |
| در محاسبات | پخش میشود | نتیجهی مشخص |
مقایسه با < یا > |
همیشه false | مقدار مشخص |
این جدول، تفاوتهای کلیدی را نشان میدهد. در تجربهی من، سه ویژگی زیر بیشترین سردرگمی را ایجاد میکنند:
یک: NaN با خودش برابر نیست. این ویژگی، از استاندارد IEEE 754 میآید که NaN را بهعنوان یک مقدار «نامعین» تعریف میکند. اگر دو مقدار نامعین داشته باشیم، نمیتوان گفت برابر هستند، چون یکی از آنها ممکن است از یک عملیات متفاوت آمده باشد.
دو: NaN در مقایسههای بزرگتر و کوچکتر هم false میدهد. NaN > 5 و NaN < 5 هر دو false هستند. این رفتار، در مرتبسازی و فیلترها میتواند نتایج غیرمنتظره بدهد.
سه: NaN در تبدیل به بولی، false میدهد. یعنی if (NaN) مقدار false میدهد. این رفتار، از این واقعیت میآید که NaN یک مقدار falsy است.
در چارچوب کلی JavaScript، این ویژگیها باعث میشوند که NaN در ظاهر عجیب بهنظر برسد. ولی در باطن، این رفتارها از یک اصل بنیادین میآیند: NaN باید بهراحتی قابل تشخیص باشد. اگر NaN با خودش برابر بود، تشخیص آن در میان اعداد معتبر بسیار سختتر میشد. مبانی انواع داده در مفاهیم پایه جاوااسکریپت آمده است.
typeof NaN و معمای نوع
یکی از اولین شگفتیهایی که برنامهنویسان تازهکار با آن مواجه میشوند، این است که typeof NaN مقدار "number" برمیگرداند:
console.log(typeof NaN); // "number"
console.log(NaN instanceof Number); // false
این رفتار، از یک اصل بنیادین میآید: NaN یک مقدار عددی است که نمایندهی «عدد نامعتبر» است. به همین دلیل، JavaScript آن را از نوع number در نظر میگیرد. ولی NaN یک شیء Number نیست، بلکه یک مقدار اولیهی عددی است.
پیامدهای این رفتار:
- بررسی
typeof x === "number"، NaN را هم شامل میشود. اگر میخواهید NaN را از اعداد معتبر جدا کنید، این بررسی کافی نیست. - در توابعی که نوع عدد را بررسی میکنند، NaN بهعنوان یک عدد معتبر در نظر گرفته میشود.
- در ساختارهای داده که بر اساس نوع متمایز میکنند، NaN با اعداد عادی در یک دسته قرار میگیرد.
راهحل: برای تشخیص عدد معتبر، از Number.isFinite() یا ترکیب typeof با بررسی NaN استفاده کنید:
function isValidNumber(value) {
return typeof value === "number" && !Number.isNaN(value) && Number.isFinite(value);
}
console.log(isValidNumber(5)); // true
console.log(isValidNumber(NaN)); // false
console.log(isValidNumber(Infinity)); // false
console.log(isValidNumber("5")); // false
این تابع، در اعتبارسنجی دادههای ورودی، بسیار مفید است. مبانی تفاوت انواع داده در خطای TypeError در جاوااسکریپت آمده است.
معمای مقایسه: NaN !== NaN
شاید عجیبترین ویژگی NaN، این باشد که NaN با خودش برابر نیست:
console.log(NaN === NaN); // false
console.log(NaN == NaN); // false
console.log(NaN !== NaN); // true
این رفتار، از استاندارد IEEE 754 میآید که در آن NaN یک مقدار «نامعین» است. اگر دو مقدار نامعین داشته باشیم، نمیتوان گفت برابر هستند. مثال: اگر نتیجهی parseInt("hello") و parseInt("world") هر دو NaN باشند، نمیتوان گفت این دو نتیجه برابر هستند.
پیامدهای عملی این رفتار:
یک: نمیتوانید از === برای بررسی NaN استفاده کنید. یعنی if (value === NaN) همیشه false است و این بررسی کار نمیکند.
دو: در آرایهها، indexOf و includes برای NaN رفتار متفاوت دارند.
const arr = [1, 2, NaN, 4];
console.log(arr.indexOf(NaN)); // -1 (پیدا نمیکند)
console.log(arr.includes(NaN)); // true (پیدا میکند)
این تفاوت، از این میآید که includes از الگوریتم SameValueZero استفاده میکند که NaN را با NaN برابر میداند، در حالی که indexOf از === استفاده میکند.
سه: در Set و Map، NaN بهعنوان یک کلید معتبر کار میکند.
const set = new Set();
set.add(NaN);
set.add(NaN);
console.log(set.size); // 1 (چون SameValueZero)
برای بررسی NaN، از Number.isNaN یا Object.is استفاده کنید:
console.log(Number.isNaN(NaN)); // true
console.log(Object.is(NaN, NaN)); // true
این دو روش، تنها راههای مطمئن برای بررسی NaN هستند. مبانی ساختار دادهها در آرایهها در جاوااسکریپت آمده است.
نُه علت رایج تولید NaN
در تجربهی من روی صدها پروژهی JavaScript، NaN از نُه علت مشخص میآید. شناخت این علتها، تشخیص را در چند ثانیه ممکن میکند.
- parseInt و parseFloat که شکست میخورند: تبدیل رشتهی غیرعددی به عدد.
- عملیات ریاضی با مقادیر غیرعددی: جمع رشته و عدد بدون تبدیل.
- ورودی فرم با مقدار خالی یا غیرعددی: مقدار رشتهای از فیلد input.
- پاسخ API با فیلد غیرعددی: دادهای که انتظار عدد دارد، ولی رشته میآید.
- جمع آرایه با مقادیر undefined: کاهش (reduce) روی آرایه با مقادیر نامعتبر.
- Math.sqrt و توابع ریاضی با ورودی منفی: جذر عدد منفی.
- تبدیل تاریخ و زمان به عدد: Date نامعتبر یا فرمت اشتباه.
- مقدار غیرعددی از CMS یا localStorage: دادهی ذخیرهشده با فرمت اشتباه.
- عملیات روی مقادیر NaN موجود: پخش NaN در محاسبات بعدی.
هر علت، نشانههای مخصوص به خود و راهحل اختصاصی دارد. در بخشهای بعدی، هر علت را جداگانه باز میکنم.
parseInt و parseFloat که شکست میخورند
شایعترین منبع NaN در JavaScript، شکست توابع parseInt و parseFloat است. این توابع، وقتی رشتهی ورودی قابل تبدیل به عدد نباشد، NaN برمیگردانند:
console.log(parseInt("123")); // 123
console.log(parseInt("123abc")); // 123 (تا اولین کاراکتر غیرعددی)
console.log(parseInt("abc")); // NaN
console.log(parseInt("")); // NaN
console.log(parseFloat("3.14")); // 3.14
console.log(parseFloat("3.14.15")); // 3.14
console.log(parseFloat("hello")); // NaN
نکتهی ظریف: parseInt روی رشتهی خالی و رشتهای که با کاراکتر غیرعددی شروع میشود، NaN برمیگرداند. ولی اگر رشته با عدد شروع شود و بعد کاراکتر غیرعددی داشته باشد، فقط قسمت عددی را برمیگرداند.
راهحل: قبل از parse، ورودی را بررسی کنید یا از Number() و بررسی NaN استفاده کنید:
function safeParseInt(value, defaultValue = 0) {
const parsed = parseInt(value, 10);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
console.log(safeParseInt("123")); // 123
console.log(safeParseInt("abc")); // 0
console.log(safeParseInt("")); // 0
نکتهی مهم: همیشه radix را در parseInt مشخص کنید (مثلاً parseInt(value, 10)) تا از رفتار ناخواسته در رشتههایی که با صفر شروع میشوند جلوگیری کنید. مبانی تفاوت undefined و NaN در خطای undefined در جاوااسکریپت آمده است.
عملیات ریاضی با مقادیر غیرعددی
دومین منبع شایع NaN، عملیات ریاضی با مقادیر غیرعددی است. در JavaScript، عملگر + دو رفتار متفاوت دارد: اگر یکی از عملوندها رشته باشد، عملیات الحاق (concatenation) انجام میشود؛ در غیر این صورت، جمع عددی.
console.log(10 + "5"); // "105" (الحاق)
console.log("10" + "5"); // "105" (الحقاق)
console.log(10 + "abc"); // "10abc" (الحقاق)
console.log(10 - "abc"); // NaN (تبدیل عددی ناموفق)
console.log(10 * "abc"); // NaN
console.log(10 / "abc"); // NaN
نکتهی ظریف: عملگرهای -، *، / همیشه عددی هستند، بنابراین اگر یکی از عملوندها قابل تبدیل به عدد نباشد، NaN برمیگردانند. ولی عملگر + اگر یکی از عملوندها رشته باشد، به الحاق تبدیل میشود.
راهحل: همیشه قبل از عملیات ریاضی، مقادیر را به عدد تبدیل کنید:
function safeAdd(a, b) {
const numA = Number(a);
const numB = Number(b);
if (Number.isNaN(numA) || Number.isNaN(numB)) {
return 0; // یا هر مقدار پیشفرض دیگر
}
return numA + numB;
}
نکتهی مهم: عملگر Number() روی رشتهی خالی، مقدار 0 برمیگرداند. اگر میخواهید رشتهی خالی را NaN در نظر بگیرید، از parseInt یا parseFloat استفاده کنید. جزئیات کامل در مفاهیم پایه جاوااسکریپت آمده است.
ورودی فرم و DOM با مقدار غیرعددی
سومین منبع شایع NaN، ورودی فرم است. مقدار input.value همیشه یک رشته است، حتی اگر type="number" باشد:
const input = document.getElementById("quantity");
const value = input.value; // "5" (رشته)
const total = value * 10; // 50 (تبدیل خودکار)
در این مثال، عملگر * مقدار رشته را به عدد تبدیل میکند. ولی اگر کاربر مقدار خالی یا غیرعددی وارد کند:
const input = document.getElementById("quantity");
const value = input.value; // "" یا "abc"
const total = value * 10; // NaN (تبدیل ناموفق)
راهحل: اعتبارسنجی ورودی کاربر:
function calculateTotal(inputElement, price) {
const rawValue = inputElement.value.trim();
if (rawValue === "") {
return 0;
}
const quantity = parseInt(rawValue, 10);
if (Number.isNaN(quantity)) {
return 0;
}
return quantity * price;
}
نکتهی حرفهای: در فرمهای واقعی، همیشه از اعتبارسنجی در چند لایه استفاده کنید: در سطح HTML (attributes مثل required و pattern)، در سطح JavaScript (این تابع)، و در سطح سرور (بهعنوان آخرین لایه دفاعی). مبانی DOM در کار با DOM در جاوااسکریپت آمده است.
JSON و پاسخهای API
چهارمین منبع NaN، پردازش پاسخهای API است. فیلدهای عددی در JSON، ممکن است بهعنوان رشته برگردند یا اصلاً وجود نداشته باشند:
const response = {
product: "Book",
price: "12.50" // رشته، نه عدد
};
const total = response.price * 2; // 25 (تبدیل خودکار)
const discount = response.discount * 2; // NaN (undefined * 2)
در این مثال، فیلد discount وجود ندارد و مقدار آن undefined است. عملگر * روی undefined، NaN برمیگرداند.
راهحل: اعتبارسنجی پاسخ API با مقادیر پیشفرض:
function processProduct(apiResponse) {
const price = parseFloat(apiResponse.price) || 0;
const discount = parseFloat(apiResponse.discount) || 0;
return {
price,
discount,
total: price - discount
};
}
نکتهی ظریف: عملگر || اگر مقدار سمت چپ falsy باشد (مثل 0 یا NaN)، مقدار سمت راست را برمیگرداند. ولی این رویکرد، مقدار صفر معتبر را با مقدار پیشفرض جایگزین میکند. بهتر است از بررسی صریح استفاده کنید:
const price = parseFloat(apiResponse.price);
const safePrice = Number.isNaN(price) ? 0 : price;
مبانی کار با JSON در JSON چیست و چگونه دادهها را ساختاردهی میکند آمده است.
جمع و میانگین آرایه با مقادیر undefined
پنجمین منبع NaN، عملیات روی آرایهها است. اگر آرایه حاوی مقادیر undefined، null، یا رشتههای غیرعددی باشد، جمع یا میانگین NaN میشود:
const numbers = [1, 2, "abc", 4];
const sum = numbers.reduce((acc, n) => acc + n, 0);
// "12abc4" (الحقاق رشته!)
const product = numbers.reduce((acc, n) => acc * n, 1);
// NaN (ضرب در "abc")
در مثال اول، عملگر + با رشته، الحاق میکند و نتیجه یک رشته است. در مثال دوم، عملگر * روی رشتهی غیرعددی، NaN برمیگرداند.
راهحل: قبل از کاهش، مقادیر را فیلتر و تبدیل کنید:
function safeSum(numbers) {
return numbers
.map(n => Number(n))
.filter(n => !Number.isNaN(n))
.reduce((acc, n) => acc + n, 0);
}
console.log(safeSum([1, 2, "3", "abc", 4])); // 10
این رویکرد، در محاسبات آماری روی دادههای کاربر، ضروری است. نگاه عمیقتر به مبانی آرایهها در آرایهها در جاوااسکریپت آمده است.
Math.sqrt و توابع ریاضی
ششمین منبع NaN، استفاده از توابع Math با ورودی نامعتبر است:
console.log(Math.sqrt(-1)); // NaN
console.log(Math.log(-1)); // NaN
console.log(Math.log(0)); // -Infinity
console.log(Math.acos(2)); // NaN
console.log(Math.asin(2)); // NaN
این توابع، در دامنهی محدودی از مقادیر معتبر هستند. خارج از آن دامنه، NaN برمیگردانند. راهحل: بررسی دامنه قبل از فراخوانی:
function safeSqrt(value) {
const num = Number(value);
if (Number.isNaN(num) || num < 0) {
return null; // یا هر مقدار پیشفرض دیگر
}
return Math.sqrt(num);
}
نکتهی ظریف: در JavaScript، برخلاف Python که خطای ValueError میدهد، Math.sqrt(-1) بیصدا NaN برمیگرداند. این رفتار میتواند باعث پخش NaN در محاسبات بعدی شود. مبانی تفاوت رفتار در زبانهای مختلف در مباحث JavaScript آمده است.
تبدیل تاریخ و زمان به عدد
هفتمین منبع NaN، تبدیل تاریخ به عدد است. اگر یک شیء Date نامعتبر باشد، متدهای آن NaN برمیگردانند:
const date = new Date("invalid");
console.log(date.getTime()); // NaN
console.log(date.getFullYear()); // NaN
console.log(Number(date)); // NaN
راهحل: بررسی اعتبار تاریخ قبل از استفاده:
function safeDate(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date;
}
این رویکرد، در پردازش دادههای تاریخ از API یا فرمهای کاربر، ضروری است. مبانی کار با تاریخ در مباحث JavaScript آمده است.
مقدار غیرعددی از CMS یا localStorage
هشتمین منبع NaN، خواندن مقدار از localStorage، sessionStorage، یا CMS است. در این سیستمها، مقادیر همیشه بهعنوان رشته ذخیره میشوند:
localStorage.setItem("cart_count", "abc");
const count = localStorage.getItem("cart_count"); // "abc"
const total = count * 10; // NaN
راهحل: اعتبارسنجی مقادیر خواندهشده از storage:
function getCartCount() {
const raw = localStorage.getItem("cart_count");
const count = parseInt(raw, 10);
return Number.isNaN(count) ? 0 : count;
}
نکتهی مهم: localStorage.getItem برای کلیدهای ناموجود، مقدار null برمیگرداند. سپس parseInt(null, 10) مقدار NaN میدهد. همیشه مقادیر پیشفرض مشخص داشته باشید.
روشهای تشخیص NaN
در JavaScript، چند روش برای تشخیص NaN وجود دارد که هرکدام رفتار متفاوتی دارند:
روش اول: Number.isNaN
دقیقترین روش تشخیص NaN:
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("hello")); // false
console.log(Number.isNaN(undefined)); // false
console.log(Number.isNaN(5)); // false
این روش، فقط برای مقادیری که واقعاً NaN هستند، true برمیگرداند. اگرچه ورودی هر نوعی را میپذیرد، ولی فقط روی مقادیر عددی NaN پاسخ میدهد.
روش دوم: Object.is
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(NaN, 5)); // false
این روش، از الگوریتم SameValue استفاده میکند و NaN را با NaN برابر میداند.
روش سوم: isNaN (سراسری)
console.log(isNaN(NaN)); // true
console.log(isNaN("hello")); // true (!)
console.log(isNaN(undefined)); // true (!)
console.log(isNaN(5)); // false
این روش، ابتدا ورودی را به عدد تبدیل میکند و بعد بررسی میکند که آیا NaN است یا نه. به همین دلیل، برای مقادیری که قابل تبدیل به عدد نیستند (مثل رشتههای غیرعددی)، هم true برمیگرداند. این رفتار، در بعضی موارد مفید و در بعضی موارد گیجکننده است.
تفاوت isNaN و Number.isNaN
تفاوت بین isNaN (سراسری) و Number.isNaN (متد) یکی از رایجترین سؤالات تازهکارهاست. تفاوت اصلی در مرحلهی تبدیل نوع است:
| ورودی | isNaN | Number.isNaN |
|---|---|---|
| NaN | true | true |
| "hello" | true | false |
| undefined | true | false |
| null | false | false |
| {} | true | false |
| [] | false | false |
| "123" | false | false |
این جدول، تفاوت را بهوضوح نشان میدهد:
isNaNابتدا مقدار را باNumber()به عدد تبدیل میکند، سپس بررسی میکند که آیا NaN است.Number.isNaNمقدار را تبدیل نمیکند و فقط بررسی میکند که آیا مقدار، دقیقاً NaN است.
در ۹۰ درصد موارد، Number.isNaN انتخاب درست است چون دقیقتر عمل میکند. isNaN سراسری، در مواردی که عمداً میخواهید مقادیر قابل تبدیل را هم بررسی کنید، مفید است.
// سناریوی اشتباه: استفاده از isNaN برای بررسی NaN
const value = "5";
if (isNaN(value)) {
// اجرا نمیشود، چون "5" قابل تبدیل به عدد است
}
// سناریوی درست: استفاده از Number.isNaN
if (Number.isNaN(value)) {
// اجرا نمیشود، چون "5" NaN نیست (تبدیل انجام نمیشود)
}
مبانی مقایسه و اپراتورها در آموزش ES6 در جاوااسکریپت آمده است.
راهبردهای رفع اصولی
بعد از تشخیص، نوبت به رفع میرسد. راهبردهای رفع، بر اساس نوع خطا متفاوت است:
راهبرد اول: تبدیل صریح قبل از محاسبه
function toNumber(value, defaultValue = 0) {
const num = Number(value);
return Number.isFinite(num) ? num : defaultValue;
}
const total = toNumber("12.50") * 2; // 25
این رویکرد، در سراسر پروژه قابل استفاده است و مدیریت خطا را متمرکز میکند.
راهبرد دوم: تابع کمکی برای محاسبات
function safeArithmetic(operation, a, b, defaultValue = 0) {
const numA = Number(a);
const numB = Number(b);
if (!Number.isFinite(numA) || !Number.isFinite(numB)) {
return defaultValue;
}
const result = operation(numA, numB);
return Number.isFinite(result) ? result : defaultValue;
}
console.log(safeArithmetic((a, b) => a + b, "5", "3")); // 8
console.log(safeArithmetic((a, b) => a + b, "hello", "3")); // 0
راهبرد سوم: استفاده از مقادیر پیشفرض منطقی
const count = getCount() ?? 0; // فقط برای null و undefined
const price = parseFloat(rawPrice) || 0; // برای مقادیر falsy
نکتهی ظریف: عملگر ?? فقط در صورت null یا undefined، مقدار پیشفرض میدهد. عملگر || همهی مقادیر falsy را شامل میشود. انتخاب بین این دو، بستگی به منطق کسبوکار دارد.
راهبرد چهارم: اعتبارسنجی در لایه ورودی
function validateNumber(value, fieldName) {
if (typeof value !== "number") {
throw new Error(`${fieldName} must be a number`);
}
if (!Number.isFinite(value)) {
throw new Error(`${fieldName} must be finite, got ${value}`);
}
return value;
}
try {
validateNumber(parseFloat(input.value), "price");
} catch (error) {
console.error(error.message);
}
راهبرد پنجم: بازنویسی منطق با Type Safety
در بعضی موارد، NaN نشانهی مشکل منطقی است. بازنویسی منطق با تعریف صریح نوع داده، بهترین راهحل است. در TypeScript، با strictNullChecks و انواع مناسب، بخش بزرگی از NaNها در زمان compile تشخیص داده میشوند. مبانی تفاوت رفتار TypeScript با JavaScript در آموزش تایپ اسکریپت از صفر آمده است.
پیشگیری در سطح معماری
در تجربهی من، بهترین راه برخورد با NaN، پیشگیری در سطح معماری است. سه رویکرد که در تمام پروژههای جدی اجرا میکنم:
رویکرد اول: تعریف نوع صریح در لایه داده
در تمام توابعی که با اعداد کار میکنند، نوع ورودی و خروجی را صریح کنید:
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}
این رویکرد در JavaScript خالص با JSDoc، و در TypeScript با نوعهای صریح، امکان تشخیص زودهنگام خطاها را فراهم میکند.
رویکرد دوم: لایهی اعتبارسنجی مرکزی
در لایهی ورودی برنامه (form submission، API response)، مقادیر را اعتبارسنجی کنید:
const validators = {
isPositiveNumber: (value) => typeof value === "number" && value > 0,
isNonEmptyString: (value) => typeof value === "string" && value.trim().length > 0,
};
function validatePayload(payload, schema) {
const errors = [];
for (const [field, validator] of Object.entries(schema)) {
if (!validator(payload[field])) {
errors.push(`Invalid field: ${field}`);
}
}
if (errors.length) {
throw new Error(errors.join(", "));
}
return payload;
}
رویکرد سوم: تستهای مرزی
برای هر تابع محاسباتی، تستهای مرزی بنویسید:
describe("calculateTotal", () => {
it("handles valid numbers", () => {
expect(calculateTotal(5, 10)).toBe(50);
});
it("handles null input", () => {
expect(calculateTotal(null, 10)).toBe(0);
});
it("handles string input", () => {
expect(calculateTotal("5", 10)).toBe(50);
});
it("handles invalid string", () => {
expect(calculateTotal("abc", 10)).toBe(0);
});
});
این رویکرد، در پروژههای واقعی، بخش بزرگی از NaNها را قبل از production کشف میکند. مبانی تست در مباحث JavaScript آمده است.
TypeScript و جلوگیری از NaN
TypeScript با type system قدرتمند، بخش بزرگی از NaNها را در زمان compile تشخیص میدهد. مهمترین ویژگیها:
یک: strictNullChecks. این گزینه، TypeScript را مجبور میکند که null و undefined را در نظر بگیرد، که منبع اصلی NaN است.
دو: strict mode. فعالسازی کامل strict: true در tsconfig.json، تمام بررسیهای نوع را فعال میکند:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true
}
}
سه: Union Types. استفاده از Union Types برای بیان مقادیر ممکن:
type Price = number | null | undefined;
function calculateTotal(price: Price, quantity: number): number {
if (typeof price !== "number" || !Number.isFinite(price)) {
return 0;
}
return price * quantity;
}
چهار: Type Guards. تعریف توابعی که نوع را تأیید میکنند:
function isValidNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
if (isValidNumber(input)) {
// TypeScript میداند input یک عدد معتبر است
const total = input * 10;
}
در پروژههای مدرن، ترکیب TypeScript با ابزارهای تحلیل ایستا مثل ESLint و Prettier، بخش بزرگی از NaNها را قبل از deployment کشف میکند. مبانی تفاوت TypeScript با JavaScript در تفاوت تایپ اسکریپت و جاوااسکریپت آمده است.
خطای NaN در محیط production
در محیط production، NaN ابعاد جدیتری دارد:
پخش بیصدای در محاسبات
برخلاف خطای TypeError در جاوااسکریپت که باعث پرتاب استثنا میشود، NaN بیصدا در محاسبات پخش میشود. این بیصدایی، تشخیص را بسیار سختتر میکند. در یک داشبورد مالی، NaN میتواند از یک فیلد مخفی به مبلغ کل، و از مبلغ کل به گزارش سالانه پخش شود.
تأثیر بر تجربه کاربری
وقتی کاربر در فرمی با NaN یا null مواجه میشود، اعتماد خود را به سرویس از دست میدهد. در فروشگاههای آنلاین، این مسئله به لغو سفارش منجر میشود. در داشبوردهای تحلیلی، به تصمیمگیری بر اساس دادهی نادرست.
پایش و آلارمدهی
در production، NaN باید بهطور مناسب پایش شود. راهحل: در توابع محاسباتی حساس، بررسی صریح NaN و لاگ کردن آن:
function trackCalculation(name, result) {
if (Number.isNaN(result)) {
console.error(`NaN detected in ${name}`);
if (typeof window !== "undefined" && window.Sentry) {
window.Sentry.captureMessage(`NaN in ${name}`, "warning");
}
}
return result;
}
پیشگیری با تست
بیشتر این خطاها را میتوان قبل از production با تستهای خودکار کشف کرد. تستهای parameterized در Jest و Vitest، امکان تست روی دامنهی وسیعی از ورودیها را فراهم میکنند. مبانی بهینهسازی در بهینهسازی جاوااسکریپت آمده است.
اشتباهات رایج در برخورد با NaN
در طول سالها، الگوهای تکراری از اشتباهات دیدهام که هر کدام میتواند پروژه را به چالش بکشد:
اشتباه اول: استفاده از value === NaN
این مقایسه همیشه false برمیگرداند و کار نمیکند. راهحل: از Number.isNaN(value) استفاده کنید.
اشتباه دوم: استفاده از isNaN سراسری برای بررسی NaN
isNaN("hello") مقدار true برمیگرداند، در حالی که "hello" NaN نیست. راهحل: از Number.isNaN استفاده کنید.
اشتباه سوم: فرض نوع داده از API
فرض اینکه فیلد عددی از API، همیشه عدد است، در بلندمدت به فاجعه میانجامد. راهحل: همیشه پاسخ API را اعتبارسنجی کنید.
اشتباه چهارم: نادیده گرفتن NaN در محاسبات میانی
اگر NaN در محاسبات میانی رخ دهد و بررسی نشود، در تمام محاسبات بعدی پخش میشود. راهحل: در نقاط حساس، بررسی صریح NaN داشته باشید.
اشتباه پنجم: استفاده از || برای مقادیر صفر معتبر
const price = parseFloat(input.value) || 10;
// اگر input.value = "0" باشد، price = 10 میشود (اشتباه!)
راهحل: از بررسی صریح یا Nullish Coalescing استفاده کنید:
const parsed = parseFloat(input.value);
const price = Number.isNaN(parsed) ? 10 : parsed;
اشتباه ششم: نبود تست برای مقادیر مرزی
اگر تستها فقط برای دادهی معتبر نوشته شوند، NaNها در production کشف میشوند. راهحل: برای هر تابع محاسباتی، تستهای مرزی بنویسید: null، undefined، رشتهی خالی، رشتهی غیرعددی، عدد منفی، و Infinity.
اشتباه هفتم: نبود لایهی اعتبارسنجی مرکزی
در پروژههای بزرگ، اعتبارسنجی پراکنده در سراسر کد، نگهداری را سخت میکند. راهحل: یک لایهی اعتبارسنجی مرکزی تعریف کنید که تمام ورودیهای برنامه را بررسی میکند.
NaN یک پیام از کیفیت داده است، نه فقط از عملیات ریاضی. اگر این سیگنال را جدی بگیرید و لایهی دادهی خود را تقویت کنید، پروژهی شما در ماههای بعد پایدارتر خواهد بود.
پرسشهای پرتکرار درباره خطای NaN در JavaScript
این پرسشها از دل تجربهی عملی و جلسات مشاوره جمعآوری شدهاند. پاسخ هر کدام بر اساس سناریوهای واقعی است.
تفاوت isNaN و Number.isNaN چیست؟
isNaN سراسری، ابتدا مقدار را با Number() به عدد تبدیل میکند، سپس بررسی میکند که آیا NaN است. بنابراین برای رشتههای غیرعددی مثل "hello" هم true برمیگرداند. Number.isNaN مقدار را تبدیل نمیکند و فقط بررسی میکند که آیا مقدار دقیقاً NaN است. در ۹۰ درصد موارد، Number.isNaN انتخاب درست است.
چرا NaN === NaN مقدار false برمیگرداند؟
این رفتار از استاندارد IEEE 754 میآید که NaN را بهعنوان یک مقدار «نامعین» تعریف میکند. اگر دو مقدار نامعین داشته باشیم، نمیتوان گفت برابر هستند، چون یکی از آنها ممکن است از یک عملیات متفاوت آمده باشد. برای بررسی NaN، از Number.isNaN یا Object.is استفاده کنید.
چرا typeof NaN === "number" است؟
NaN نمایندهی «عدد نامعتبر» است، بنابراین JavaScript آن را از نوع number در نظر میگیرد. اگر میخواهید اعداد معتبر را از NaN جدا کنید، از Number.isFinite استفاده کنید که هم NaN و هم Infinity را رد میکند.
چرا خطای NaN در production بیشتر دیده میشود؟
چون در محیط development، دادههای تست معمولاً معتبر هستند. در production، دادهی واقعی میتواند شامل رشتههای غیرعددی، مقادیر null، یا فیلدهای غایب باشد. راهحل: تست با دادههای مرزی و اعتبارسنجی صریح در لایهی ورودی.
آیا TypeScript میتواند از NaN جلوگیری کند؟
TypeScript با strictNullChecks و انواع صریح، بخش بزرگی از NaNها را در زمان compile تشخیص میدهد. ولی TypeScript نمیتواند همهی موارد را کشف کند، چون NaN یک مقدار runtime است. ترکیب TypeScript با تست و اعتبارسنجی runtime، بهترین رویکرد است.
چگونه از NaN در محاسبات مالی جلوگیری کنم؟
سه رویکرد: اول، اعتبارسنجی صریح تمام ورودیها. دوم، استفاده از توابع کمکی مثل safeArithmetic. سوم، استفاده از کتابخانههای محاسبات مالی که مدیریت NaN را بهعنوان بخشی از API خود دارند. مبانی کار با داده در کار با JSON در پروژههای واقعی آمده است.
آیا NaN روی performance تأثیر دارد؟
خود NaN از نظر performance هزینهی ناچیزی دارد. ولی وقتی NaN در محاسبات پخش میشود، میتواند منجر به خطاهای بیشتر و رندر مجدد در فریمورکها شود. راهحل: جلوگیری از تولید NaN در همان لایهی داده.
چگونه NaN را در آرایه تشخیص دهم؟
از Number.isNaN در ترکیب با متدهای آرایه استفاده کنید:
const arr = [1, NaN, 3];
const hasNaN = arr.some(Number.isNaN); // true
const filtered = arr.filter(n => !Number.isNaN(n)); // [1, 3]
چرا [NaN].includes(NaN) مقدار true برمیگرداند ولی [NaN].indexOf(NaN) مقدار -1؟
includes از الگوریتم SameValueZero استفاده میکند که NaN را با NaN برابر میداند. indexOf از === استفاده میکند که برای NaN مقدار false برمیگرداند. برای تشخیص NaN در آرایه، از includes یا findIndex با Number.isNaN استفاده کنید.
آیا NaN در JSON ذخیره میشود؟
نه. JSON.stringify(NaN) مقدار "null" برمیگرداند. یعنی اگر NaN را بهعنوان بخشی از یک شیء JSON ذخیره کنید، در JSON نهایی به null تبدیل میشود. راهحل: قبل از serialize، NaN را با یک مقدار جایگزین مشخص کنید.
چگونه NaN را به مقدار پیشفرض تبدیل کنم؟
function orDefault(value, defaultValue = 0) {
return Number.isNaN(value) ? defaultValue : value;
}
این تابع در سراسر پروژه قابل استفاده است.
آیا NaN در عملگرهای مقایسه رفتار خاصی دارد؟
بله. تمام مقایسههای عددی با NaN مقدار false برمیگردانند:
NaN < 5 // false
NaN > 5 // false
NaN <= 5 // false
NaN >= 5 // false
این رفتار در فیلتر و مرتبسازی آرایهها میتواند نتایج غیرمنتظره بدهد.
چگونه از NaN در React جلوگیری کنم؟
در React، مقادیر NaN در JSX بهعنوان NaN نمایش داده میشوند، که تجربهی کاربری بدی است. راهحل: قبل از render، بررسی صریح:
{Number.isFinite(total) ? total : "-"}
آیا NaN یک شیء است؟
نه. NaN یک مقدار اولیه از نوع number است، نه شیء. typeof NaN === "number" و NaN instanceof Number === false. این نکته در طراحی API و اعتبارسنجی مهم است.
چگونه در Vue، از NaN جلوگیری کنم؟
در Vue، از computed properties برای محاسبات استفاده کنید و در آنها مقادیر را اعتبارسنجی کنید:
computed: {
total() {
const price = Number(this.product.price);
if (!Number.isFinite(price)) return 0;
return price * this.quantity;
}
}
تفاوت NaN و Infinity چیست؟
NaN یعنی «عدد نامعتبر»، Infinity یعنی «بینهایت». هر دو از نوع number هستند. 1 / 0 مقدار Infinity میدهد، ولی 0 / 0 مقدار NaN میدهد. Number.isFinite هر دو را رد میکند.
آیا میتوانم از NaN بهعنوان کلید در Map استفاده کنم؟
بله. Map از الگوریتم SameValueZero استفاده میکند که NaN را با NaN برابر میداند:
const map = new Map();
map.set(NaN, "not a number");
console.log(map.get(NaN)); // "not a number"
آیا parseInt روی NaN، NaN برمیگرداند؟
بله. parseInt(NaN) مقدار NaN برمیگرداند، چون NaN قابل تبدیل به رشتهی معتبر عددی نیست. همین رفتار برای سایر توابع تبدیل عددی هم صادق است.
چگونه NaN را در Jest تست کنم؟
با toBeNaN یا Number.isNaN:
test("returns NaN for invalid input", () => {
expect(parseInt("hello")).toBeNaN();
});
چرا NaN.toString() مقدار "NaN" برمیگرداند؟
این یک ویژگی طراحی است که به تشخیص در لاگها کمک میکند. اگر مقدار NaN در لاگ نمایش داده شود، بهعنوان "NaN" نمایش داده میشود که بهراحتی از سایر مقادیر قابل تشخیص است. ولی در محاسبات JSON، به null تبدیل میشود.
آیا NaN در محاسبات بین JavaScript و Python یکسان است؟
مفهوم NaN در هر دو زبان یکسان است، ولی رفتار متفاوتی دارند. در Python، int("hello") خطای ValueError میدهد، در حالی که در JavaScript، parseInt("hello") بیصدا NaN برمیگرداند. اگر با Python هم کار میکنید، مباحث خطای ValueError در پایتون تفاوتها را بهطور کامل باز کرده است.
چگونه در Node.js، NaN را در سرور تشخیص دهم؟
در Node.js، از توابع کمکی و لاگ ساختارمند استفاده کنید:
function validateNumber(value, context) {
if (Number.isNaN(value)) {
const error = new Error(`NaN detected in ${context}`);
error.statusCode = 500;
throw error;
}
return value;
}
این رویکرد، در APIهای Node.js، بخش بزرگی از NaNهای پنهان را افشا میکند.
آیا Object.is(NaN, NaN) تفاوتی با Number.isNaN(NaN) دارد؟
Object.is(NaN, NaN) مقدار true برمیگرداند، ولی این تابع دو آرگومان میگیرد و برای مقایسه استفاده میشود. Number.isNaN(NaN) یک آرگومان میگیرد و برای بررسی مقدار استفاده میشود. برای بررسی NaN یک مقدار، Number.isNaN انتخاب درست است.
آیا NaN در Web Workers رفتار متفاوتی دارد؟
نه. NaN در تمام محیطهای JavaScript (مرورگر، Node.js، Web Workers، Service Workers) رفتار یکسانی دارد. تفاوتها ممکن است در انتقال NaN بین محیطها باشد، چون در postMessage، NaN بهعنوان یک مقدار معتبر منتقل میشود.
آنچه از سالها کار با NaN در JavaScript آموختم
اگر بخواهم چکیدهی این سالها را در چند جمله بگویم، سه اصل عملی دارم:
یک: NaN یک شکایت از کیفیت داده است، نه از عملیات ریاضی. هر بار که NaN میبینید، بهجای سرزنش کد، به کیفیت دادهی ورودی نگاه کنید. در ۹۰ درصد موارد، مشکل در لایهی ورودی است، نه در محاسبه.
دو: اعتبارسنجی صریح، سرمایهگذاری بلندمدت است. هر تابعی که با اعداد کار میکند، باید ورودی خود را اعتبارسنجی کند. این عادت، شما را از بخش بزرگی از NaNها نجات میدهد. هزینهی چند خط کد اعتبارسنجی، در بلندمدت چند برابر برمیگردد.
سه: TypeScript، ابزار موثر پیشگیری است. در پروژههای جدی، TypeScript با strictNullChecks بخش بزرگی از NaNها را قبل از اجرا تشخیص میدهد. این رویکرد، در پروژههای تیمی، تفاوت جدی ایجاد میکند.
در کنار این سه اصل، یک هشدار عملی هم دارم: NaN در نگاه اول یک مشکل ساده بهنظر میرسد، ولی وقتی در چارچوب کلی معماری داده دیده شود، تبدیل به یک سیگنال میشود. این سیگنال میگوید که مدل دادهی شما نیاز به بازنگری دارد. اگر این سیگنال را جدی بگیرید و ساختار داده را بهبود دهید، پروژهی شما در ماههای بعد پایدارتر و قابل نگهداریتر خواهد بود. تجربهی من میگوید که پروژههایی که NaN را بهعنوان یک سیگنال جدی میگیرند، در نهایت به معماری دادهی قویتری میرسند.
هدف این مقاله، تمامکردن همهی سناریوهای ممکن نبود. هدف، دادن یک چارچوب ذهنی برای تشخیص، پیشگیری و رفع این خطا بود. وقتی این چارچوب را درونی کنید، برخورد با NaN از یک واکنش اضطراری به یک فرآیند منظم تبدیل میشود. و این، همان تفاوتی است که بین توسعهدهندهی معمولی و توسعهدهندهای که به کدش اعتماد دارد، وجود دارد.
اگر NaN در پروژهی شما به شکلی ظاهر شده که با الگوهای این مقاله حل نشده، برای من جالب است بدانم کدام سناریو بود. تجربهی خودتان را در دیدگاهها بنویسید؛ بهویژه اگر راهحلی پیدا کردهاید که هنوز در این مقاله نیست. 🔢