Emq Mohammed

يوجد نوعان من الأعداد في JavaScript:

  1. أعداد عادية تخزَّن بصيغة 64-بت IEEE-754، تُعرف أيضًا ب "الأعداد العشرية مضاعفة الدقة" (double precision floating point numbers). هذا النوع هو ما سنستعلمه أغلب الوقت وسنسلط عليه الضوء في هذا الفصل.
  2. أعداد صحيحة كبيرة (BigInt numbers) تمثِّل عددًا صحيحًا متغير الحجم، إذ قد نلجأ إليها أحيانًا لأن النوع السابق لا يمكن أن يتجاوز القيمة 2^53 أو أن تقل عن -2^53، وسنخصص لهذا النوع فصلًا خاصًا به نظرًا للحاجة إليه في حالات خاصة.

حاليًا، لِنتوسع عن ما نعرفه عنها، وننتقل إلى الحديث عن النوع الأول، الأعداد العادية.

طرائق أخرى لكتابة عدد

تخيل أننا نريد كتابة 1 بليون. الطريقة الواضحة هي:

let billion = 1000000000;

لكن، نتجنب غالبًا كتابة سلسلة طويلة من الأصفار في الحياة الواقعية لأنه من السهل الخطأ في ذلك ولكون ذلك الأمر يأخذ وقتًا أكثر. نكتب غالبًا شيئا مثل "1bn" بدلًا من بليون أو "7.3bn" بدلًا من 7 بليون و 300 مليون. يمكن تطبيق الأمر ذاته مع الأعداد الكبيرة.

نُقَصِّر أرقام الأعداد في JavaScript بإضافة الحرف "e" للعدد وتحديد عدد الأصفار فيه:

// بليون، حرفيًا: 1 وجانبه 9 أصفار
let billion = 1e9;

alert( 7.3e9 );  // 7,300,000,000

بمعنى آخر، يضرب الشكل "XeY" العدد X في 1 متبوعًا بعدد Y من الأصفار.

1e3 = 1 * 1000
1.23e6 = 1.23 * 1000000

لنكتب الآن شيئَا صغيرًا جدًا. مثلًا، جزء من المليون من الثانية:

let ms = 0.000001;

كما قمنا سابقًا، يمكن استخدام "e" لتجنب كتابة الأصفار، يمكننا القول:

let ms = 1e-6; // ستة أصفار على يسار 1

إن قمنا بعد الأصفار في 0.000001، سنجد عددها 6. لذا يكون الرقم 1e-6.

بمعنى آخر، وجود رقم سالب بعد "e" يعني القسمة على 1 متبوعًا بِعدد الأصفار المعطى:

// بالقسمة على 1 متبوعًا ب 3 أصفار
1e-3 = 1 / 1000 (=0.001)

// بالقسمة على 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

نقطتين لِاستدعاء تابع

لاحظ أن النقطتين في ‎123456..toString(36)‎ ليست خطأ كتابي. إن أردنا استدعاء تابع مباشرة على عدد/ مثل التابع toString في المثال أعلاه، فسنحتاج إلى نقطتين بعد العدد ...

إن وضعنا نقطة واحدة فقط ‎123456.toString(36)‎ فسيكون هناك خطأ، لأن JavaScript سَتعتبر أن النقطة هي فاصلة عشرية وأن ما بعدها هو جزء عشري للعدد. فإذا وضعنا نقطة أخرى فستعرف أن الجزء العشري فارغ وتنتقل إلى الدالة.

يمكن كتابتها بهذه الطريقة أيضًا ‎(123456).toString(36)‎.

التقريب (Rounding)

أحد الخصائص الأكثر استخدامًا عند التعامل مع الأعداد هي التقريب. يوجد العديد من الدوال المدمجة للتقريب:

  • Math.floor: تقريب للجزء الأصغر: 3.1 تصبح 3، و -1.1 تصبح -2.
  • Math.ceil: تقريب للجزء الأكبر: 3.1 تصبح 4، و -1.1 تصبح -1.
  • Math.round: تقريب لأقرب عدد صحيح: 3.1 تصبح 3، 3.6 تصبح 4 و -1.1 تصبح -1.
  • Math.trunc (ليست مدعومة بواسطة المتصفح Internet Explorer): تحذف أي شيء بعد الفاصلة العشرية بدون تقريب: 3.1 تصبح 3، -1.1 تصبح -1.

يختصر الجدول في الأسفل الاختلافات بين هذه التوابع:

  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) العدد المستدعى معه إلى الخانة 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، لذا يوجد 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 بالضبط.

ليس فقط في 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 يوزان القيم كما === لكنه أكثر موثوقية لسببين:

  1. أنه يعمل مع NaN: أي "Object.is(NaN, NaN) === true" وهذا أمر جيد.
  2. القيمتان 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

المُعامِل الثاني للتابع parseInt(str, radix)‎

لدى التابع parseInt()‎ مُعامِل ثانٍ اختياري. والذي يحدد نوع النظام العددي، لذا يمكن للتابع parseInt تحويل النصوص ذات الأعداد الست عشرية، الثنائية وهكذا:

alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255

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

دوال رياضية أخرى

تحتوي JavaScript على الكائن المُدمَج Math الذي يحتوي على مكتبة صغيرة بالدوال والثوابت الرياضية. إليك بعض الأمثلة:

  • Math.random()‎: تُرجِع عددًا عشوائيًا من 0 إلى 1 (لا تتضمن 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)‎: تُرجع العدد n مرفوعًا إلى الأُس المُعطى:
alert( Math.pow(2, 10) ); // 2^10 = 1024

يوجد المزيد من الدوال والثوابت في الكائن Math، بما فيها علم المُثَلَّثات، والتي يمكنك ايجادها في توثيق الكائن Math.

الخلاصة

لكتابة أعداد كبيرة:

  • أضِف "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 عندما تحتاج ذلك، هذه المكتبة صغيرة جدًا، لكنها تغطي الاحتياجات الأساسية.

المهام

جمع الأعداد من الزائر

الأهمية: 5

انشِئ سكربت يتيح للمستخدم ادخال رقمين ثم أعرض مجموعهما.

الحل

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

alert( a + b );

لاحظ عامل الجمع الأحادي + قبل prompt. يحوِّل القيم إلى أعداد. وإلا فإن a و b ستكون نصوصًا وسيكون مجموعهما بدمجهما: "1" + "2" = "12".

لماذا ‎6.35.toFixed(1) == 6.3؟

الأهمية: 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. يمكن تمثيل الأجزاء المقسومة على 2 تُمَثَّل بشكل صحيح في النظام الثنائي. يمكننا تقريب العدد الآن:

alert( Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(rounded) -> 6.4

كرر حتى يصبح المُدخَل عددًا

الأهمية: 5

أنشِئ الدالة readNumber والتي تطلب من الزائر إدخال عدد حتى يقوم بإدخال قيمة عددية صحيحة. يجب أن تكون القيمة المُرجَعة عددًا.

يمكن للزائر إيقاف العملية بإدخال سطر فارغ أو الضغط على "CANCEL". يجب أن تُرجِع الدالة null في هذه الحالة.

الحل

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)‎ والسطر الفارغ شروط الأعداد كونها تساوي القيمة العددية 0.

بعد توقف الشيفرة يجب معاملة null والأسطر الفارغة بطريقة خاصة (إرجاع null). لأن تحويلها إلى أعداد يُرجِع 0.

حلقة غير منتهية أحيانًا

الأهمية: 4

الحلقة التالية غير منتهية، ولا تتوقف أبدًا. لماذا؟

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

الحل

ذلك لأن i لن يساوي 10 أبدًا. نفذ الشيفرة التالية لرؤية قيم i:

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

لا توجد قيمة تساوي 10 تمامًا. تحدث مثل هذه الأمور بسبب ضياع الدقة عند إضافة الأجزاء مثل 0.2. الخلاصة، تجنب التحقق من المساواة عند التعامل مع الأجزاء العشرية.

رقم عشوائي من العدد الأدنى إلى الأقصى

الأهمية: 2

تُنشِئ الدالة Math.random()‎ المُضَمَنَة في اللغة قيمة عشوائية بين 0 و 1 (ليس بما في ذلك 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. يمكن القيام بذلك في مرحلتين:

  1. إذا ضربنا قيمة عشوائية من 0…1 في max-min. فإن فترة القيم الممكنة تزيد 0..1 إلى 0..max-min.
  2. إذا أضفنا 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) );

قيمة صحيحة عشوائية من min إلى max

الأهمية: 2

أنشِئ دالة randomInteger(min, max)‎ تقوم بتوليد قيمة صحيحة عشوائية من min إلى max بما في ذلك min و max.

يجب أن يظهر كل رقم من الفترة min..max بفرص متساوية. مثال على طريقة العمل:

alert( randomInteger(1, 5) ); // 1
alert( randomInteger(1, 5) ); // 3
alert( randomInteger(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 أقل بمرتين من باقي القيم. إن شغلنا المثال أعلاه لعدة مرات، فينرى ظهور 2 بصورة أكبر.

يحدث ذلك لأن 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

نلاحظ الآن أن لدى 1 قيم أقل بمرتين من 2 وكذلك 3.

الطريقة الصحيحة

يوجد العديد من الطرق الصحيحة لحل هذه المهمة. إحداها هو بتعديل حدود الفترة. للتأكد من وجود فرص متساوية، نُوَلِّد قيمًا من 0.5 إلى 3.5، ثم إضافة الاحتمالات الممكنة للأطراف:

function randomInteger(min, max) {
  
  // (max+0.5) إلى (min-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) {
  
  // (max+1) إلى min التقريب الآن من 
  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

لدى جميع الفترات الطول ذاته مما يجعل التوزيع النهائي موحدًا.

ترجمة -وبتصرف- للفصل Numbers من كتاب The JavaScript Language

اقرأ أيضًا





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن