۱۲ اکتبر ۲۰۲۲

اعداد

در جاوااسکریپت، دو نوع عدد وجود دارد:

  1. اعداد معمولی در جاوااسکریپت با فرمت 64 بیتی IEEE-754 ذخیره می‌شوند، همچنین با “اعداد اعشاری با دقت یک صدم” هم شناخته می‌شوند. ما اکثر اوقات از این اعداد استفاده می‌کنیم، و درباره آنها در این فصل صحبت خواهیم کرد.

  2. اعداد BigInt، برای نمایش اعدادی با طول دلخواه استفاده می‌شوند. آنها بعضی اوقات مورد نیاز هستند، چون اعداد معمولی نمی‌توانند از 253 بیشتر یا از -253 کمتر باشند. چون bigintها در چند حوزه خاص استفاده می‌شوند، ما به آنها یک فصل خاص BigInt اختصاص می‌دهیم.

پس اینجا درباره اعداد معمولی صحبت می‌کنیم. بیایید دانش‌مان درباره آنها را گسترش دهیم.

راه‌های دیگر نوشتن یک عدد

فرض کنید نیاز داریم بنویسیم یک میلیارد. راه واضح این است:

let billion = 1000000000;

همچنین ما می‌توانیم از خط تیره _ به عنوان جداکننده استفاده کنیم:

let billion = 1_000_000_000;

اینجا خط تیره _ نقش “syntactic sugar” را بازی می‌کند، و عدد را خواناتر می‌کند. موتور جاوااسکریپت به راحتی _ بین ارقام را نادیده می‌گیرد، پس دقیقا عددی مانند یک میلیارد بالا است.

اما در زندگی واقعی ما عموما از نوشتن رشته حرف با تعداد زیاد صفر خودداری میکنیم به خاطر اینکه به راحتی ممکن است خطا داشته باشیم و اشتباه بنویسیم. همینطور، ما تنبل هستیم! ما معمولا یه چیزی شبیه “1bn” مینویسیم به جای یک میلیارد یا “7.3bn” برای هفت میلیارد و سیصد میلیون. این حقیقت برای اکثر اعداد بزرگ هم صحیح است.

در جاوااسکریپت، ما یک عدد را براساس اضافه کردن حرف "e" به انتهای آن خلاصه می‌کنیم و با آن تعداد صفرها مشخص می‌شود.

let billion = 1e9;  // 1 billion, literally: 1 and 9 zeroes

alert( 7.3e9 );  // 7.3 billions (same as 7300000000 or 7_300_000_000)

به زبانی دیگر، "e" عدد را در 1 با تعداد صفر داده شده ضرب می‌کند.

1e3 === 1 * 1000; // e3 یعنی *1000
1.23e6 === 1.23 * 1000000; // e6 یعنی *1000000

حالا بگذارید مقداری خیلی کوچک بنویسیم. مثلا؛ یک میکروثانیه، (یک میلیونیوم ثانیه):

let mсs = 0.000001;

دقیقا مثل قبل، استفاده از "e" می‌تواند کمک کند. اگر ما بخواهیم که از نوشتن صفرها خودداری کنیم، می‌توانیم بنویسیم:

let mcs = 1e-6; // پنج صفر در سمت چپ ۱

اگر ما تعداد صفرهای 0.000001 را بشماریم، شش تا از آنها موجودست. بنابراین طبعا می‌شود 1e-6.

به زبانی دیگر، یک عدد منفی بعد "e"، به معنی تقسیم بر یک با تعداد صفرهای داده شده است.

// -3 divides by 1 with 3 zeroes
1e-3 === 1 / 1000; // 0.001

// -6 divides by 1 with 6 zeroes
1.23e-6 === 1.23 / 1000000; // 0.00000123

// مثالی با یک عدد بزرگتر
1234e-2 === 1234 / 100; // ممیز دو بار حرکت می‌کند، 12.34

اعداد پایه ۱۶، دودویی، پایه ۸

اعداد پایه ۱۶ برای نمایش رنگ‌ها، کدگذاری حروف و بسیاری دیگر، به طور وسیعی در جاوااسکریپت مورد استفاده قرار می‌گیرند. بنابراین، یک راه کوتاهتری برای نوشت آنها وجود دارد: 0x و سپس عدد.

به عنوان مثال:

alert( 0xff ); // 255
alert( 0xFF ); // 255 (the same, case doesn't matter)

اعداد هشت‌تایی و دودویی به ندرت مورد استفاده قرار میگیرند اما نحوه‌ی استفاده‌ی آنها به صورت پیشوند‌های 0b و 0o است:

let a = 0b11111111; // binary form of 255
let b = 0o377; // octal form of 255

alert( a == b ); // true, the same number 255 at both sides

سه نوع سیستم عددی با پشتیبانی مخصوص به خود وجود دارد. برای بقیه‌ی سیستم‌ها ما باید از تابع parseInt استفاده کنیم. (که ما در بخش‌های بعدی خواهیم دید.)

toString(base)

تابع num.toString(base)، یک رشته حرف نمایشگر num را در سیستم عددی با پایه داده شده خروجی می‌دهد.

به عنوان مثال:

let num = 255;

alert( num.toString(16) );  // ff
alert( num.toString(2) );   // 11111111

پایه میتواند از بین ۲ تا ۳۶ تغییر کند. در حالت عادی، ۱۰ است.

حالات معمول استفاده بدین شکلند:

  • base=16 برای رنگ‌های هگزایی، کدگذاری حروف و غیره، ارقام میتوانند به صورت 0..9 یا A..F باشند.

  • base=2 برای دیباگ کردن عملوند‌های بیتی هستند، ارقام میتوانند 0 یا 1 باشند.

  • base=36 بزرگترین است، ارقام میتوانند 0..9 یا A..Z باشند. کل الفبای لاتین برای نمایش یک عدد مورد استفاده قرار میگیرد. یک حالت خنده دار، اما مفید برای ۳۶ وقتی است که ما نیاز داریم تا یک نشانگر بزرگ عددی را به چیز کوچکتری تبدیل کنیم، به عنوان مثال برای . ساختن لینک‌ های کوتاه شده (short url). میتوان به سادگی اعداد را در پایه‌ی ۳۶ نمایش داد:

    alert( 123456..toString(36) ); // 2n9c
دو نقطه برای صدا زدن یک تابع

توجه داشته باشید که دو نقطه در 123456..toString(36)، غلط املایی نیست. اگر میخواهیم یک تابعی را مستقیما روی عدد صدا بزنیم مثل toString در مثال بالا، آنگاه ما نیاز داریم تا بعد از آن دوتا نقطه بگذاریم.

اگر ما یک نقطه بگذاریم: 123456.toString(36)، آنگاه خطایی به وجود می‌آید، چراکه قواعد نوشتاری جاوااسکریپت بعد از یک نقطه، آن را قسمت اعشاری آن در نظر میگیرد. و اگر یک نقطه بیشتر بگذاریم، جاوااسکریپت فرض میکند قسمت اعشاری خالی‌ست و سپس تابع فراخوانده میشود.

همچنین میتوانیم بنویسیم (123456).toString(36):

رند کردن

یکی از عملگرهایی در اعداد زیاد مورد استفاده قرار میگیرد، رند کردن است.

چندین تابع از پیش آماده شده برای رند کردن به شرح زیر است:

Math.floor
رند پایین: ۳.۱ میشود ۳, و -۱.۱ میشود .
Math.ceil
رند بالا: ۳.۱ میشود ۴, و -۱.۱ میشود .
Math.round
رند کردن به نزدیک ترین عدد صحیح: ۳.۱ میشود ۳, ۳.۶ میشود ۴ و -۱.۱ میشود .
Math.trunc (توسط اینترنت اکسپلورر ساپورت نمیشود)
حذف کردن قسمت اعشاری بدون رند کردن: ۳.۱ میشود ۳, -۱.۱ میشود .

این جدول تفاوت بین ‌آنهارا خلاصه کرده است:

Math.floor Math.ceil Math.round Math.trunc
۳.۱ ۳ ۴ ۳ ۳
۳.۶ ۳ ۴ ۴ ۳
-۱.۱
-۱.۶

این توابع تمام حالات کار با قسمت اعشاری یک عدد را پوشش میدهند. اما چطور میتوانیم عدد را تا رقم n-ام بعد از اعشار رند کنیم؟

برای مثلا، داریم ۱.۲۳۴۵ و میخواهیم تا ۲ رقم اعشار آن را رند کنیم یعنی ۱.۲۳

دو روش برای اینکار داریم:

۱. ضرب و تقسیم

برای مثال، برای گرد کردن عدد تا دومین رقم اعشاری، می‌توانیم عدد را در`۱۰۰` ضرب کنیم، تابع رند کردن را صدا بزنیم و سپس دوباره تقسیم کنیم.
```js run
let num = 1.23456;

alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
```

۲. تابع toFixed(n) عدد را تا رقم n-ام بعد اعشار رند میکند و سپس آن را به صورت رشته حرفی حرفی خروجی میدهد.

```js run
let num = 12.34;
alert( num.toFixed(1) ); // "12.3"
```

این عدد را به نزدیک ترین مقدار رند میکند مشابه `Math.round`:

```js run
let num = 12.36;
alert( num.toFixed(1) ); // "12.4"
```

توجه داشته باشید که مقدار `toFixed` یک رشته است. اگر قسمت بعد اعشار کوچک‌تر از آنجه نیاز است باشد، صفر به آخر آن اضافه خواهد شد:

```js run
let num = 12.34;
alert( num.toFixed(5) ); // "12.34000", صفر اضافه شده تا دقیقا ۵ رقم شود 
```

ما می‌توانیم آن را با کمک عملگر جمع یگانه یا با فراخوانی `Number()` تبدیل کنیم؛ برای مثال بنویسیم  `+num.toFixed(5)`.

محاسبات تقریبی

در درون سیستم، یک عدد به شکل ۶۴-بیتی است IEEE-754، بنابراین دقیقا ۶۴ بیت برای ذخیره‌ی یک عدد داریم: ۵۲ تا از آنها برای ذخیره کردن ارقام هستند، ۱۱ تا از آنها برای ذخیره کردن جایگاه نقطه‌ی مشخص کننده‌ی اعشار (که برای اعداد صحیح صفر است)، و یک بیت برای علامت آن.

اگر عددی بیش از حد بزرگ باشد، حافظه‌ی ۶۴ بیتی سرریز می‌شود و به مقدار بی‌نهایت تبدیل می‌شود:

alert( 1e500 ); // Infinity

اتفاقی که زیاد مشخص نیست و کمتر اتفاق می‌افتد، از دست دادن دقت است.

این آزمون (اشتباه!)برابری را در نظر بگیرید:

alert( 0.1 + 0.2 == 0.3 ); // false

این درست است، اگر ما بررسی کنیم که جمع 0.1 و 0.2، 0.3 است، ما مقدار false را در جواب میگیریم.

عجیب است! چی هست پس اگر 0.3 نیست؟!

alert( 0.1 + 0.2 ); // 0.30000000000000004

اوه! فرض کنید شما در حال ساخت فروشگاهی اینترنتی هستید و بازدیدکننده کالاهایی به مبلغ $0.10 و $0.20 را به سبد خرید خود اضافه می‌کند. مجموع هزینه سفارش $0.30000000000000004 خواهد بود. این هر شخصی را شکه می‌کند!

اما چرا این اتفاق می‌افتد؟

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

مقدار 0.1 چیست؟ مقدار یک که بر ده تقسیم شده، 1/10 یعنی یک دهم. در سیستم اعداد اعشاری چنین اعدادی به سادگی قابل نمایش هستند. آن را با یک سوم 1/3 مقایسه کنید. به کسری بی‌پایان تبدیل می‌شود 0.33333(3).

بنابراین، تقسیم های از توان ده، قطعا در سیستم اعشاری کار میکند اما تقسیم‌های بر ۳ اینطور نیست. به علت مشابه، در سیستم اعداد دودویی، تقسیم توان‌های ۲ هم قطعا کار میکند اما ۱/۱۰، کسر دودویی بی‌پایانی میشود.

در حقیقت هیچ راهی برای ذخیره کردن دقیقا ۰.۱ یا دقیقا ۰.۲ در سیستم دودویی وجود ندارد، دقیقا مثل اینکه برای ذخیره سازی مقدار یک سوم به عنوان یک کسر اعشاری راهی وجود ندارد.

فرمت عددی IEEE-754، این مسأله را با کمک رند کردن به نزدیک‌ترین عدد ممکن حل می‌کند. این قوانین رند کردن عموما نمیگذارند که ما آن مقدار کوچک دقت گم شده را متوجه بشویم، بنابراین عدد به شکل 0.3 خواهد بود. اما آگاه باشید که این از دست دادن دقت هنوز وجود دارد.

میتوانیم این را در عمل هم ببینیم:

alert( 0.1.toFixed(20) ); // 0.10000000000000000555

و هنگامی که ما دو عدد را جمع میکنیم، مقدار از دست دادگی دقت آنها با هم جمع میشود.

به همین علت است که 0.1 + 0.2، دقیقا 0.3 نمیشود.

نه تنها جاوااسکریپت

این مشکل در دیگر زبان‌های برنامه‌نویسی بسیاری وجود دارد.

PHP، Java، C، Perl، Ruby دقیقا نتیجه‌ی مشابه را میدهند چراکه بر پایه‌ی فرمت عددی یکسانی بنا شده‌‌اند.

آیا ما میتوانیم راهی برای حل این مسأله پیدا کنیم؟ طبعا، قابل اطمینان ترین راه حل این است که نتیجه را با کمک متد toFixed(n) رند کنیم:

let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // "0.30"

توجه داشته باشید که toFixed همیشه مقدار رشته حرفی برمیگرداند. این تابع حتما مطمئن میشود که تا دو رقم اعشار را حساب می‌کند. که البته این روش منطقی‌‌ست وقتی مثلا در یک فروشگاه اینترنتی ما نیاز داریم مقدار $0.30 را نمایش دهیم. برای حالات دیگر، میتوانیم از جمع واحد استفاده کنیم تا آنرا به یک عدد تبدیل کنیم:

let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3

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

alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001

پس روش ضرب/تقسیم ارور را کاهش می‌دهد، اما آن را به طور کامل ازبین نمی‌برد.

گاهی اوقات می‌توانیم به طور کلی از کسرها فرار کنیم. برای مثال اگر ما با یک فروشگاه سر و کار داریم، می‌توانیم قیمت‌ها را به جای دلار به صورت سنت (cent) ذخیره کنیم. اما اگر یک تخفیف 30 درصدی را اعمال کنیم چه؟ در عمل، فرار کردن از کسرها به ندرت ممکن است. زمانی که نیاز شد فقط آنها را رند کنید تا “دم‌شان” را ببرید.

یک چیز خنده‌دار!

اجرا کنید:

// Hello! I'm a self-increasing number!
alert( 9999999999999999 ); // shows 10000000000000000

این همان مشکل قبلی‌ست: از دست دادن دقت. برای عدد ۶۴ بیت وجود دارد، ۵۲ تا از آنها برای ذخیره‌ ارقام است اما این کافی نیست. پس کم اهمیت‌ترین ارقام ناپدید میشوند.

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

دو صفر

یک اتفاق جالب دیگر که در نمایش دادن اعداد درون سیستم می‌افتد وجود دو نوع صفر میباشد! 0 و -0.

این به این علت است که علامت با یک بیت مشخص میشود بنابراین هر عدد میتواند مثبت یا منفی باشد، حتی صفر.

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

آزمون‌ها: isFinite و isNaN

آیا این دو مقدار عددی خاص را به یاد دارید؟

  • Infinity-Infinity) یک مقدار عددی خاص هستند بزرگتر (کوچکتر) از هرچیزی.
  • NaN یک خطا را نشان می‌دهد.

آنها به مدل number مربوطند اما اعداد معمولی نیستند، پس توابع خاصی برای بررسی آنها وجود دارد.

  • isNaN(value) آرگومان‌هایش را به یک عدد تبدیل میکند و سپس آن را برای NaN بودن می‌آزماید:

    alert( isNaN(NaN) ); // true
    alert( isNaN("str") ); // true

    اما آیا ما به این تابع نیاز داریم؟ نمیتوانیم صرفا از تساوی === NaN استفاده کنیم؟‌ متاسفانه جواب خیر است. مقدار NaN، مقداری یکتاست و با هیچ چیز، حتی خودش برابر نیست.

    alert( NaN === NaN ); // false
  • isFinite(value) آرگومان‌هایش را به عدد تبدیل میکند و در صورتی که عددی معمولی باشد true خروجی می‌دهد نه NaN/Infinity/-Infinity:

    alert( isFinite("15") ); // true
    alert( isFinite("str") ); // false, because a special value: NaN
    alert( isFinite(Infinity) ); // false, because a special value: Infinity

بعضی اوقات isFinite برای صحت سنجی اینکه یک رشته حرفی، عددی معمولیست به کار برده می‌شود.

let num = +prompt("Enter a number", '');

// صحیح برگردانده میشود مگر اینکه وارد کنید Infinity, -Infinity یا چیزی غیر از عدد
alert( isFinite(num) );

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

Number.isNaN و Number.isFinite

متدهای Number.isNaN و Number.isFinite نسخه‌های «سخت‌گیرتر» از تابع‌های isNaN و isFinite هستند. آن‌ها به طور خودکار آرگومان خود را به عدد تبدیل نمی‌کنند بلکه در عوض بررسی می‌کنند که آیا آرگومان به نوع number تعلق دارد یا خیر.

  • Number.isNaN(value) اگر آرگومان به نوع number تعلق داشته باشد و برابر با NaN باشد مقدار true را برمی‌گرداند. در غیر این صورت false برگردانده می‌شود.

    alert( Number.isNaN(NaN) ); // true
    alert( Number.isNaN("str" / 2) ); // true
    
    // :به تفاوت توجه کنید
    alert( Number.isNaN("str") ); // false به نوع رشته تعلق دارد نه عدد پس "str" چون
    alert( isNaN("str") ); // true را دریافت می‌کند پس NaN را به یک عدد تبدیل می‌کند و از این تبدیل "str" رشته isNaN چون
  • Number.isFinite(value) اگر آرگومان به نوع number تعلق داشته باشد و NaN/Infinity/-Infinity نباشد مقدار true برگردانده می‌شود. در غیر این صورت false را برمی‌گرداند.

    alert( Number.isFinite(123) ); // true
    alert( Number.isFinite(Infinity) ); // false
    alert( Number.isFinite(2 / 0) ); // false
    
    // :به تفاوت توجه کنید
    alert( Number.isFinite("123") ); // false چون "123" به نوع رشته تعلق دارد نه نوع عدد پس
    alert( isFinite("123") ); // true رشته "123" را به عدد 123 تبدیل می‌کند پس isFinite چون

به نحوی، Number.isNaN و Number.isFinite ساده‌تر و سرراست‌تر از تابع‌های isNaN و isFinite هستند. اگرچه در عمل، isNaN و isFinite بیشتر استفاده می‌شوند چون برای نوشتن کوتاه‌تر هستند.

مقایسه کنید با Object.is

یک متد درون‌ساخت خاص به نام Object.is وجود دارد که مقادیر را مثل === مقایسه میکند، اما برای دو حالت مرزی قابل اعتمادتر است:

۱. با NaN کار میکند: Object.is(NaN, NaN) === true، که چیز خوبیست. ۲. مقادیر 0 و -0 متفاوت هستند: Object.is(0, -0) === false، به ندرت اهمیت دارد، اما این مقادیر در اصل متفاوتند.

در تمام حالات دیگر، Object.is(a, b) با a === b برابراست.

ما Object.is را اینجا ذکر می‌کنیم چون اغلب در مشخصات جاوااسکریپت استفاده می‌شود. زمانی که یک الگوریتم درونی نیاز دارد که دو مقدار را برای اینکه دقیقا یکسان باشند مقایسه کند، از Object.is استفاده می‌کند (از درون SameValue فراخوانی می‌شود).

parseInt و parseFloat

تبدیلات عددی که از یک جمع + یا Number() استفاده میکنند، سخت‌گیر هستند.

alert( +"100px" ); // NaN

تنها استثنا، کاراکتر خالی در شروع یا انتهای رشته حرفی هستند که آنها در تابع در نظر گرفته نمی‌شوند.

اما در دنیای واقعی، ما مقادیر در واحدهای مختلفی داریم، مثل "100px" یا "12pt" در CSS. همینطور در بسیاری از کشورها، نماد پولی آنها بعد از مقدار عددی ظاهر میشود. مثل "19€"، که میخواهیم آن مقدار را از قسمت حرفی جدا کنیم.

به همین علت است که parseInt و parseFloat استفاده می‌شوند.

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

alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5

alert( parseInt('12.3') ); // 12, تنها قسمت صحیح عدد برگردانده شده
alert( parseFloat('12.3.4') ); // 12.3, نقطه‌ی دومی، فرآیند خوانده شدن را متوقف می‌کند.

حالاتی وجود دارد که parseInt/parseFloat ممکن است مقدار NaN را برگردانند. این برای حالتی‌ست که هیچ رقمی نتواند خوانده شود:

alert( parseInt('a123') ); // NaN, اولین حرف این رشته‌ی حرفی، فرآیند را متوقف می‌کند.
آرگومان دوم parseInt(str, radix)

تابع parseInt()، یک پارامتر اختیاری دومی هم دارد که مقدار پایه‌ی سیستم عددی را مشخص میکند، به طبع میتوانیم رشته‌ حرفی اعداد پایه ۱۶، پایه ۲ و به همین ترتیب را به دست آوریم:

alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255, بدون 0x هم کار می‌کند

alert( parseInt('2n9c', 36) ); // 123456

توابع ریاضی دیگر

جاوااسکریپت یک شئ از پیش آماده‌ شده Math دارد که شامل کتابخانه‌ای کوچک از توابع ریاضی و ثوابت است.

تعدادی مثال:

Math.random()

یک عدد تصادفی از بین ۰ تا ۱ برمی‌گرداند (که شامل ۱ نمیشود(.

alert( Math.random() ); // 0.1234567894322
alert( Math.random() ); // 0.5435252343232
alert( Math.random() ); // ... (هر عدد تصادفی‌ای)
Math.max(a, b, c...) / Math.min(a, b, c...)

بزرگترین/کوچک‌ترین عدد را از بین تعداد دلخواه آرگومان‌ها بر‌می‌گرداند

alert( Math.max(3, 5, -10, 0, 1) ); // 5
alert( Math.min(1, 2) ); // 1
Math.pow(n, power)

عدد n را به توان داده شده می‌رساند.

alert( Math.pow(2, 10) ); // دو به توان ده = 1024

توابع و ثوابت بیشتری در شئ Math وجود دارد، مثل روابط مثلثات که میتوانید در مستندات برای شئ Math پیدا کنید.

خلاصه

برای نوشتن اعدادی که صفر زیاد دارند:

  • حرف "e" را با تعداد صفرها به انتهای عدد اضافه کنید. مثل: 123e6 که 123 است با ۶ صفر 123000000.
  • یک عدد منفی بعد "e" باعث تقسیم شدن عدد بر یک با تعداد صفر مشخص شده میشود. مانند 123e-6 به معنی `0.000123 (123 میلیونیوم).

برای سیستم‌های عددی متفاوت:

  • میتوان اعداد را مستقیما در فرم پایه۱۶ (0x) نوشت، پایه۸ (0o) و دودویی (0b) نوشت.
  • parseInt(str, base) یک عدد صحیح را از هر سیستم عددی با پایه‌ی 2 ≤ base ≤ 36 را استخراج می‌کند.
  • num.toString(base) یک عدد را به یک رشته‌ی حرفی در سیستم عددی با پایه داده شده تبدیل می‌کند.

برای آزمایش عادی عددها:

  • isNaN(value) آرگومان خود را به یک عدد تبدیل می‌کند و بررسی می‌کند که NaN است یا خیر.
  • Number.isNaN(value) بررسی می‌کند که آرگومان آن به نوع number تعلق دارد یا خیر و اگر داشت، بررسی می‌کند که NaN هست یا خیر
  • isFinite(value) آرگومان خود را به عدد تبدیل می‌کند و اگر یک عدد معمولی باشد true برمی‌گرداند نه اینکه NaN/Infinity/-Infinity باشد.
  • Number.isFinite(vlaue) بررسی می‌کند که آیا آرگومان آن به نوع number تعلق دارد یا خیر و اگر داشت، بررسی می‌کند که NaN/Infinity/-Infinity نباشد

برای تبدیل مقادیری مثل 12pt و 100px به یک عدد:

  • parseInt/parseFloat را برای تبدیلات ساده استفاده کنید که یک عدد را از یک رشته‌ی حرفی می‌خواند و سپس مقداری که قبل از بروز خطا خوانده‌ست را برمی‌گرداند.

برای کسرها:

  • با کمک Math.floor، Math.ceil، Math.trunc، Math.round یا num.toFixed(precision) رند کنید.
  • به یاد داشته باشید که یک دقت از دست رفته‌ای در حین کار با کسر ها وجود دارد.

توابع ریاضی بیشتر:

  • Math شئ را وقتی به آنها نیاز دارید ببینید. کتابخانه‌ی بسیار کوچکیست اما توابع پایه‌ای را پوشش می‌دهد.

تمارین

اهمیت: 5

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

اجرای دمو

پی نوشت: حواستان به مدل‌های داده‌ها باشد.

let a = +prompt("The first number?", "");
let b = +prompt("The second number?", "");

alert( a + b );

توجه کنید که جمع واحد + قبل از prompt است. این یعنی در همان لحظه به مقدار عددی تبدیل می‌شود.

در غیر اینصورت، a و b، رشته حرفی میبودند و جمع آنها، پیوست کردن آنها به یکدیگر می‌بود که یعنی:‌ "1" + "2" = "12".

اهمیت: 4

بنابر مستندات Math.round و toFixed، هردو به نزدیکترین عدد آن را رند میکنند: 0..4 به پایین رند میشود و 5..9 به بالا.

به عنوان مثال:

alert( 1.35.toFixed(1) ); // 1.4

در مثال مشابه زیر، چرا 6.35 به 6.3 رند میشود، نه 6.4؟

alert( 6.35.toFixed(1) ); // 6.3

چطور 6.35 را رند کنیم؟

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

alert( 6.35.toFixed(20) ); // 6.34999999999999964473

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

و اما 1.35 چی؟

alert( 1.35.toFixed(20) ); // 1.35000000000000008882

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

چطور میتوانیم مشکل 6.35 را حل کنیم اگر بخواهیم که درست رند شود؟

باید به عدد صحیح نزدیک قبلی‌ش رندش کنیم:

alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000

توجه داشته باشید که 63.5 هیچ از دست دادن دقتی ندارد. به خاطر قسمت اعشاری آن 0.5 که در اصل 1/2 است. کسرهای تقسیم شده بر دو به درستی در سیستم دودویی نمایش داده می‌شوند، حالا میتوانیم آن را رند کنیم:

alert( Math.round(6.35 * 10) / 10 ); // 6.35 -> 63.5 -> 64(rounded) -> 6.4
اهمیت: 5

یک تابع readNumber بسازید که تا زمانی که بازدیدکننده یک عدد وارد نکرده باشد، نرود.

خروجی به فرمت عدد باید باشد.

بازدید کننده میتواند با وارد کردن یک خط خالی یا فشردن دکمه‌ی “CANCEL” پردازش را متوقف کند. در این حالت، تابع خروجی null را می‌دهد. [demo]

باز کردن یک sandbox همراه با تست‌ها.

function readNumber() {
  let num;

  do {
    num = prompt("Enter a number please?", 0);
  } while ( !isFinite(num) );

  if (num === null || num === '') return null;

  return +num;
}

alert(`Read: ${readNumber()}`);

جواب اندکی پیچیده‌ست که به خاطر این است که باید null/خالی را هم در نظر بگیریم.

پس ما تا وقتی ورودی را می‌پذیریم که عدد عادی باشد. هردوی null (cancel) و خط خالی هم در این شرایط میگنجد، چونکه در فرم عددی صفر هستند.

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

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

اهمیت: 4

این حلقه بی‌نهایت است. هیچوقت تمام نمی‌شود. چرا؟

let i = 0;
while (i != 10) {
  i += 0.2;
}

به این علت است که i هیچوقت برابر ده نمیشود.

این تکه کد را اجرا کنید تا مقدار حقیقی i را ببینید:

let i = 0;
while (i < 11) {
  i += 0.2;
  if (i > 9.8 && i < 10.2) alert( i );
}

هیچکدام از آنها دقیقا ده نیست.

چنین اتفاقاتی به علت از دست رفتن دقت در حین جمع کردن کسرهایی مثل 0.2 رخ می‌دهد.

نتیجه: وقتی با کسرهای اعشاری کار میکنید، از تساوی استفاده نکنید.

اهمیت: 2

تابع از پیش آماده شده‌ی Math.random()، یک مقدار تصادفی از 0 تا 1 میسازد (به جز خود یک).

تابع random(min, max) را بنویسید که یک عدد اعشاری از بین min تاmax را می‌سازد (به جز خود max).

مثال‌هایی ازینکه چگونه کار می‌کند:

alert( random(1, 5) ); // 1.2345623452
alert( random(1, 5) ); // 3.7894332423
alert( random(1, 5) ); // 4.3435234525

نیاز داریم که تمام مقادیر را در بازه 0…1 به مقادیر از min تا max ارتباط دهیم.

این به دو روش قابل انجام است:

۱. اگر یک عدد تصادفی از 0…1 را در عددی max-min ضرب کنیم، بازه‌ی مقادیر ممکن از 0..1 به 0..max-min افزایش می‌یابد.

۲. حالا اگر min را اضافه کنیم، بازه‌ی ممکن از min تا max میشود.

تابع:

function random(min, max) {
  return min + Math.random() * (max - min);
}

alert( random(1, 5) );
alert( random(1, 5) );
alert( random(1, 5) );
اهمیت: 2

یک تابع randomInteger(min, max) می‌سازد که یک عدد صحیح تصادفی از min تا max که هر دو شامل مقادیر min و max می‌شود را خروجی می‌دهد.

هر عدد از بازه‌ی min..max باید احتمال یکسانی داشته باشد.

مثال‌هایی از کارکردش:

alert( random(1, 5) ); // 1
alert( random(1, 5) ); // 3
alert( random(1, 5) ); // 5

میتوانید از راه حل قبلی هم به عنوان پایه استفاده کنید.

جواب ساده اما اشتباه

جواب اشتباه اما ساده این است که مقداری از min تا max رو تولید کنیم و رندش کنیم:

function randomInteger(min, max) {
  let rand = min + Math.random() * (max - min);
  return Math.round(rand);
}

alert( randomInteger(1, 3) );

این تابع کار میکند اما غلط است. احتمال اینکه مقادیر لبه min و max را در نتیجه بگیریم، نصف بقیه‌ست.

اگر شما مثال بالا را به کرات اجرا کنید، میبینید که عدد ۲ اکثر اوقات ظاهر می‌شود.

به این علت است که Math.round() اعداد تصادفی از بازه 1..3 را میگیرد و به شکل زیر رند می‌کند.

values from 1    ... to 1.4999999999  become 1
values from 1.5  ... to 2.4999999999  become 2
values from 2.5  ... to 2.9999999999  become 3

حالا میتوانیم به وضوح ببینیم که عدد ۲، دو برابر عدد یک مقادیر به آن نسبت داده میشود. همینطور هم برای عدد ۳.

راه حل صحیح

راه حال‌های صحیح زیادی وجود دارد. یکی از آنها تنظیم نقاط مرزی‌ست. برای اطمینان یافتن از ازینکه بازه‌ها برابرند میتوانیم مقادیر را از 0.5 تا 3.5 تولید کنیم، سپس احتمال لازم صحیح را به حالات لبه نسبت دهیم:

function randomInteger(min, max) {
  // now rand is from  (min-0.5) to (max+0.5)
  let rand = min - 0.5 + Math.random() * (max - min + 1);
  return Math.round(rand);
}

alert( randomInteger(1, 3) );

راه دیگر میتواند استفاده از Math.floor باشد برای یک عدد تصادفی از min تا max+1:

function randomInteger(min, max) {
  // here rand is from min to (max+1)
  let rand = min + Math.random() * (max + 1 - min);
  return Math.floor(rand);
}

alert( randomInteger(1, 3) );

حال، تمام بازه ها بدین شکل می‌شوند:

values from 1  ... to 1.9999999999  become 1
values from 2  ... to 2.9999999999  become 2
values from 3  ... to 3.9999999999  become 3

همه ی بازه‌ها الان طول یکسانی دارند و در نهایت توزیع یکسانی دارند.

نقشه آموزش

نظرات

قبل از نظر دادن این را بخوانید…
  • اگر پیشنهادی برای بهبود ترجمه دارید - لطفا یک ایشوی گیت‌هاب یا یک پول‌ریکوئست به جای کامنت‌گذاشتن باز کنید.
  • اگر چیزی را در مقاله متوجه نمی‌شوید – به دقت توضیح دهید.
  • برای قراردادن یک خط از کد، از تگ <code> استفاده کنید، برای چندین خط – کد را درون تگ <pre> قرار دهید، برای بیش از ده خط کد – از یک جعبهٔ شنی استفاده کنید. (plnkr، jsbin، codepen…)