کار با اعداد و عملگرهای محاسباتی
استفاده از اعداد و انجام اعمال محاسباتی بر روی آنها از معمولترین کارها در برنامهنویسی است. در این بخش قصد داریم مهمترین مباحث مربوط به اعداد در جاوا اسکریپت را مورد بررسی قرار دهیم.
به قطعه کد زیر و خروجی آن توجه کنید.
let num1 = 3330000;
num1.toExponential();
← "3.33e+6"
همانطور که میبینید، متد toExponential مقدار ذخیره شده در متغیر num1 را به حالت نمایی همین عدد تبدیل کرده و بازمیگرداند. همچنین دقت کنید که مقدار بازگردانده شده یک رشته است، نه یک عدد. زیرا همانطور که قبلاً اشاره شده است، اعداد همیشه به صورت دهدهی در خروجی نمایش داده میشوند. و برای نمایش آنها به اشکال دیگر باید آنها را به رشته تبدیل کنیم.
اما نکته مهم دیگری که در کد فوق باید به آن توجه شود، نحوهی استفاده از متغیر num1 است. پیشتر اشاره شد که دادهها در جاوا اسکریپت به دو دستهی اصلی تقسیم میشوند. دادههای اولیه (Primitive Data Types) و اشیاء (Objects). و هر یک از این دستهها نیز به دستههای کوچکتری تقسیم میشوند که در مورد دستهی اول قبلاً توضیحات لازم داده شده، و در مورد دستهی دوم بعداً بیشتر صحبت خواهیم کرد.
میدانیم که اعداد در دستهی اول، یعنی انواع دادهی اولیه قرار دارند. همچنین میدانیم که هر متد، تابعی است که به یک شئ خاص تعلق دارد. اما همانطور که در قطعه کد فوق میبینید. متغیر عددی num1 به صورت یک شئ رفتار کرده و دارای متدی به نام toExponential است. بنابراین باید متغیر num1 را یک شئ تلقی کنیم. چرا که دارای یک متد است. و این در تناقض با گفتههای پیشین است که متغیرهای عددی را از انواع دادهی اولیه معرفی میکرد.
Wrapper Object
در واقع هیچ تناقضی در تعاریف قبلی وجود ندارد. کلید حل مسئله در مفهومی است به نام Wrapper Object. در جاوا اسکریپت (و بسیاری زبانهای دیگر)، تمام دادههای اولیه، به صورت خودکار داخل یک شئ قرار میگیرند که اصطلاحاً این شئ را Wrapper Object یا شئ محفظه یا شئ در بر گیرنده مینامند. همانطور که قبلاً دیدیم اشیاء میتوانند متدهایی داشته باشند که اعمالی را بر روی خاصیتهای آنها انجام دهند.
در اینجا نیز شیئی که به عنوان Wrapper Object این متغیر عددی را در بر میگیرد، دارای تعدادی متد است که میتوانند اعمالی را بر روی این عدد (که حالا یک خاصیت از همین شئ است) انجام دهند. یکی از این اعمال، تبدیل آن عدد به شکل نمایی است که با استفاده از متد toExponential انجام میشود. متدهای دیگری را نیز در ادامه خواهیم دید.
نام شیئی که به عنوان Wrapper Object متغیرهای عددی را در بر میگیرد، Number است. این همان شیئی است که قبلاً هم از آن استفاده کردهایم و یکی از متدهای آن، یعنی isInteger را قبلاً به کار بردهایم. Wrapper Object ها با توجه به نوع دادهی اولیه متفاوت هستند. مثلاً نام شئ در بر گیرندهی انواع رشتهای، String است که در بخش بعدی با آن آشنا خواهیم شد.
پس هر یک از انواع دادهی اولیه دارای تعدادی متد نیز هستند. در مورد دادههای عددی متد toExponential را دیدیم. متد دیگری که بر روی دادههای عددی به کار برده میشود متد toFixed است. این متد عدد x را به عنوان ورودی دریافت میکند، سپس مقدار ذخیره شده در متغیر عددی را تا x رقم اعشار رند میکند. در قطعه کد زیر نتیجهی اجرای این متد بر روی مقادیر عددی مختلف را میبینید.
let num1 = 23.45675;
num1.toFixed(3);
← "23.457"
const num2 = 11.23
num2.toFixed(4);
← "11.2300"
همانطور که میبینید در صورتی که تعداد ارقام اعشاری در عدد اولیه، کمتر از عدد تعیین شده در متد toFixed باشد. به تعداد لازم، رقم صفر به انتهای مقدار خروجی اضافه میشود. ضمن اینکه باز هم باید توجه کنید که خروجی این متد نیز یک رشته است نه یک عدد. همچنین توجه کنید که مقدار ذخیره شده در متغیر یا ثابت اصلی (num1 و num2) هیچ تغییری نمیکند و این متغیرها مقدار قبلی خود را حفظ میکنند.
متد بعدی toPrecision است. این متد نیز عدد x را به عنوان ورودی دریافت کرده و تعداد ارقام معنیدار عدد اولیه را به x رقم تغییر میدهد و در صورت نیاز خروجی را به شکل نمایی تبدیل میکند و یا تعدادی صفر به انتهای آن اضافه میکند. قطعه کد زیر نیز نتیجهی اجرای این متد را در ۳ حالت مختلف نمایش میدهد.
let num1 = 23.45675;
num1.toPrecision(3);
← "23.5"
const num2 = 2345675;
num2.toPrecision(3);
← "2.35e+6"
const num3 = 2345;
num3.toPrecision(6);
← "2345.00"
اما نکتهی مهم دیگر در استفاده از Wrapper Object ها و متدهای آنها این است که برای استفاده از آنها نیازی به تعریف و مقدار دهی یک متغیر یا ثابت نیست. و مستقیماً میتوان از این متدها بر روی مقادیر لفظی (literal) استفاده کرد. مثلا فرض کنید قصد داریم عدد ۳۳۲۰۰۰۰۰ را با استفاده از متد toExponential به شکل نمایی تبدیل کنیم. در این صورت میتوان به جای قرار دادن این مقدار در یک متغیر و استفاده از نام آن متغیر قبل از متد toExponential، دقیقاً مقدار عددی مورد نظر را قبل از نام متد قرار داد. این در رابطه با انواع دیگر دادهها (مانند دادههای رشتهای یا دودویی) نیز صادق است. اما در مورد دادههای عددی یک نکتهی مهم را باید در نظر داشته باشید. به کد زیر توجه کنید.
33200000.toExponential();
← Uncaught SyntaxError: Invalid or unexpected token
همانطور که میبینید این دستور یک خطا تولید میکند. دلیل بروز خطا این است که کاراکتر نقطه در اعداد معنی خاصی دارد و برای جدا کردن بخش صحیح از بخش اعشاری اعداد به کار برده میشود. به همین دلیل در کد فوق، متد toExponential به عنوان بخش اعشاری عدد در نظر گرفته میشود که یک مقدار نامعتبر است. در نتیجه با خطا مواجه میشویم. برای حل این مشکل در دادههای عددی چندین روش وجود دارد که میتوانید یکی از آنها را برای حل مشکل به کار ببرید.
۱- استفاده از ۲ کاراکتر نقطه به جای یک نقطه
33200000..toExponential();
← "3.32e+7"
۲- قرار دادن یک فاصله بین عدد و نقطه
33200000 .toExponential();
← "3.32e+7"
۳- نوشتن اعداد با اعشار کامل (حتی اگر عدد صحیح باشد)
33200000.0.toExponential();
← "3.32e+7"
3.1415.toFixed(2);
← "3.14"
۴- قرار دادن عدد در پرانتز
(33200000).toExponential();
← "3.32e+7"
توجه کنید که برای متغیرهای غیر عددی نیازی به استفاده از روشهای فوق نیست و استفاده از یک نقطه کافی است.
عملگرهای ریاضی و محاسباتی
قبلاً اشارهی کوتاهی به مفهوم عملگر (Operator) کرده بودیم و عملگر typeof را نیز معرفی کرده بودیم. در هر زبان برنامهنویسی دهها عملگر مختلف جهت انجام انواع محاسبات، تبدیلها و ... وجود دارد. در این بخش قصد داریم در مورد دستهای از این عملگرها، یعنی عملگرهای ریاضی و محاسباتی صحبت کنیم.
انجام محاسبات ریاضی در برنامهنویسی بسیار معمول است. لذا در تمام زبانهای برنامهنویسی تعدادی عملگر برای انجام این امور در نظر گرفته شده است. سادهترین عملگرهای محاسباتی برای انجام ۴ عمل اصلی (جمع، تفریق، ضرب و تقسیم) به کار برده میشوند. در قطعه کد زیر نمونههایی از کاربرد این عملگرها را میبینید.
let num1 = 10 , num2 = 20 , num3 = 2.5 , num4 = 5 , answer;
answer = num1 + num2; // answer = 30
answer = num1 - num2; // answer = -10
answer = num2 * num3; // answer = 50
answer = answer / num4; // answer = 10
همچنین میتوان از عملگر "%" جهت محاسبهی باقی مانده، و از عملگر "**" جهت به توان رساندن استفاده کرد. مثالهایی از این عملگرها را در قطعه کد زیر میبیند.
let answer;
answer = 10 % 3; // answer = 1
answer = 10 ** 3; // answer = 1000
همچنین هر ۶ عملگر فوق دارای حالت ساده شدهای هستند که امکان خلاصهنویسی را فراهم میکنند. این عملگرها و معنی هر یک را در قطعه کد زیر میبینید.
let num1;
num1 += 2; // num1 = num1 + 2
num1 -= 3; // num1 = num1 - 3
num1 *= 5; // num1 = num1 * 5
num1 /= 2; // num1 = num1 / 5
num1 %= 4; // num1 = num1 % 4
num1 **= 3; // num1 = num1 ** 3
برای حالتهای خاصی که هدفمان اضافه کردن یک واحد به یک متغیر، یا کاهش یک واحد از آن باشد، دو عملگر سادهتر نیز وجود دارد که در قطعه کد زیر مشاهده میکنید.
let num1 = 5;
num1++; // after execution : num1 = num1 + 1 = 6
num1--; // after execution : num1 = num1 - 1 = 5
--num1; // after execution : num1 = num1 - 1 = 4
++num1; // after execution : num1 = num1 + 1 = 5
عملگرهای "++" و "--
" بر خلاف موارد قبلی، عملگر یگانی (Unary Operator) هستند. یعنی فقط یک عملوند دارند که مقدار آن عملوند را به ترتیب یک واحد افزایش یا کاهش میدهند. اما تمام عملگرهای قبلی، عملگر دودویی (Binary) بودند. یعنی یک عملوند در سمت چپ، و یک عملوند در سمت راست آنها قرار میگیرد. همچنین عملگر typeof که قبلاً با آن آشنا شده بودیم نیز یک عملگر یگانی است. چرا که فقط یک عملوند دارد.
اما اگر به کد فوق توجه کنید میبینید که هر یک از عملگرهای "++" و "--
" به دو شکل به کار رفتهاند. اگر این عملگرها قبل از عملوند خود قرار گیرند. به ترتیب به آنها پیشافزایش و پیشکاهش گفته میشود. اما اگر بعد از عملوند خود قرار گیرند. به ترتیب پسافزایش و پسکاهش نامیده میشوند. اما آیا تفاوتی میان این دو حالت وجود دارد؟ در قطعه کد فوق هیچ تفاوتی در نتیجهی نهایی بین دو حالت ذکر شده وجود ندارد. اما در شرایط پیچیدهتر این دو حالت با هم متفاوت هستند.
در صورتی که این عملگرها در یک عبارت محاسباتی به کار برده شوند، آنگاه پیشافزایش و پیشکاهش قبل از ارزیابی عبارت محاسباتی عمل میکنند. اما پسافزایش و پسکاهش، پس از ارزیابی عبارت محاسباتی عمل میکنند. یعنی در حالت دوم، این عملگرها تاثیری در ارزیابی کل عبارت محاسباتی ندارند و فقط مقدار عملوند خود را تغییر میدهند. یعنی این تغییر در ارزیابی کل عبارت به حساب نیامده و از مقدار قبلی عملوندها در محاسبه استفاده میشود. مثال زیر این مفهوم را بهتر روشن میکند.
let a = 4 , b = 5 , answer;
answer = a++ * b++; // after execution : answer = 4 * 5 = 20, a = 5 , b = 6
answer = ++a * ++b; // after execution : answer = 6 * 7 = 42, a = 6 , b = 7
answer = a++ * --b; // after execution : answer = 6 * 6 = 36, a = 7 , b = 6
بهتر است دستورات فوق (و چند دستور مشابه) را خودتان چند بار اجرا کنید تا تفاوت بین دو حالت ذکر شده را دقیقاً متوجه شوید.
مقادیر ویژهی Infinity و NaN
نتیجهی ارزیابی یک عبارت محاسباتی، همیشه به صورت یک عدد حقیقی محدود (کراندار) قابل بیان نیست. به عنوان مثال میدانیم که تقسیم یک عدد بر صفر، در ریاضیات بینهایت ارزیابی میشود و با نماد "∞" نمایش داده میشود. در برخی زبانهای برنامهنویسی چنین محاسبهای امکانپذیر نیست و یک خطا تولید میکند. اما در جاوا اسکریپت تقسیم یک عدد بر صفر، خطایی تولید نمیکند. بلکه مقدار بینهایت را تولید میکند که این مقدار در جاوا اسکریپت با مقدار ویژهی Infinity نمایش داده میشود. به طور کلی نتیجهی هر محاسبهای اگر بزرگتر از بزرگترین عدد قابل نمایش در جاوا اسکریپت باشد، بینهایت یا Infinity خواهد بود. بزرگترین عدد قابل نمایش در جاوا اسکریپت نیز عدد "1.7976931348623157e+308" میباشد و هر مقداری بزرگتر از این (مثبت یا منفی) به عنوان بینهایت تلقی میشود. قطعه کد زیر چند نمونه از محاسبات منجر به تولید مقدار بینهایت را نشان میدهد.
2 / 0;
← Infinity
-2 / 0;
← -Infinity
2e444;
← Infinity
همچنین اگر نتیجهی یک عبارت محاسباتی، به صورت ریاضی قابل بیان نباشد و منجر به یک عدد (محدود یا بینهایت) نشود، نتیجهی آن عبارت، NaN خواهد بود که مخفف Not a Number است. مثلاً اگر یک عدد را در یک رشته ضرب کنید، نتیجه NaN خواهد بود. به قطعه کد زیر توجه کنید.
2 * 'Hello';
← NaN
3 - 'JavaScript';
← NaN
همچنین توجه کنید که هر دو مقدار Infinity و NaN از نظر نوع داده، در دستهی دادههای عددی قرار میگیرند. لذا استفاده از عملگر typeof برای این مقادیر، مقدار "number" را بازمیگرداند.
typeof Infinity;
← "number"
typeof NaN;
← "number"
همچنین با استفاده از متدهای isFinite و isNaN که متعلق به شئ Number هستند میتوان تشخیص داد که مقدار یک متغیر عددی یا نتیجهی یک عبارت محاسباتی Infinity یا NaN هست یا خیر؟ متد isFinite در صورت بینهایت بودن مقدار ورودی، مقدار false و در غیر این صورت مقدار true را بازمیگرداند. متد isNaN نیز در صورتی که مقدار ورودی NaN باشد مقدار true و در غیر این صورت مقدار false را بازمیگرداند. قطعه کد زیر چند نمونه از اجرای این متدها را نشان میدهد.
Number.isFinite(1/0);
← false
Number.isFinite(-Infinity);
← false
Number.isFinite(NaN);
← false
Number.isFinite(42);
← true
Number.isNaN(NaN);
← true
Number.isNaN(44);
← false
Number.isNaN(Infinity);
← false
Number.isNaN(4 - 'HTML');
← true
در این بخش جزئیات بیشتری در رابطه با متغیرهای عددی و رفتار آنها و نحوهی انجام محاسبات ریاضی بر روی آنها مطرح شد. در بخش بعدی نیز جزئیات بیشتری در رابطه با متغیرهای رشتهای ارائه خواهد شد.