يوجد نوعان من الأعداد في JavaScript:
- أعداد عادية تخزَّن بصيغة 64-بت IEEE-754، تُعرف أيضًا ب “الأعداد العشرية مضاعفة الدقة” (double precision floating point numbers). هذا النوع هو ما سنستعلمه أغلب الوقت وسنسلط عليه الضوء في هذا الفصل.
- أعداد صحيحة كبيرة (BigInt numbers) تمثِّل عددًا صحيحًا متغير الحجم، إذ قد نلجأ إليها أحيانًا لأن النوع السابق لا يمكن أن يتجاوز القيمة 2^53 أو أن تقل عن‑2^53، وسنخصص لهذا النوع فصلًا خاصًا به نظرًا للحاجة إليه في حالات خاصة.
حاليًا، لِنتوسع عن ما نعرفه عنها، وننتقل إلى الحديث عن النوع الأول، الأعداد العادية.
طرق أخرى لكتابة عدد
تخيل أننا نريد كتابة 1 بليون. الطريقة الواضحة هي:
let billion = 1000000000;
We also can use underscore _
as the separator:
let billion = 1_000_000_000;
Here the underscore _
plays the role of the “syntactic sugar”, it makes the number more readable. The JavaScript engine simply ignores _
between digits, so it’s exactly the same one billion as above.
In real life though, we try to avoid writing long sequences of zeroes. We’re too lazy for that. We’ll try to write something like "1bn"
for a billion or "7.3bn"
for 7 billion 300 million. The same is true for most large numbers.
In JavaScript, we can shorten a number by appending the letter "e"
to it and specifying the zeroes count:
alert( 7.3e9 ); // 7.3 billions (same as 7300000000 or 7_300_000_000)
let billion = 1e9; // بليون، حرفيًا: 1 وجانبه 9 أصفار
In other words, `e` multiplies the number by `1` with the given zeroes count.
```js
1e3 = 1 * 1000 // e3 means *1000
1.23e6 = 1.23 * 1000000 // e6 means *1000000
لنكتب الآن شيئَا صغيرًا جدًا. مثلًا، جزء من المليون من الثانية:
let ms = 0.000001;
كما قمنا سابقًا، يمكن استخدام "e"
لتجنب كتابة الأصفار، يمكننا القول:
let ms = 1e-6; // ستة أصفار على يسار 1
إن قمنا بعد الأصفار في 0.000001
، سنجد عددها 6. لذا يكون الرقم 1e-6
.
بمعنى آخر، وجود رقم سالب بعد "e"
يعني القسمة على 1 متبوعًا بِعدد الأصفار المعطى:
// -3 بالقسمة على 1 متبوعًا ب 3 أصفار
1e-3 = 1 / 1000 (=0.001)
// -6 بالقسمة على 1 متبوعًا ب 6 أصفار
1.23e-6 = 1.23 / 1000000 (=0.00000123)
الأعداد الست عشرية، والثنائية والثمانية
تُستخدَم الأعداد الست عشرية بكثرة في JavaScript لتمثيل الألوان، وتشفير الأحرف ولأشياء أخرى عديدة. لذا فإن هناك طريقة أقصر لكتابتها وذلك بوضع السابقة 0x
ثم الرقم. مثلًا:
alert( 0xff ); // 255
alert( 0xFF ); // 255 (العدد ذاته, لا يوجد اختلاف باختلاف حالة الأحرف)
تستخدم الأنظمة الثنائية والثمانية نادرًا، لكنها مدعومة أيضًا باستخدام السابقة 0b
والسابقة 0o
على التوالي:
let a = 0b11111111; // الهيئة الثنائية لِلعدد 255
let b = 0o377; // الهيئة الثمانية لِلعدد 255
alert( a == b ); // صحيح، العدد ذاته 255 في كلا الجانبين
يوجد ثلاثة أنظمة عددية فقط مدعومة بالشكل السابق. يجب استخدام الدالة parseInt
لباقي الأنواع (سنشرحها لاحقًا في هذا الفصل).
toString(base)
يُرجِع التابع num.toString(base)
تمثيلًا نصيًا للمتغير num
إلى النظام العددي المُعطى base
. مثلًا:
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
يمكن أن تختلف قيمة base
من 2
حتى 36
، والقيمة الافتراضية هي 10
.
حالات الاستخدام الشائعة:
base=16
: تستخدم للألوان الست عشرية، وتشفير الأحرُف وغيرها، قد تحوي الخانات الأرقام0..9
أو الأحرفA..F
.base=2
: يستخدم بكثرة في تصحيح العمليات الدقيقة، يمكن أن يحوي الرقمين0
أو1
.base=36
: هو الحد الأعلى، يمكن أن يحوي الأرقام0..9
أو الأحرُفA..Z
. يمكن استخدام جميع الأحرف اللاتينية لتمثيل عدد. قد يبدو أمرًا ممتعًا لكن يكون مفيدًا في حال احتجنا لتحويل معرف عددي طويل إلى عدد أقصر، مثلًا، لتقصير رابط url. يمكن تمثيله بالنظام العددي ذي الأساس36
:
alert( 123456..toString(36) ); // 2n9c
Rounding
One of the most used operations when working with numbers is rounding.
There are several built-in functions for rounding:
Math.floor
- Rounds down:
3.1
becomes3
, and-1.1
becomes-2
. Math.ceil
- Rounds up:
3.1
becomes4
, and-1.1
becomes-1
. Math.round
- Rounds to the nearest integer:
3.1
becomes3
,3.6
becomes4
, the middle case:3.5
rounds up to4
too. Math.trunc
(not supported by Internet Explorer)- Removes anything after the decimal point without rounding:
3.1
becomes3
,-1.1
becomes-1
.
Here’s the table to summarize the differences between them:
Math.floor |
Math.ceil |
Math.round |
Math.trunc |
|
---|---|---|---|---|
3.1 |
3 |
4 |
3 |
3 |
3.6 |
3 |
4 |
4 |
3 |
-1.1 |
-2 |
-1 |
-1 |
-1 |
-1.6 |
-2 |
-1 |
-2 |
-1 |
These functions cover all of the possible ways to deal with the decimal part of a number. But what if we’d like to round the number to n-th
digit after the decimal?
For instance, we have 1.2345
and want to round it to 2 digits, getting only 1.23
.
إن وضعنا نقطة واحدة فقط 123456.toString(36)
فسيكون هناك خطأ، لأن JavaScript سَتعتبر أن النقطة هي فاصلة عشرية وأن ما بعدها هو جزء عشري للعدد. فإذا وضعنا نقطة أخرى فستعرف أن الجزء العشري فارغ وتنتقل إلى الدالة.
يمكن كتابتها بهذه الطريقة أيضًا (123456).toString(36)
.
## التقريب (Rounding)
أحد الخصائص الأكثر استخدامًا عند التعامل مع الأعداد هي التقريب. يوجد العديد من الدوال المدمجة للتقريب:
alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
```
يختصر الجدول في الأسفل الاختلافات بين هذه التوابع:
| | `Math.floor` | `Math.ceil` | `Math.round` | `Math.trunc` |
| ---- | ---------- | --------- | ---------- | ---------- |
| 3.1 | 3 | 4 | 3 | 3 |
| 3.6 | 3 | 4 | 4 | 3 |
| -1.1 | -2 | -1 | -1 | -1 |
| -1.6 | -2 | -1 | -2 | -1 |
تُعَطِّي هذه التوابع جميع الاحتمالات الممكنة للتعامل مع الجزء العشري للعدد، لكن ماذا إن كنا نريد تقريب العدد إلى خانة محدَّدة بعد الفاصلة العشرية؟
مثلًا، لدينا العدد `1.2345` ونريد تقريب إلى خانتين لنحصل على `1.23` فقط. يوجد طريقتين للقيام بذلك:
1- الضرب والقسمة:
مثلًا، لتقريب الرقم إلى الخانة الثانية بعد الفاصلة العشرية، يمكننا ضرب العدد في `100`، ثم نستدعي تابع التقريب ثم نقسم على نفس العدد.
let num = 1.23456;
alert( Math.floor(num * 100) / 100 ); // 1.23456 → 123.456 → 123 → 1.23
2- يقرب التابع [`toFixed(n)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) العدد المستدعى معه إلى الخانة `n` بعد الفاصلة العشرية ويُرجِع تمثيلًا نصيًا للنتيجة.
let num = 12.34; alert( num.toFixed(1) ); // “12.3”
يعمل التابع على تقريب العدد للأكبر أو الأصغر وفقًا إلى أقرب قيمة، مثل التابع `Math.round`:
let num = 12.36; alert( num.toFixed(1) ); // “12.4”
لاحظ أن مخرجات التابع `toFixed` هي نص. إن كان الجزء العشري أقل من المطلوب، تُضاف الأصفار إلى نهاية الرقم:
let num = 12.34; alert( num.toFixed(5) ); // “12.34000”, أصفار مضافة لجعل عدد الخانات 5
يمكننا تحويل المخرجات إلى عدد باستخدام الجمع الأحادي أو باستدعاء الدالة `Number()`: `+num.toFixed(5) `.
## حسابات غير دقيقة
يُمَثَّل العدد داخليًا بصيغة 64-بِت [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754-2008_revision)، لذا يوجد 64 بِت لتخزين العدد: تستخدم 52 منها لتخزين أرقام العدد، و 11 منها لتخزين مكان الفاصلة العشرية (تكون أصفارًا للاعداد الصحيحة)، و 1 بِت لإشارة العدد.
إن كان العدد كبيرًا جدًا، فَسيزداد عن مساحة التخزين 64-بِت، معطيًا ما لا نهاية:
alert( 1e500 ); // ما لا نهاية
ما قد يكون أقل وضوحًا، ويحدث غالبًا هو ضياع الفاصلة. لاحظ الاختبار الخطأ التالي:
alert( 0.1 + 0.2 == 0.3 ); // خطأ
الجملة السابقة تحدث فعليًا، إن فحصنا ما إن كان مجموع `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)`.
لذا، فإن من المؤكد أن تعمل الأعداد المقسومة على مضاعفات العدد `10` في النظام العشري، ولا تعمل المقسومة على `3`. وكذلك أيضًا في النظام الثنائي، تعمل الأعداد المقسومة على مضاعفات العدد `2`، لكن يصبح العدد `1/10` كسورًا ثنائية غير منتهية.
لا يوجد طريقة لتخزين العدد ذاته 0.1 أو 0.2 بالضبط باستخدام النظام الثنائي، كما لا يمكن تخزين الثلث كجزء عشري.
يحل نمط الأعداد IEEE-754 هذه المشكلة بتقريب العدد إلى أقرب عدد ممكن. لا تتيح لنا قواعد التقريب هذه رؤية خسارة الأجزاء الصغيرة، لكنها تكون موجودة. يمكننا رؤية ذلك فعليًا:
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
وعند جمع عددين، فإن الأجزاء المفقودة تظهر.
هذا يفسر لِمَ `0.1 + 0.2` لا تساوي `0.3` بالضبط.
```smart header="Not only JavaScript"
هذه المشكلة موجودة في العديد من اللغات البرمجية الأخرى مثل 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
لذا، يقلل نهج الضرب والقسمة الخطأ لكنه لا يزيلة كليًا.
قد نحاول تجنب الأجزاء كليًا في بعض الأحيان. كما لو كنا نتعامل مع متجر إلكتروني، حتى نتمكن من تخزين الأسعار بالسنت بدلًا من الدولار. لكن ماذا إن وضعنا تخفيضًا ب 30%؟ لنكن أكثر دقة، يكون تجنب الأجزاء كليًا نادرًا جدًا. قم بتقريبها فقط لتتخلص من الأجزاء الغير المرغوبة عند الحاجة.
جرب تشغيل ما يلي:
// مرحبًا! أنا عدد يزداد من تلقاء نفسه!
alert( 9999999999999999 ); // يظهر 10000000000000000
هذه الحالة تعاني من المشكلة ذاتها: ضياع الدقة. يوجد 64 بِت للعدد، يمكن استخدام 52 منها لتخزين العدد، لكنها غير كافية. لذا يظهر الرقم الأخير.
لا تعرض JavaScript خطأ في مثل هذه الحالات فهي تقوم بأفضل ما لديها لملائمة العدد للصيغة المطلوبة. لكن، لسوء الحظ هذه الصيغة ليست كبيرة كفاية.
نتيجة أخرى سلبية للتمثيل الداخلي للأعداد هو وجود صفرين 0
و -0
. ذلك لأن الإإشارة تُمَثَّل ببِت مستقل، لذا فيمكن لأي عدد أن يكون سالبًا أو موجبًا بما في ذلك الصفر.
لا يكون هذا الفرق ملحوظًا في أغلب الحالات، لأن المعامِلات مُعَدَّة لتعاملهما كعدد واحد.
الفحص: isFinite و isNaN
هل تذكر القيم العددية الخاصة التالية؟
Infinity
(و-Infinity
) هي قيمة عددية خاصة تكون أكبر أو أصغر من أي شيء.NaN
تُمَثِّل وجود خطأ (ليس عددًا Not a Number).
تنتمي هذه القيم إلى النوع 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, NaN
alert( isFinite(Infinity) ); // false, Infinity
تستخدم isFinite
أحيانًا للتحقق ما إن كان النص عددًا عاديًا:
let num = +prompt("Enter a number", '');
// أو قيمة غير عددية -Infinity أو Infinity سيكون صحيحًا إلا إن أدخلت
alert( isFinite(num) );
يرجى ملاحظة أن الفراغ أو المسافة الواحدة تُعامل معاملة الصفر 0
في جميع التوابع العددية بما فيها isFinite
.
Object.is
يوجد تابع خاص مدمج في اللغة يدعى Object.is
يوزان القيم كما ===
لكنه أكثر موثوقية لسببين:
- أنه يعمل مع
NaN
: أيObject.is(NaN, NaN) === true
وهذا أمر جيد. - القيمتان
0
و-0
مختلفتان:Object.is(0, -0) === false
، الأمر صحيح تقنيًا، لأن العدد لديه إشارة داخليًا مما يجعل القيم مختلفة حتى لو كانت باقي الخانات أصفارًا.
يكون التابع Object.is(a, b)
نفس a === b
في باقي الحالات.
تُستخدم طريقة الموازنة هذه غالبًا في توصيف JavaScript. عندما تحتاج خوارزمية لموازنة كون قيمتين متطابقتان تمامًا فإنها تستخدم 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, the first symbol stops the process
parseInt(str, radix)
alert(parseInt('0xff', 16)); // 255
alert(parseInt('ff', 16)); // 255, و تعمل ايضا بدون 0x
alert(parseInt('2n9c', 36)); // 123456
دوال رياضية أخرى
تحتوي JavaScript على الكائن المُدمَج Math الذي يحتوي على مكتبة صغيرة بالدوال والثوابت الرياضية. إليك بعض الأمثلة:
Math.random()
-
Returns a random number from 0 to 1 (not including 1).
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)
-
Returns
n
raised to the given power.alert( Math.pow(2, 10) ); // 2 in power 10 = 1024
There are more functions and constants in Math
object, including trigonometry, which you can find in the docs for the Math object.
Summary
To write numbers with many zeroes:
- Append
"e"
with the zeroes count to the number. Like:123e6
is the same as123
with 6 zeroes123000000
. - A negative number after
"e"
causes the number to be divided by 1 with given zeroes. E.g.123e-6
means0.000123
(123
millionths).
For different numeral systems:
- Can write numbers directly in hex (
0x
), octal (0o
) and binary (0b
) systems. parseInt(str, base)
parses the stringstr
into an integer in numeral system with givenbase
,2 ≤ base ≤ 36
.num.toString(base)
converts a number to a string in the numeral system with the givenbase
.
الملخص
لكتابة أعداد كبيرة:
- أضِف
"e"
مع عدد الأصفار الخاصة بالعدد المطلوب، مثل:123e6
هو123
مع 6 أصفار. - قيمة سالبة بعد
"e"
تقسم العدد على 1 مع عدد الأصفار المُعطى.
لِأنظمة العد المختلفة:
- يمكن كتابة الأعداد مباشرة بالنظام الستعشري (
0x
)، أو الثُماني (0o
)، أو الثُنائي (0b
). parseInt(str, base)
تحوِّل النصstr
إلى عدد صحيح بالنظام العددي المُعطىbase
، و2 ≤ base ≤ 36
.num.toString(base)
تحوِّل العدد إلى نص بالنظام العددي المُعطىbase
.
لتحويل القيم مثل 12pt
and 100px
إلى عدد:
- استخدم
parseInt
أوparseFloat
لتحويل سلس، والتي تقرأ العدد من نص وتُرجِع القيمة التي استطاعت قرائتها قبل حصول أي خطأ.
للأجزاء:
- التقريب باستخدام
Math.floor
، أوMath.ceil
، أوMath.trunc
، أوMath.round
أوnum.toFixed(precision)
. - تذكر وجود ضياع في دقة الجزء العشري عند التعامل مع الكسور.
للمزيد من الدوال الرياضية:
- اطلع على الكائن Math عندما تحتاج ذلك، هذه المكتبة صغيرة جدًا، لكنها تغطي الاحتياجات الأساسية.