שליטה בתהליך

תהליך הבקרה הוא הסדר שבו רכיב התרגום של JavaScript מפעיל הצהרות. אם סקריפט לא כולל הצהרות שמשנות את הזרימה, הוא יופעל מההתחלה ועד הסוף, שורה אחר שורה. מבני בקרה משמשים כדי לקבוע אם קבוצה של הצהרות תבוצע על סמך קבוצת קריטריונים מוגדרת, להוציא לפועל קבוצה של הצהרות שוב ושוב או לקטוע רצף של הצהרות.

הצהרות מותנות

הצהרות מותנות קובעות אם יש להפעיל קוד על סמך תנאי אחד או יותר. הצהרה מותנית מפעילה את הקוד שהיא מכילה אם התנאי המשויך (או קבוצת התנאים) מקבל את הערך true. אחרת, המערכת תדלג על הקוד.

ifelse

ההצהרה if בודקת תנאי בתוך הסוגריים המותאמים שאחריה. אם התנאי בתוך הסוגריים מקבל את הערך true, מתבצעת הפעלה של ההצהרה או פקודת הבלוק שאחרי הסוגריים המותאמים:

if ( true ) console.log( "True." );
> "True."

if ( true ) {
    const myString = "True.";
    console.log( myString );
}
> "True."

אם התנאי בתוך הסוגריים יהיה false, המערכת תתעלם מההצהרה שאחריה:

if ( false ) console.log( "True." );

מילת מפתח else שמיד אחרי הצהרת if וההצהרה המתבצעת באופן מותנה מציינת את ההצהרה שצריך לבצע אם התנאי if שווה false:

if ( false ) console.log( "True." )''
else console.log( "False" );
> "False."

כדי לשרשר יחד כמה הצהרות if, אפשר ליצור את ההצהרה המתבצעת באופן מותנה אחרי else הצהרה נוספת של if:

const myCondition = 2;
if ( myCondition === 5 ) console.log( "Five." );
else if ( myCondition === 2 ) console.log( "Two." );

כדי לשפר את הקריאות, מומלץ מאוד להשתמש בתחביר של הצהרת חסימה בהתאם לתנאים הבאים, אבל else if סעיפים הם בדרך כלל יוצאי דופן במקרה הזה:

if ( myCondition === 5 ) {
    console.log( "Five." );
} else if ( myCondition === 3 ) {
    console.log( "Three" );
} else {
    console.log( "Neither five nor three." );
}
> "Neither five nor three."

מפעיל Ternary

if מריץ הצהרה באופן מותנה. האופרטור המשולש (באופן מדויק יותר, אבל נקרא פחות אופרטור המותנה השלישי) הוא קיצור שמשמש להפעלת ביטוי מותנה. כפי שאפשר להבין מהשם, האופרטור המשולש הוא האופרטור היחיד של JavaScript שמשתמש בשלושה אופרנדים:

  • תנאי שצריך להעריך ואחריו סימן שאלה (?).
  • הביטוי שצריך להפעיל אם התנאי מקבל את הערך true, ואחריו נקודתיים (:).
  • הביטוי שצריך להפעיל אם התנאי מקבל את הערך false.

משתמשים בדרך כלל כדי להגדיר או להעביר ערך באופן מותנה:

const myFirstResult  = true  ? "First value." : "Second value.";
const mySecondResult = false ? "First value." : "Second value.";

myFirstResult;
> "First value."

mySecondResult;
> "Second value."

switchcase

השתמש במשפט switch כדי להשוות בין הערך של ביטוי לרשימה של ערכים אפשריים שהוגדרה באמצעות מילת מפתח אחת או יותר של case. התחביר הזה יוצא דופן כי הוא מגיע מכמה מההחלטות המוקדמות ביותר לגבי עיצוב של JavaScript. התחביר switch...case משתמש במילת המפתח switch, ואחריה ביטוי שרוצים להעריך מוקף בסוגריים, ואחריו זוג תואם של סוגריים מסולסלים. הגוף של switch יכול להכיל case מילות מפתח, בדרך כלל אחת או יותר, ואחריהן ביטוי או ערך, ואחריו נקודתיים (:).

כש רכיב התרגום מגיע ל-case עם ערך שתואם לביטוי שנבדק בסוגריים אחרי מילת המפתח switch, הוא יריץ את כל ההצהרות הבאות אחרי הסעיף case:

switch ( 2 + 2 === 4 ) {
  case false:
    console.log( "False." );
  case true:
    console.log( "True." );
}
> "True."

כל ההצהרות שמופיעות אחרי ה-case התואם מבוצעות, גם אם הן סגורות בהצהרת חסימה.

switch ( 2 + 2 === 4 ) {
    case false:
    console.log( "False." );
  case true:
    let myVariable = "True.";
    console.log( myVariable );

}
> "True."

אחת הטעויות בשימוש ב-switch…case היא שאחרי שנמצא התאמה, המתרגם של JavaScript יוציא לפועל הצהרה כל שקשורה ל-case שנמצאה בו התאמה, אפילו לסעיפים אחרים של case. המעבר ל-case הבא נקרא 'מעבר אל':

switch ( 2 + 2 === 7 ) {
    case false:
    console.log( "False." );
  case true:
    console.log( "True." );
}
> "False."
> "True."

כדי למנוע נפילה של כל מקרה לגופו, יש לסיים כל בקשת תמיכה במילת המפתח break, שתפסיק באופן מיידי את ההערכה של הגוף switch:

switch ( 2 + 2 === 7 ) {
    case false:
    console.log( "False." );
    break;
  case true:
    console.log( "True." );
    break;
}
> "False."

אם אין case שתואם לערך המותנה, switch בוחר את המשפט default אם קיים:

switch ( 20 ) {
    case 5:
    console.log( "The value was five." );
    break;
  case 10:
    console.log( "The value was ten." );
    break;
  default:
    console.log( "The value was something unexpected." );
}
> "The value was something unexpected."

עם זאת, מעבר בין כותרות חל גם על default, מה שעלול להוביל לתוצאות לא צפויות. כדי לתקן זאת, מסיימים את ההצהרה default ב-break או מציבים אותה בסוף רשימת המקרים.

switch ( 20 ) {
  default:
    console.log( "The value was something unexpected." );
  case 10:
    console.log( "The value was ten." );
    break;
  case 5:
    console.log( "The value was five." );
    break;
}
> The value was something unexpected.
> The value was ten.

סעיפים של case לא דורשים הצהרת חסימה כדי לקבץ מספר הצהרות, ולכן סעיפים case ו-default לא יוצרים היקף מילוני בעצמם:

let myVariable;
switch ( true ) {
  case true:
    let myVariable = "True.";
    break;
  default:
    let myVariable = "False.";
    break;
}
> Uncaught SyntaxError: redeclaration of let myVariable

כדי לנהל את ההיקף, צריך להשתמש בהצהרות חסימה:

let myVariable;
switch ( true ) {
  case true: {
    let myVariable = "True.";
    break;
  }
  default: {
    let myVariable = "False.";
    break;
  }
}

לולאות ואיטרציה

לולאות מאפשרות לחזור על סדרת הצהרות כל עוד תנאי מתקיים או עד שתנאי מסוים מתקיים. אפשר להשתמש בלולאות כדי להריץ קבוצת הוראות מספר קבוע של פעמים, עד שמתקבלת תוצאה ספציפית, או עד שהמתרגם מגיע לסוף של מבנה נתונים שניתן לחזור עליו (למשל, הרכיב האחרון במערך, במפה או בקבוצה, במאפיין הסופי של אובייקט או בתו האחרון במחרוזת).

לולאות מפריעות לזרימה 'מלמעלה למטה' של הפעלת הסקריפט על ידי חזרה על קבוצת הצהרות עד שתנאי אחד או יותר מתקיימים או לא מתקיימים יותר, בהתאם לתחביר שבו נעשה שימוש ליצירת הלולאה. אחרי שהלולאה מסתיימת, הביצוע ממשיך להצהרות הבאות אחריו. בדוגמה הבאה, ההצהרות בגוף הלולאה מבוצעות שלוש פעמים לפני שהמתרגם ממשיך הלאה:

let iterationCount = 0;
console.log( "Pre-loop." );
while( iterationCount < 3 ) {
  iterationCount++;
  console.log( "Loop iteration." );
}
console.log( "Continuing on." );
> "Pre-loop."
> "Loop iteration."
> "Loop iteration."
> "Loop iteration."
> "Continuing on."

אם התנאים לא יכולים להתקיים במהלך ביצוע הלולאה, הלולאה תמשיך ללא הגבלה. הלולאות האינסופיות האלה הן טעות נפוצה בתכנות, שעשויה לגרום לשרשור הביצוע הראשי להשהות ללא הגבלת זמן, או אפילו לקרוס כרטיסייה בדפדפן.

הדוגמה הבאה תופעל כל עוד הערך הבוליאני true נשאר true. מכיוון שערכים בוליאניים לא ניתנים לשינוי, נוצרת לולאה אינסופית.

console.log( "Pre-loop." );
while( true ) {
console.log( "Loop iteration." );
}
> "Pre-loop."
> "Loop iteration."
> "Loop iteration."
> "Loop iteration."
> "Loop iteration."
> "Loop iteration."
…

יש להימנע מהשארת לולאות אינסופיות בקוד הייצור. אם יצרתם בטעות קישור כזה במהלך הפיתוח, תוכלו לתקן אותו על ידי סגירת הכרטיסייה בדפדפן שבה הוא פועל, עדכון הקוד כך שהלולאה כבר לא תהיה אינסופית ופתיחה מחדש של הדף.

while

לולאת while נוצרת באמצעות מילת המפתח while ואחריה זוג סוגריים תואמים שמכילים תנאי להערכה. אם התנאי שצוין מקבל את הערך true בהתחלה, תתבצע הפעלה של ההצהרה (או פקודת הבלוק) שאחרי הסוגריים האלה. אם לא, הלולאה לעולם לא תרוץ. אחרי כל איטרציה, התנאי נבדק מחדש ואם הוא עדיין true, הלולאה חוזרת.

let iterationCount = 0;
while( iterationCount < 3 ) {
  iterationCount++;
  console.log( `Loop ${ iterationCount }.` );
}
> "Loop 1."
> "Loop 2."

אם התרגום מוצא הצהרת continue בלולאת while, הוא מפסיק את האיטרציה, מעריך מחדש את התנאי וממשיך את הלולאה אם אפשר:

let iterationCount = 0;
while( iterationCount <= 5 ) {
  iterationCount++;
  if( iterationCount === 3 ) {
    continue;
  }
  console.log( `Loop ${ iterationCount }.` );
}
console.log( "Loop ended." );
> "Loop 1."
> "Loop 2."
> "Loop 4."
> "Loop 5."
> "Loop ended."

אם המתורגמן מוצא הצהרה break בלולאה while, האיטרציה הזו נפסקת והתנאי לא מוערך מחדש, ומאפשר לו להמשיך:

let iterationCount = 1;
while( iterationCount <= 5 ) {
  if( iterationCount === 3 ) {
    console.log(`Iteration skipped.``);`
    break;
  }
  console.log( `Loop ${ iterationCount }.` );
  iterationCount++;
}
console.log( "Loop ended." );
> "Loop 1."
> "Loop 2."
> "Iteration skipped.
> "Loop ended."

אפשר להשתמש ב-while כדי לחזור מספר מסוים של פעמים, כפי שאפשר לראות בדוגמה הקודמת, אבל התרחיש לדוגמה הנפוץ ביותר ל-while הוא לולאה באורך לא ידוע:

let randomize = () => Math.floor( Math.random() * 10 );
let randomNum = randomize();
while( randomNum !== 3 ){
  console.log( `The number is not ${ randomNum }.` );
  randomNum = randomize();
}
console.log( `The correct number, ${ randomNum }, was found.` );
> "The number is not 0."
> "The number is not 6."
> "The number is not 1."
> "The number is not 8."
> "The correct number, 3, was found."

dowhile

do...while הוא וריאנט של לולאת while שבה ההערכה המותנית מתרחשת בסוף כל איטרציה של הלולאה. המשמעות היא שגוף הלולאה תמיד מופעל פעם אחת לפחות.

כדי ליצור לולאת do...while, משתמשים במילת המפתח do ואחריה ההצהרה (או פקודת החסימה) שתבוצע בכל איטרציה של הלולאה. מיד אחרי ההצהרה, מוסיפים while ומתאימים סוגריים עם התנאי שצריך להעריך. כשהתנאי הזה לא מוערך יותר ל-true, הלולאה מסתיימת.

let iterationCount = 1;
do {
  console.log( `Loop ${ iterationCount }.` );
  iterationCount++;
} while ( iterationCount < 3 );
> "Loop 1."
> "Loop 2."
> "Loop 3."

כמו בלולאת while, התרחיש לדוגמה הנפוץ ביותר ל-do...while הוא לולאה באורך לא ידוע:

let randomNum;
do {
  randomNum = ( () => Math.floor( Math.random() * 10 ) )();
  console.log( `Is the number ${ randomNum }?` );
} while ( randomNum !== 3 );
console.log( `Yes, ${ randomNum } was the correct number.` );
> "Is the number 9?"
> "Is the number 2?"
> "Is the number 8?"
> "Is the number 2?"
> "Is the number 3?"
> "Yes, 3 was the correct number."

for

משתמשים בלולאות for כדי לחזור על כמות ידועה. בבסיסי קוד מדור קודם, נעשה שימוש לעיתים קרובות באיטרציה על הרכיבים במערך.

כדי ליצור לולאת for, משתמשים במילת המפתח for ואחריה קבוצת סוגריים שמקבלת את שלושת הביטויים הבאים לפי הסדר ומופרדים בתווי סמיקול:

  1. ביטוי שיש להעריך כשהלולאה מתחילה
  2. תנאי שקובע אם הלולאה צריכה להמשיך
  3. ביטוי שצריך לבצע במסקנה של כל לולאה

אחרי הסוגריים האלה מוסיפים את ההצהרה (בדרך כלל הצהרת חסימה) שתבוצע במהלך הלולאה.

for( let i = 0; i < 3; i++ ) {
  console.log( "This loop will run three times.")
}

הביטוי הראשון מפעיל משתנה שפועל כמונה. הביטוי הזה מוערך פעם אחת, לפני האיטרציה הראשונה של הלולאה. אפשר לאתחל את המשתנה הזה באמצעות let (או באופן היסטורי var) כמו כל משתנה אחר, וההיקף שלו הוא גוף הלולאה. למשתנים האלה יכול להיות כל מזהה חוקי, אבל לעיתים קרובות הם נקראים i עבור 'איטרציה' או 'אינדקס'. נראה שהשיטה הזו סותרת את השיטות המומלצות לעבודה עם שמות של מזהים שניתנים לחיזוי, אבל המוסכמה הזו מבוססת מספיק כדי שתהיה ברורה למפתחים אחרים במבט חטוף. מכיוון שאוספים שנוספו לאינדקס לא נכללים באינדקס, למשתנים האלה יש כמעט תמיד ערך ראשוני של 0.

כמו במקרים אחרים של לולאה, התנאי הוא ביטוי שקובע אם יש לבצע את הלולאה. בדרך כלל משתמשים באפשרות הזו כדי להגדיר גבול עליון למונה האיטרציה. התרגום מעריך את התנאי לפני הפעלת הלולאה for בפעם הראשונה.אם התנאי לא מבצע בתחילה את הערך true, גוף הלולאה לא מופעל.

הביטוי הסופי מבוצע בסוף כל איטרציה בלולאה. לרוב משתמשים בו כדי להגדיל את המזהה ב-1.

בדרך כלל, for לולאות חוזרות דרך מערכים בבסיסי קוד ישנים יותר. במקרים כאלה, התנאי שמצוין להמשך הלולאה הוא מספר איטרציה קטן או שווה לאורך המערך שעובר חזרה. המשתנה המשמש למעקב אחר מספר האיטרציה הנוכחי משמש לחיפוש הערך שמשויך לאינדקס הזה במערך, וכך ניתן לבצע פעולה לגבי כל רכיב במערך לפי הסדר:

var myArray = [ true, false, true ];
for( let i = 0; i <= myArray.length; i++ ) {
  console.log( myArray[ i ] );
}
> true
> false
> true

הגישה הזו יצאה משימוש לטובת גישות מודרניות יותר למעבר בין מבני נתונים חוזרים.

for [...] of [...]

שימוש בלולאות for...of... כדי לחזור על הערכים שמאוחסנים במבנה נתונים קבוע, כמו מערך, קבוצה או מפה.

לולאת for...of... משתמשת במילת המפתח for ואחריה קבוצה של סוגריים שמכילות משתנה, אחריה of, וחוזרת על מבנה הנתונים. המשתנה יכול להיות הצהרה שמבוצעת כאן באמצעות let, const או var, משתנה שהוצהר קודם לכן במסגרת ההיקף הנוכחי, מאפיין אובייקט או מופע של משימת השמדה. היא מכילה את הערך של הרכיב שתואם לאיטרציה הנוכחית של הלולאה.

const myIterable = [ true, false, true ];
for( const myElement of myIterable ) {
  console.log( myElement );
}
> true
> false
> true

בדוגמה הזו, השימוש ב-const ל-myElement פועל על אף ש-myElement מקבל ערך חדש בכל איטרציה של הלולאה. הסיבה לכך היא שהמשתנים שמוצהרים באמצעות let או const מוקפים בהצהרת החסימה בתוך הלולאה. המשתנה מאותחל בתחילת כל איטרציה, ומוסר בסיום כל איטרציה.

forin

שימוש בלולאות for...in... כדי לחזור על המאפיינים בני ספירה של אובייקט, כולל מאפיינים שעברו ספירה בירושה. כמו בלולאה for...of..., גם כאן לולאת for...in... משתמשת במילת המפתח for ואחריה קבוצה של סוגריים שמכילים משתנה שמכיל את הערך של מפתח המאפיין שתואם לאיטרציה הנוכחית של הלולאה. אחרי המשתנה הזה מופיעה מילת המפתח in, ולאחר מכן האובייקט חוזר על:

const myObject = { "myProperty" : true, "mySecondProperty" : false };
for( const myKey in myObject ) {
  console.log( myKey );
}
> "myProperty"
> "mySecondProperty"

שוב, למרות שהערך של myKey משתנה עם כל איטרציה של הלולאה, אפשר להשתמש ב-const ללא שגיאה, כי המשתנה נמחק בפועל בסוף כל איטרציה, ואז נוצר מחדש בהתחלה.

הערך שמשויך לכל מפתח מאפיין לא זמין ישירות לתחביר for...in.... עם זאת, מכיוון שללולאה יש גישה למפתח המאפיין בכל איטרציה, תוכלו להשתמש במפתח הזה כדי "לחפש" את הערך שלה:

const myObject = { "myProperty" : true, "mySecondProperty" : false };
for( const myKey in myObject ) {
  const myValue = myObject[ myKey ];
  console.log( `${ myKey } : ${ myValue }` );
}
> "myProperty : true"
> "mySecondProperty : false"

המאפיינים שעברו בירושה מבנאים מובנים אי אפשר לספור אותם. כלומר, for...in... לא חוזר על עצמו דרך מאפיינים שהועברו בירושה מה-constructor Object. עם זאת, כל המאפיינים המספורים בשרשרת אב הטיפוס של האובייקט נכללים:

const myPrototype = { "protoProperty" : true };
const myObject = Object.create( myPrototype, {
    myProperty: {
    value: true,
    enumerable: true
    }
});
for ( const myKey in myObject ) {
  const myValue = myObject[ myKey ];
  console.log( `${ myKey } : ${ myValue }` );
}
> "myProperty : true"
> "protoProperty : true"

JavaScript מספק שיטות מובנות לקביעה אם נכס הוא נכס ישיר של האובייקט ולא מאפיין בשרשרת האב טיפוס של האובייקט: השיטות המודרניות Object.hasOwn() והשיטות Object.prototype.hasOwnProperty() הקודמות. השיטות האלה בודקות אם המאפיין שצוין עובר בירושה (או לא מוצהר), ומחזיר true רק למאפיינים המיידיים של האובייקט שצוין:

const myPrototype = { "protoProperty" : true };
const myObject = Object.create( myPrototype, {
    myProperty: {
    value: true,
    enumerable: true
    }
});
for ( const myKey in myObject ) {
  const myValue = myObject[ myKey ];
  if ( Object.hasOwn( myObject, myKey ) ) {
    console.log( `${ myKey } : ${ myValue }` );
  }
}
> "myProperty : true"

יש גם שלוש שיטות סטטיות שכל אחת מחזירה מערך שמורכב ממפתחות ספירה של אובייקט (Object.keys()), ערכים (Object.values()) או צמדי מפתח-ערך (Object.entries()):

const myObject = { "myProperty" : true, "mySecondProperty" : false };
Object.keys( myObject );
> Array [ "myProperty", "mySecondProperty" ]

כך אפשר לבצע איטרציה על מפתחות של אובייקטים, ערכים או צמדי מפתח/ערך (באמצעות משימת מחיקה) בלי לכלול מאפיינים שבבעלות אב הטיפוס של אותו אובייקט:

const myPrototype = { "protoProperty" : "Non-enumerable property value." };
const myObject = Object.create( myPrototype, {
    myProperty: {
    value: "Enumerable property value.",
    enumerable: true
    }
});

for ( const propKey of Object.keys( myObject ) ) {
  console.log( propKey );
}
> "myProperty"

for ( const propValue of Object.values( myObject ) ) {
  console.log( propValue );
}
> "Enumerable property value."

for ( const [ propKey, propValue ] of Object.entries( myObject ) ) {
  console.log( `${ propKey } : ${ propValue }` );
}
> "myProperty : Enumerable property value."

forEach()

השיטות forEach() שמספקות הבנאים Array, Map, Set ו-NodeList מספקות קיצור דרך שימושי לאיטרציה על מבנה נתונים בהקשר של פונקציית קריאה חוזרת. בניגוד לצורות אחרות של לולאה, לא ניתן להפריע ללולאה שנוצרה באמצעות כל method forEach() באמצעות break או continue.

forEach היא שיטה בבעלות אב הטיפוס של כל מבנה נתונים. כל שיטה forEach מצפה לפונקציית קריאה חוזרת כארגומנט, אם כי יש הבדלים קלים בין המונחים של הארגומנטים הכלולים כשהפונקציה נקראת. ארגומנט שני אופציונלי מציין ערך של this שישמש כהקשר להפעלה של פונקציית הקריאה החוזרת.

פונקציית הקריאה החוזרת בשימוש עם Array.forEach מספקת פרמטרים שמכילים את הערך של הרכיב הנוכחי, את האינדקס של הרכיב הנוכחי ואת המערך שבו הופעלה השיטה forEach:

const myArray = [ true, false ];
myArray.forEach( ( myElement, i, originalArray ) => {
  console.log( i, myElement, originalArray  );
});
> 0 true Array(3) [ true, false ]
> 1 false Array(3) [ true, false ]

פונקציית הקריאה החוזרת בשימוש עם Map.forEach מספקת פרמטרים שמכילים את הערך שמשויך לאלמנט הנוכחי, את המפתח שמשויך לרכיב הנוכחי ואת השיטה 'מיפוי' הופעלה השיטה forEach:

const myMap = new Map([
  ['myKey', true],
  ['mySecondKey', false ],
]);
myMap.forEach( ( myValue, myKey, originalMap ) => {
    console.log( myValue, myKey, originalMap  );
});
> true "myKey" Map { myKey → true, mySecondKey → false }
> false "mySecondKey" Map { myKey → true, mySecondKey → false }

קריאה חוזרת (callback) של Set.forEach כוללת פרמטרים דומים. מכיוון של-Set אין אינדקסים או מפתחות נפרדים לערכים, הארגומנט השני מספק ערך מיותר, שניתן להתעלם ממנו, רק כדי לשמור על התחביר עקבי עם השיטות האחרות של forEach.

const mySet = new Set([ true, false ]);
mySet.forEach( ( myValue, myKey, originalSet ) => {
  console.log( myValue, myKey, originalSet  );
});
> true true Set [ true, false ]
> false false Set [ true, false ]

איטרטורים

רכיב iterable הוא כל מבנה נתונים שמורכב מאלמנטים נפרדים שאפשר לחזור עליהם באמצעות הגישות שפירטנו קודם. איטרטור הוא אובייקט איטרטור שתואם לפרוטוקול איטרטור, כלומר הוא צריך ליישם שיטת next() שמתקדמת בין הרכיבים, היא מכילה אחד בכל פעם, בכל פעם שמופעלת לשיטה הזו, ומחזירה אובייקט לכל רכיב רציף בפורמט ספציפי.

מבני הנתונים המובנים שניתנים להעברה ב-JavaScript (כמו מערך, מפה ו-Set) אינם איטרטורים בעצמם, אבל כולם יורשים שיטת iterator שניתן לגשת אליה באמצעות @@iterator הסמל המוכר, שמחזיר אובייקט איטרטור שנוצר ממבנה הנתונים האיטרטור:

const myIterable = [ 1, 2, 3 ];
const myIterator = myIterable[ Symbol.iterator ]();

myIterable;
> (3) [1, 2, 3]

myIterator;
> Array Iterator {}

קריאה לשיטה next() בשלבים באיטרטור שהיא מכילה, אחד בכל פעם, וכל קריאה מחזירה אובייקט שמכיל שני מאפיינים: value, שמכיל את הערך של האלמנט הנוכחי ו-done, ערך בוליאני שמציין אם האיטרטור עבר את הרכיב האחרון במבנה הנתונים. הערך של done הוא true רק כאשר קריאה ל-next() מובילה לניסיון לגשת לרכיב מעבר לאלמנט האחרון באיטרטור.

const myIterable = [ 1, 2, 3 ];
const myIterator = myIterable[ Symbol.iterator ]();

myIterator.next();
> Object { value: 1, done: false }

myIterator.next();
> Object { value: 2, done: false }

myIterator.next();
> Object { value: 3, done: false }

myIterator.next();
> Object { value: undefined, done: true }

פונקציות גנרטור

משתמשים במילת המפתח function* (שימו לב לכוכבית) כדי להצהיר על פונקציית מחולל או כדי להגדיר ביטוי של פונקציית מחולל:

function* myGeneratorFunction() { };

בדומה לאיטרטורים, פונקציות המחולל שומרות על מצב. קריאה לפונקציית מחולל מחזירה אובייקט Generator חדש, אבל לא מפעילה את הקוד באופן מיידי בגוף הפונקציה:

function* myGeneratorFunction() {
  console.log( "Generator function body ")
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject;
> Generator {  }

typeof myGeneratorObject;
> "object"

אובייקטים גנרטיביים פועלים לפי פרוטוקול איטרטור. הערך שכל קריאה ל-next() בפונקציה של מחולל מחזירה נקבע באמצעות ביטוי yield, שמשהה את הביצוע של פונקציית המחולל ומחזיר את הערך של הביטוי שמכיל את מילת המפתח yield. קריאות מאוחרות יותר אל next() ממשיכות את הביצוע של הפונקציה, ומשהות בביטוי הבא של yield ומחזירות את הערך המשויך.

function* myGeneratorFunction() {
  yield "My first yielded value.";
  yield "My second yielded value.";
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next();
> Object { value: "My first yielded value.", done: false }

myGeneratorObject.next();
> Object { value: "My second yielded value.", done: false }

כאשר מפעילים את next() לאחר שלא צוינו ערכים נוספים באמצעות yield, return או throw (במקרה של שגיאה), שארית הפונקציה גוף מבצעת והאובייקט שהוחזר מכיל value של undefined ומאפיין done של true:


function* myGeneratorFunction() {
    console.log( "Start of the generator function." );
    yield "First";
    console.log( "Second part of the generator function."  );
    yield "Second";
    console.log( "Third part of the generator function." );
    yield "Third";
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next();
> "Start of the generator function."
> Object { value: "First", done: false }

myGeneratorObject.next();
> "Second part of the generator function."
> Object { value: "Second", done: false }

myGeneratorObject.next();
> "Third part of the generator function."
> Object { value: "Third", done: false }

myGeneratorObject.next();
> Object { value: undefined, done: true }

השתמשו ב-next() רק באובייקט שהפונקציה של המחולל מחזירה, ולא בפונקציית המחולל עצמה. אחרת, כל קריאה לפונקציית המחולל תיצור אובייקט מחולל חדש:

function* myGeneratorFunction() {
  yield "First";
  yield "Second";
};

myGeneratorFunction().next();
> Object { value: "First", done: false }

myGeneratorFunction().next();
> Object { value: "First", done: false }

כמו בכל פונקציה, פונקציית המחולל נעצרת כשהיא נתקלת במילת מפתח return. לאחר מכן היא מחזירה אובייקט להקשר ההפעלה שמכיל את הערך שהוחזר ומאפיין done עם הערך true.

function* myGeneratorFunction() {
  yield 1;
  yield 2;
  return 3;
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next().done;
> Object { value: 1, done: false }

myGeneratorObject.next().done;
> Object { value: 2, done: false }

myGeneratorObject.next();
> Object { value: 3, done: true }

ביטוי yield יכול לקבל חלק מהסמנטיקה של מזהה, וכך לאפשר 'תקשורת' דו-כיוונית מהחלק המושעה של פונקציית המחולל ובחזרה אליו. כשערך מועבר ל-method next() של מחולל כארגומנט, הוא מחליף את הערך שמשויך לביטוי yield שהושעה הקודם:

function* myGeneratorFunction() {
    const firstYield = yield;
    yield firstYield + 10;
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next();
> Object { value: undefined, done: false }

myGeneratorObject.next( 5 );
> Object { value: 15, done: false }

חשוב לזכור שהפעולה הזו מחליפה את כל הביטוי שמשויך ל-yield הקודם, ולא מקצים מחדש את הערך של yield הקודם לערך שצוין ב-next():

function* myGeneratorFunction() {
    const firstYield = yield;
    const secondYield = yield firstYield + 100;
    yield secondYield + 10;
};
const myGeneratorObject = myGeneratorFunction();

myGeneratorObject.next();
> Object { value: undefined, done: false }

myGeneratorObject.next( 10 ); // Can be thought of as changing the value of the `firstYield` variable to `10
> Object { value: 110, done: false }

myGeneratorObject.next( 20 ); // Can be thought of as changing the value of the `secondYield` variable to `20`, _not_ `20 + 100;`
> Object { value: 30, done: false }

המערכת מתעלמת מכל ארגומנט שמועבר לקריאה הראשונה אל next() מכיוון שאין ביטוי yield קודם שיכול לקבל את הערך הזה. כמו בכל פונקציה אחרת, ארגומנטים שמועברים לקריאה הראשונית של הפונקציה של המחולל זמינים בכל היקף הגוף של פונקציית המחולל:

function* myGeneratorFunction( startingValue ) {
    let newValue = yield startingValue + 1;
    newValue = yield newValue + 10;
    yield startingValue + 20;
};
const myGeneratorObject = myGeneratorFunction( 2 );

myGeneratorObject.next( 1 );
> Object { value: 3, done: false }

myGeneratorObject.next( 5 );
> Object { value: 15, done: false }

myGeneratorObject.next( 10 );
Object { value: 22, done: false }

האופרטור yield* (שימו לב לכוכבית) משמש עם אפשרות איטרטיבית, כמו פונקציית מחולל אחרת, כדי לחזור ולהפיק כל ערך שהאופרנד שלו מחזיר:

function* mySecondaryGenerator() {
  yield 2;
  yield 3;
}

function* myGenerator() {
  yield 1;
  yield* mySecondaryGenerator();
  yield 4;
  return 5;
}

const myIterator = myGenerator();

myIterator.next();
> Object { value: 1, done: false }

myIterator.next();
> Object { value: 2, done: false }

myIterator.next();
> Object { value: 3, done: false }

myIterator.next();
> Object { value: 4, done: false }

myIterator.next();
> Object { value: 5, done: true }

JavaScript אסינכרוני

למרות שבמהותו JavaScript הוא סינכרוני, יש מנגנונים שמאפשרים למפתחים לנצל את לולאת האירועים כדי לבצע משימות אסינכרוניות.

הבטחות

Promise הוא placeholder לערך שלא ידוע מתי ההבטחה נוצרת. זהו קונטיינר שמכתיב פעולה אסינכרונית, המונחים שלפיהם הפעולה נחשבת להצלחה או לכישלון, הפעולות שצריך לבצע בכל אחד מהמקרים והערך שמתקבל כתוצאה מכך.

יוצרים מכונה של Promise באמצעות האופרטור new עם הפונקציה המובנית של Promise. ה-constructor הזה מקבל פונקציה בשם executor כארגומנט. הפונקציה הביצוע משמשת בדרך כלל לביצוע פעולה אסינכרונית אחת או יותר, ואז מכתיבות את המונחים שבהם יש להחשיב את ההבטחה כהפרה או דחייה שלה. Promise מוגדרת כ-Pending (בהמתנה) בזמן שפונקציית הביצוע פועלת. אחרי שה-הפעלה מסיים, הבטחה נחשבת כמומשה (או נפתרה, במקורות מסוימים לתיעוד) אם פונקציית הביצוע והפעולה האסינכרונית שהיא מבצעת הושלמו בהצלחה, ונדחית אם פונקציית הביצוע נתקלת בשגיאה או שהפעולה האסינכרונית שמבוצעת נכשלת. אחרי מימוש או דחייה של התחייבות, היא נחשבת להסדרה.

const myPromise = new Promise( () => { });

ה-constructor קורא לפונקציה מבצעים עם שני ארגומנטים. הארגומנטים האלה הם פונקציות שמאפשרות לממש או לדחות באופן ידני את Promise:

const  myPromise = new Promise( ( fulfill, reject ) => { });

הפונקציות שמשמשות למימוש או לדחייה של ה-Promise מופעלות כארגומנט (בדרך כלל בתור שגיאה לדחייה):

const myPromise = new Promise( ( fulfill, reject ) => {
  const myResult = true;
  setTimeout(() => {
    if( myResult === true ) {
        fulfill( "This Promise was successful." );    
    } else {
        reject( new Error( "This Promise has been rejected." ) );
    }
  }, 10000);
});

myPromise;
> Promise { <state>: "pending" }

myPromise;
> Promise { <state>: "fulfilled", <value>: "This Promise was successful." }

שרשרת הבטחות

אפשר לבצע פעולה על אובייקט Promise שנוצר באמצעות השיטות then(), catch() ו-finally() שהועברו בירושה מ-Promise. כל אחת מהשיטות האלה מחזירה Promise (הבטחה) שאפשר לפעול לפיה באופן מיידי באמצעות then(), catch() או finally(), וכך לשרשר את ההבטחות שמתקבלות.

then() מספקת שתי פונקציות של קריאה חוזרת כארגומנטים. משתמשים במודל הראשון כדי לקיים את ההבטחה שהתקבלה, ובשני כדי לדחות אותה. שתי השיטות מקבלות ארגומנט אחד שמספק ל-Promise שמתקבל את הערך שלה.

const myPromise = new Promise( ( fulfill, reject ) => {
  const myResult = true;
  setTimeout(() => {
    if( myResult === true ) {
        fulfill( "This Promise was fulfilled." );    
    } else {
        reject( new Error( "This Promise has been rejected." ) );
    }
  }, 100);
});

myPromise.then( successfulResult => console.log( successfulResult ), failedResult => console.error( failedResult ) );
> "This Promise was successful."

אפשר גם להשתמש ב-then() כדי לטפל רק במצב הממולא וב-catch כדי לטפל במצב שנדחה. קריאה ל-catch עם ארגומנט יחיד שמכיל את הערך שצוין בשיטת הדחייה של Promise:

const myPromise = new Promise( ( fulfill, reject ) => {
  const myResult = false;
  setTimeout(() => {
    if( myResult === true ) {
        fulfill( "This Promise was fulfilled." );    
    } else {
        reject( new Error( "This Promise has been rejected." ) );
    }
  }, 100);
});

myPromise
  .then( fulfilledResult => console.log(fulfilledResult ) )
  .catch( rejectedResult => console.log( rejectedResult ) )
  .finally( () => console.log( "The Promise has settled." ) );
> "Error: This Promise has been rejected."
> "The Promise has settled."

בניגוד ל-then ול-catch, שמאפשרות לפונקציית handler לרוץ כשה-Promise מתמלא או נדחה, מתבצעת קריאה לפונקציה שמועברת כארגומנט ב-method finally, גם אם ההבטחה מומשה וגם אם נדחתה. הפונקציה של ה-handler מופעלת ללא ארגומנטים, כי היא לא מיועדת לפעול עם הערכים שהועברו מה-Promise, אלא רק כדי להפעיל את הקוד אחרי שה-Promise הושלם.

בו-זמניות (concurrency)

הבנאי של Promise מספק ארבע שיטות לעבודה עם מספר הבטחות קשורות, באמצעות רכיב iterable שמכיל אובייקטים של Promise. בכל אחת מהשיטות האלה מתקבלת הבטחה שממומשת או נדחתה בהתאם למצב ההבטחות שהועברו אליה. לדוגמה, Promise.all() יוצר הבטחה שמתקיימת רק אם כל הבטחה שהועברה לשיטה הזו מתמלאת:

const firstPromise  = new Promise( ( fulfill, reject ) => fulfill( "Successful. ") );
const secondPromise = new Promise( ( fulfill, reject ) => fulfill( "Successful. ") );
const thirdPromise  = new Promise( ( fulfill, reject ) => fulfill( "Successful. ") );
const failedPromise = new Promise( ( fulfill, reject ) => reject( "Failed.") );
const successfulPromises = [ firstPromise, secondPromise, thirdPromise ];
const oneFailedPromise = [ failedPromise, ...successfulPromises ];

Promise.all( successfulPromises )
  .then( ( allValues ) => {
    console.log( allValues );
  })
  .catch( ( failValue ) => {
    console.error( failValue );
  });
> Array(3) [ "Successful. ", "Successful. ", "Successful. " ]

Promise.all( oneFailedPromise  )
    .then( ( allValues ) => {
      console.log( allValues );
    })
    .catch( ( failValue ) => {
     console.error( failValue );
    });
> "Failed."

אלו הן השיטות של Promise בו-זמניות (concurrency).

Promise.all()
ימומש רק אם כל ההבטחות ימולאו.
Promise.any()
אם אחת מההבטחות תמומש או תידחה רק אם כל ההבטחות נדחות.
Promise.allSettled()
מומשה לאחר שההבטחות הגיעו, בלי קשר לתוצאה שלהן.
Promise.race()
נדחה או סופק על סמך התוצאה של ההבטחה הראשונה, תוך התעלמות מכל ההבטחות שהוסדרו מאוחר יותר.

async/await

כשמשתמשים במילת המפתח async לפני הצהרת פונקציה או ביטוי פונקציה, כל ערך שהפונקציה מחזירה מוחזר כהבטחה שמומשה שמכיל את הערך הזה. כך תוכלו להריץ ולנהל פעולות אסינכרוניות באמצעות אותם תהליכי עבודה כמו הפיתוח הסינכרוני.

async function myFunction() {
  return "This is my returned value.";
}

myFunction().then( myReturnedValue => console.log( myReturnedValue ) );
> "This is my returned value."

הביטוי await משהה את הביצוע של פונקציה אסינכרונית בזמן הסדרת ה-Promise המשויך. אחרי הסדרת ההבטחה, הערך של הביטוי await הוא הערך של ההבטחה שמומש או נדחה.

async function myFunction() {
  const myPromise  = new Promise( ( fulfill, reject ) => { setTimeout( () => fulfill( "Successful. "), 5000 ); });
  const myPromisedResult = await myPromise;
  return myPromisedResult;
}

myFunction()
  .then( myResult => console.log( myResult ) )
  .catch( myFailedResult => console.error( myFailedResult ) );
> "Successful."

כל ערך שאינו הבטחה שכלול בביטוי await מוחזר כהבטחה שמומשה:

async function myFunction() {
  const myPromisedResult = await "String value.";
  return myPromisedResult;
}

myFunction()
  .then( myResult => console.log( myResult ) )
  .catch( myFailedResult => console.error( myFailedResult ) );
> "String value."

בחינת ההבנה

באיזה סוג של לולאה אתם משתמשים כדי לחזור על כמות ידועה?

for
while
do...while