٢٥ مارس ٢٠٢١

الحلقات التكرارية: while و for

أحيانًا نحتاج لتكرار مجموعة من الأوامر.

مثل عرض منتجات من قائمة واحدًا تلو الآخر أو تكرار نفس الأمر عشر مرات.

الحلقات التكرارية هي طريقة لتكرار الأوامر.

حلقة “while”

إن حلقة while تكتب بالطريقة التالية:

while (condition) {
    // code
    // so-called "loop body"
}

طالما condition محقق يتم تنفيذ الكود المكتوب بداخلها.

على سبيل المثال فإن الكود التالي يعرضقيمة i طالما i < 3:

let i = 0;
while (i < 3) {
    // يعرض 0 ثم 1 ثم 2
    alert(i);
    i++;
}

تنفيذ الأوامر لمرة واحدة تكرار. المثال السابق يقوم بثلاثة عمليات تكرار.

إذا حذفت i++ سيتم تنفيذ الأوامر (نظريًا) إلى الأبد. ولكن في الحقيقة يقوم المتصفح بمنع حدوث هذا وعند استخدام جافا سكريبت في جانب Server يمكننا لإنهاء العملية.

يمكن لأي تعبير أن يكون شرط للتكرار وليس فقط عمليات المقارنة. يتم تنفيذ العملية وتحويل الناتج إلى قيمة منطقية بواسطة while.

على سبيل المثال يمكن اختصار while (i != 0) إلى while (i):

let i = 3;
while (i) { // عندما i تصبح 0, يصبح الشرط falsy وتتوقف الحلقة
  alert( i );
  i--;
}
الأقواس المعقوفة غير ضرورية عند تنفيذ أمر واحد

إذا أردنا تنفيذ أمر واحد فقط فيمكن تجاهل الأقواس المعقوفة {…}:

let i = 3;
while (i) alert(i--);

حلقة “do…while”

يمكن نقل الشرط إلى بعد الأوامر باستخدام نمط do..while:

do {
    // loop body
} while (condition);

سيتم أولا تنفيذ الأوامر ثم اختبار الشرط وإذا تحقق سيتم تنفيذ الأوامر مرة أخرى.

على سبيل المثال:

let i = 0;
do {
    alert(i);
    i++;
} while (i < 3);

هذه الطريقة يجب أن تستخدم فقط إذا أرد للأوامر أن تنفذ مرة واحدة على الأقل متجاهلًا الشرط. ففي الغالب يفضل استخدام: while(…) {…}.

حلقة “for”

حلقة for معقدة أكثر ولكنها الأكثر استخدامًا في تكرار الأوامر.

تكتب كالآتي:

for (begin; condition; step) {
    // ... loop body ...
}

لنتعرف على معاني هذه الأجزاء باستخدام مثال. الحلقة التالية تنفذ الأمر alert(i) ابتداءًا من i تساوي 0 حتى (لكن لا تشمل) 3:

for (let i = 0; i < 3; i++) {
    // تعرض 0 ثم 1 ثم 2
    alert(i);
}

لنشرح for جزء بجزء:

الجزء
begin i = 0 ينفذ مرة واحدة فقط في البداية.
condition i < 3 يتم اختباره قبل كل عملية تكرار وإذا لم يتحقق يتوقف التكرار.
body alert(i) تنفذ طالما الشرط محقق.
step i++ ينفذ بعد body في كل عملية تكرار.

الخوارزمية العامة للتكرار تعمل كالتالي:

نفذ begin
→ (if condition → نفذ body ثم نفذ step)
→ (if condition → نفذ body ثم نفذ step)
→ (if condition → نفذ body ثم نفذ step)
→ ...

وهكذا يتم تنفيذ begin مرة واحدة وبعد ذلك يبدأ التكرار: بعد كل عاختبار للشرط condition يتم تنفيذ body و step.

إذا كنت جديد على الحلقات التكرارية فيفضل الرجوع للمثال وتنفيذه خطوة بخطوة على ورقة.

هذا ما يحدث تمامًا في مثالنا:

// for (let i = 0; i < 3; i++) alert(i)

// نفذ begin
let i = 0;
// if condition → نفذ body ثم نفذ step
if (i < 3) {
    alert(i);
    i++;
}
// if condition → نفذ body ثم نفذ step
if (i < 3) {
    alert(i);
    i++;
}
// if condition → نفذ body ثم نفذ step
if (i < 3) {
    alert(i);
    i++;
}
// ...finish, because now i == 3
Inline variable declaration

هنا تم تعريف العداد i داخل الحلقة. وهذا يسمى “inline” variable declaration. وهذه المتغيرات تكون متاحة فقط داخل الحلقة.

for (let i = 0; i < 3; i++) {
  alert(i); // 0, 1, 2
}
alert(i); // خطأ! متغير غير معرف

بدلًا من تعريف متغير جديد يمكننا استخدام واحد معرف مسبقًا:

let i = 0;

for (i = 0; i < 3; i++) { // استخدام متغير معرف مسبقًا
  alert(i); // 0, 1, 2
}

alert(i); // 3, يمكن التعامل معه لأنه معرف خارج الحلقة

أجزاء يمكن تخطيها

أي جزء من for يمكن الاستغناء عنه.

مثلًا إذا حذفنا begin لن يكون لدينا ما نفعله في بداية الحلقة.

مثل:

let i = 0; // تم تعريف وتخصيص قيمة المتغير

for (; i < 3; i++) {
    // لا نحتاج "begin"
    alert(i); // 0, 1, 2
}

يمكن أيضًا حذف جزء step:

let i = 0;

for (; i < 3; ) {
    alert(i++);
}

هذا يجعلها تطابق while (i < 3).

يمكننا حذف كل شئ وخلق حلقة لا نهائية فارغة:

for (;;) {
    // تتكرر دائمًا
}

لاحظ أن كلتا الفاصلتين المنقوطتين ; داخل for يجب أن يوضعا وإلا سيظهر خطأ لغوي.

إيقاف الحلقة

في العادة تتوقف الحلقة عندما يصبح الشرط غير محقق.

ولكن يمكننا اجبارها على التوقف في أي وقت باستخدام كلمة break.

على سبيل المثال فإن الحلقة التالية تطلب من المستخدم إدخال مجموعة أرقام وتتوقف إذا لم يدخل رقم:

let sum = 0;

while (true) {

  let value = +prompt("ادخل رقم", '');

  if (!value) break; // (*)

  sum += value;

}
alert( 'المجموع: ' + sum );

تم تنشيط break في السطر (*) إذا قام المستخدم بإدخال سطر فارغ أو إيقاف الإدخال. وهذا يوقف الحلقة مباشرة ويذهب إلى السطر المكتوب بعدها والذي هو alert.

دمج “الحلقات اللانهائية + break” يستخدم في الحالات التي نريد فيها اختبار الشرط في منتصف التكرار أو في عدة أماكن وليس في بداية التكرار.

المتابعة للتكرار التالي

كلمة continue هي نسخة أخف من break. هي لا توقف الحلقة كلها ولكنها توقف التكرار الحالي وتبدأ تكرار جديد.

يمكننا استخدامها إذا انتهينا من التكرار الحالي ونريد الانتقال إلى تكرار جديد.

الكود التالي يستخدم continue لعرض القيم الفردية فقط:

for (let i = 0; i < 10; i++) {

  // if true, تخطي الجزء الباقي من التكرار
  if (i % 2 == 0) continue;

  alert(i); // 1, then 3, 5, 7, 9
}

في القيم الزوجية من i تقوم continue بإيقاف التكرار الحالى وتنتقل لتكرار جديد من for (بالرقم التالي). ولهذا فإن alert ينفذ فقط مع القيم الفردية.

``smart header=“تساعد continue على تقليل التداخل” يمكن عرض الأرقام الفردية بالطريقة التالية:

for (let i = 0; i < 10; i++) {
    if (i % 2) {
        alert(i);
    }
}

هذا الكود مطابق تمامًا للسابق. يمكننا فقط وضع الكود داخل if بدلًا من استخدام continue.

ولكن هذا ينتج مستوى آخر من التداخل (استدعاء alert داخل أقواس معقوفة). إذا كان ما بداخل if سطور كثيرة فهذا سيقلل من إمكانية قراءة الكود بوضوح.

````warn header="لا يمكن استخدام `break/continue` في الجانب الأيمن من '?'"
لا يمكن استخدام هذه التعبيرات `break/continue` مع العامل الشرطي `?`.

على سبيل المثال إذا أخذنا هذا الكود:

```js
if (i > 5) {
  alert(i);
} else {
  continue;
}
```

...وكتبناه باستخدام علامة الاستفهام:


```js no-beautify
(i > 5) ? alert(i) : continue; // continue لا يسمح باستخدامها هنا
```

...يتوقف عن العمل مع خطأ لغوي.

وهذا سبب آخر لعدم استخدام `?` بدلًا من `if`.

عنونة break/continue

أحيانا نريد الخروج من مجموعة حلقات متداخلة مرة واحدة.

في المثال التالي نقوم بالتكرار باستخدام i و j, ونطلب احداثيات (i, j) من (0,0) إلى (2,2):

for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        let input = prompt(`Value at coords (${i},${j})`, "");

        // إذا أردنا الخروج من هنا إلى Done (بالأسفل)?
    }
}

alert("Done!");

نحتاج إلى طريقة لإيقاف العملية إذا قام المستخدم بإلغاء الإدخال.

وضع break بعد input سيوقف فقط الحلقة الداخلية. وهذا غير مجدي – جاءت العنونة لإنقاذ الموقف!

إن label يقوم بتعريف الحلقة باستخدام نقطتين قبلها:

labelName: for (...) {
  ...
}

استخدام break <labelName> في الحلقة سيوقف الحلقة ذات العنوان المحدد:

outer: for (let i = 0; i < 3; i++) {

  for (let j = 0; j < 3; j++) {

    let input = prompt(`Value at coords (${i},${j})`, '');

    // إذا أدخل نص فارغ أو ألغى الإدخال قم بإيقاف الحلقتين
    if (!input) break outer; // (*)

    // أفعل شئ ما بالقيمة...
  }
}
alert('Done!');

في الكود السابق تقوم break outer بالبحث عن label اسمه outer وتوقف هذه الحلقة.

لذلك ينتقل من (*) إلى alert('Done!').

يمكن أيضا وضع العنوان في سطر منفصل:

outer:
for (let i = 0; i < 3; i++) { ... }

يمكن استخدام continue أيضًا مع label. وفي هذه الحالة سينتقل للتكرار التالي من الحلقة المحددة.

Labels لا تستخدم للإنتقال إلى أي مكان

Labels لا تسمح لنا بالإنتقال إلى أي مكان داخل الكود.

فعلى سبيل المثال لا يمكننا فعل التالي:

break label; // تنتقل إلى الحقل بالأسفل (لا تعمل)

label: for (...)

إستخدام continue يكون ممكنًا فقط من داخل الحلقه.

break ربما يمكن وضعه قبل الشيفرة ايضًا, as label: { ... }, لكنها لا تستخدم أبدًا بهذه الطريقة. وهي تعمل أيضًا من الداخل إلى الخارج فقط.

ملخص

تحدثنا عن 3 أنواع من الحلقات:

  • while – يتم فحص الشرط قبل التكرار.
  • do..while – يتم فحص الشرط بعد التكرار.
  • for (;;) – يتم فحص الشرط قبل التكرار, بعض المتغيرات الأخرى للإعداد.

لعمل حلقة لانهائية غالبا يتم استخدام while(true). ويمكن إيقافها مثل أي حلقة أخرى باستخدام break.

إذا لم نرد فعل أي شئ في التكرار الحالي ونريد الإنتقال إلى التكرار التالي يمكننا استخدام continue.

break/continue تدعم العناوين قبل الحلقة. العنوان هو الطريقة الوحيدة ل break/continue للإنتقال بين الحلقات المتداخلة إلى الحلقة الخارجية.

مهمه

ما هي آخر قيمة سيتم عرضها ؟ ولماذا ؟

let i = 3;

while (i) {
  alert( i-- );
}

الإجابة: 1.

let i = 3;

while (i) {
  alert( i-- );
}

كل تكرار يقوم بتقليل قيمة i بمقدار 1. وتقوم while(i) بإيقاف الحلقة عندما i = 0.

وهكذا تكون الخطوات كالتالي:

let i = 3;

alert(i--); // عرض 3, تقليل i إلى 2

alert(i--) // عرض 2, تقليل i إلى 1

alert(i--) // عرض 1, تقليل i إلى 0

// تم, while(i) تتوقف الحلقة

في كل تكرار قم بكتابة القيمة التي سيتم عرضها وقارن إجابتك مع الحل.

هل تقوم كلتا الحلقتين بعرض نفس القيم أم لا ؟

  1. صيغة prefix ++i:

    let i = 0;
    while (++i < 5) alert( i );
  2. صيغة postfix i++

    let i = 0;
    while (i++ < 5) alert( i );

السؤال يوضح كيف يمكن للصيغ postfix/prefix أن تؤدي إلى نتائج مختلفة عندما تستخدم للمقارنة.

  1. من 1 إلى 4

    let i = 0;
    while (++i < 5) alert( i );

    أول قيمة هي i = 1 لأن ++i أولًا تزيد i ثم تقوم بإرجاع القيمة الجديدة. لذلك فإن أول عملية مقارنة هي 1 < 5 ويقوم alert بعرض 1.

    ثم يتبع ب 2, 3, 4… – واحدًا بعد الآخر. المقارنة دائما تستخدم القيمة بعد الزيادة لأن ++ قبل المتغير.

    أخيرًا i = 4تزيد إلى 5 والمقارنة while(5 < 5) تفشل وتتوقف الحلقة. لذلك لا يتم عرض 5 .

  2. من 1 إلى 5

    let i = 0;
    while (i++ < 5) alert( i );

    أول قيمة هي i = 1. صيغة postfix i++ تزيد i وترجع القيمة القديمة ولذلك تكون المقارنة i++ < 5 تستخدم i = 0 (على العكس من ++i < 5).

    ولكن استدعاء alert منفصل. فهو ينفذ بعد الزيادة والمقارنة. لذلك يحصل على القيمة الحالية i = 1.

    ثم يتبعه 2, 3, 4…

    لنتوقف عند i = 4. صيغة prefix ++i تزيدها وتستخدم 5 في المقارنة. لكن postfix i++ تزيد i إلى 5وترجع القيمة القديمة. فتكون المقارنة while(4 < 5) – true وينفذ alert.

    قيمة i = 5 هي آخر قيمة لأن في الخطوة التالية while(5 < 5) تكون false.

قم بكتابة القيمة التي ستعرض في كل كل حلقة وقارنها بالحل.

هل ستقوم كلتا الحلقتين بعرض نفس القيم أم لا ؟

  1. صيغة postfix:

    for (let i = 0; i < 5; i++) alert( i );
  2. صيغة prefix:

    for (let i = 0; i < 5; ++i) alert( i );

الإجابة: من 0 إلى 4 في كلتا الحالتين.

for (let i = 0; i < 5; ++i) alert( i );

for (let i = 0; i < 5; i++) alert( i );

يمكن إيجاد الحل بسهولة من خوارزمية for:

  1. تنفذ لمرة واحدة i = 0 قبل أي شئ (begin).
  2. تفحص الشرط i < 5
  3. لو true – تنفذل الأمر alert(i) ثم i++

الزيادة i++ منفصلة عن فحص الشرط (2). هي فقط أمر آخر.

لا يتم استخدام القيمة الراجعة من عملية الزيادة لذلك لا فرق بين i++ و ++i.

استخدم حلقة for لعرض الأرقام الزوجية من 2 إلى 10.

قم بتشغيل العرض التوضيحي

for (let i = 2; i <= 10; i++) {
    if (i % 2 == 0) {
        alert(i);
    }
}

نستخدم عامل باقي القسمة % لإيجاد باقي القسمة وفحص إذا كان الرقم زوجي.

اعد كتابة الكود التالي وقم بتبديل حلقة for بحلقة while دون تغيير الخرج.

for (let i = 0; i < 3; i++) {
  alert( `number ${i}!` );
}
let i = 0;
while (i < 3) {
  alert( `number ${i}!` );
  i++;
}

أكتب حلقة تطلب رقم أكبر من 100. إذا قام المستخدم بإدخال رقم آخر اسأله مرة أخرى.

يجب أن تظل تسأل المستخدم حتى يدخل رقم أكبر من 100 أو يوقف الإدخال أو يدخل نص فارغ.

هنا نفترض أن المستخدم يدخل فقط أرقام فلا داعي للتأكد من القيم الغير رقمية.

قم بتشغيل العرض التوضيحي

let num;

do {
  num = prompt("ادخل رقم أكبر من 100?", 0);
} while (num <= 100 && num);

حلقة do..while تكرر طالما الشرطين محققين:

  1. شرط num <= 100 – أن القيمة لا تزال أقل من أو تساوي 100.
  2. والشرط && num يكون false عندما num تكون null أو نص فارغ. وهنا تتوقف حلقة while.

لاحظ أن إذا كان num يساوي null يكون الشرط num <= 100 يساوي true لذلك لن تتوقف عملية الإدخال إذا قام المستخدم بإلغائها فكلا الشرطين مطلوب.

أي رقم صحيح أكبر من 1 يسمى رقم أولي إذا كان لا يقبل القسمة إلا على 1 ونفسه فقط.

مثلًا الرقم 5 هو رقم أولي لأنه لا يقبل القسمة على 2, 3 و 4.

اكتب برنامج يعرض الأرقام الأولية بين 2 و n.

إذا كانت n = 10 يكون الناتج 2,3,5,7.

يجب أن يعمل البرنامج مع أي قيمة ل n وليس قيمة ثابتة.

يوجد العديد من الخوارزميات لهذا السؤال.

دعنا نستخدم حلقات متداخلة:

For each i in the interval {
  check if i has a divisor from 1..i
  if yes => the value is not a prime
  if no => the value is a prime, show it
}

الكود باستخدام عناوين:

let n = 10;

nextPrime:
for (let i = 2; i <= n; i++) { // for each i...

  for (let j = 2; j < i; j++) { // look for a divisor..
    if (i % j == 0) continue nextPrime; // not a prime, go next i
  }

  alert( i ); // a prime
}

يوجد العديد من الطرق لجلعه أفضل. مثلا يمكننا البحث عن القيم التي تقبل القسمة من 2 حتى الجذر التربيعي ل i. ولكن إذا أردنا أن نكون أفضل مع الأرقام الكبيرة يجب استخدام بعض الرياضيات المعقدة وخوارزميات مثل Quadratic sieve, General number field sieve etc.

خريطة الدورة التعليمية