Управління потоком виконання та обробка помилок

Переклад цієї статті ще не завершено. Будь ласка, допоможіть перекласти цю статтю з англійської мови

JavaScript підтримує компактний набір інструкцій, зокрема інструкцій для управління потоком, які можна використати, щоб включити інтерактивність до вашого додатку. Цей розділ надає огляд таких інструкцій.

Довідник JavaScript містить вичерпні дані про інструкції, описані у цьому розділі. Символ крапки з комою (;) розділяє інструкції у JavaScript-коді.

Будь-який вираз у JavaScript також являється інструкцією. Повну інформацію про вирази дивіться у розділі Вирази та оператори.

Блокова інструкція

Найбільш базовою інструкцією є блокова, що об'єднує кілька інструкцій в одну. Блок обмежується парою фігурних дужок:

{
  інструкція_1;
  інструкція_2;
  .
  .
  .
  інструкція_n;
}

Приклад

Блокова інструкція зазвичай використовується поряд з інструкціями для управління потоком виконання (наприклад, if, for, while).

while (x < 10) {
  x++;
}

У даному випадку, { x++; } є блоковою інструкцією.

Важливо: JavaScript до ECMAScript2015 не мав блокової області видимості. Змінні, об'явлені всередині блоку, були видимі в області меж зовнішньої функції або ж усього скрипта. І ефект від їхнього зміни поширювався за межі блоку. Інакше кажучи, блокові інструкції не створювали області видимості.

"Поодинокі" блоки у JavaScript можуть мати зовсім інші наслідки, ніж у мовах C чи Java. Наприклад:

var x = 1;
{
  var x = 2;
}
console.log(x); // в результаті - 2

В результаті отримуємо 2, тому що інструкція var x всередині блоку перебуває в одній області видимості з інструкцією var x перед блоком. У C чи Java подібний код поверне 1.

Починаючи з ECMAScript2015, декларації змінних let і const мають блокову область видимості. Докладніше на довідкових сторінках let і const.

Умовні інструкції

Умовною інструкцією називається набір команд, що виконаються, якщо певна умова буде істинною. JavaScript підтримує два види умовних інструкцій: if...else та switch.

Інструкція if...else

Використовуйте if, щоб виконати інструкцію, якщо логічна умова являється істинною. Використовуйте необов'яковий else, щоб виконати інструкцію, якщо умова являється хибною. Інструкція if виглядає так:

if (умова) {
  інструкція_1;
} else {
  інструкція_2;
}

Тут умова може бути будь-яким виразом, що зводиться до true чи false (дивіться розділ Boolean для роз'яснення, що і як обчислюється до true чи false). Якщо умова обчислюється до true, виконується інструкція_1; інакше виконується інструкція_2. інструкція_1 та інструкція_2 можуть бути будь-якими, включаючи вкладені інструкції if.

Можна також суміщувати інструкції у вигляді else if, щоб послідовно перевірити кілька умов, як-от, наприклад:

if (умова_1) {
  інструкція_1;
} else if (умова_2) {
  інструкція_2;
} else if (умова_n) {
  інструкція_n;
} else {
  інструкція_остання;
} 

У випадку наявності кількох таких умов буде виконано лише найперший обчислений до true. Щоб виконати кілька інструкцій, слід об'єднати їх у блок ({ ... }) .

Найкращі практики

Загалом, хорошою практикою вважається завжди використовувати блоки інструкцій, особливо при вкладенні інструкцій if:

if (умова) {
  інструкція_1_виконується_якщо_умова_правильна;
  інструкція_2_виконується_якщо_умова_правильна;
} else {
  інструкція_3_виконується_якщо_умова_хибна;
  інструкція_4_виконується_якщо_умова_хибна;
}
Не рекомендується використовувати звичайні присвоєння в умовних виразах, бо присвоєння можна сплутати з порівнянням при перегляді коду.
Наприклад, не слід писати ось так:
// Ймовірно, буде прочитано як "x == y"
if (x = y) {
  /* statements here */
}

Якщо все ж таки потрібно використати присвоєння всередині умовного виразу, загальною практикою є додавання зайвих дужок навколо присвоєння, як-от:

if ((x = y)) {
  /* інструкції тут */
}

Хибні значення

Наступні значення обчислюються до false (також знані як Falsy значення):

  • false
  • undefined
  • null
  • 0
  • NaN
  • порожня стрічка ("")

Всі інші значення (включно з усіма об'єктами), обчислюються до true при передачі в умовний вираз.

Увага: не плутайте примітивні булеві значення true та false із істинними і хибними значеннями об'єкту Boolean. Наприклад:

var b = new Boolean(false);
if (b) // цей умовний вираз буде істинним
if (b == true) // цей умовний вираз буде хибним

Приклад

У наведеному далі прикладі функція checkData повертає true, якщо в об'єкті  Text три символи, а інакше вона показує сповіщення та повертає false.

function checkData() {
  if (document.form1.threeChar.value.length == 3) {
    return true;
  } else {
    alert('Enter exactly three characters. ' +
    document.form1.threeChar.value + ' is not valid.');
    return false;
  }
}

Інструкція switch

Інструкція switch дозволяє програмі оцінити вираз і спробувати співставити значення виразу з міткою case. Якщо відповідність знайдено  , програма виконує пов'язану з цим інструкцію.

Інструкція switch виглядає наступним чином:

switch (expression) {
  case label_1:
    statements_1
    [break;]
  case label_2:
    statements_2
    [break;]
    ...
  default:
    statements_def
    [break;]
}

JavaScript розуміє switch інструкцію наступним чином:

  • Програма спочатку шукає пункт case із міткою, що відповідає значенню виразу, а потім передає керування цьому пунктові, у якому виконуються відповідні інструкції.
  • Якщо відповідної мітки не знайдено, програма шукає необов'язковий пункт default:
    • Якщо пунт default знайдено, програма передає керування йому, виконуючи відповідні інструкції.
    • Якщо пункту default не знайдено, програма продовжує виконання інструкцій після закінчення switch.
    • (Традиційно, пункт default записується останнім, але це не обов'язково.)

Інструкція break

Необов'язкова інструкція break, пов’язана із кожним пунктом case, забезпечує те, що програма виходить з конструкції switch, якщо виконується відповідна умова, і продовжує виконання інструкцій після конструкції switch. Якщо break не використовується, програма продовжує виконання інструкції наступних пунктів всередині switch, не перевіряючи умови.

Приклад

У наступному прикладі, якщо fruittype дорівнює "Bananas", програма виконує пов'язану з пукнтом "Bananas" інструкцію. Коли виникає break, програма припиняє switch та виконує наступну після switch інструкцію. Якщо break б була опущена, інструкції для пункту "Cherries" також би виконалися.

switch (fruittype) {
  case 'Oranges':
    console.log('Oranges are $0.59 a pound.');
    break;
  case 'Apples':
    console.log('Apples are $0.32 a pound.');
    break;
  case 'Bananas':
    console.log('Bananas are $0.48 a pound.');
    break;
  case 'Cherries':
    console.log('Cherries are $3.00 a pound.');
    break;
  case 'Mangoes':
    console.log('Mangoes are $0.56 a pound.');
    break;
  case 'Papayas':
    console.log('Mangoes and papayas are $2.79 a pound.');
    break;
  default:
   console.log('Sorry, we are out of ' + fruittype + '.');
}
console.log("Is there anything else you'd like?");

Інструкції для обробки винятків

Використовуючи інструкцію throw , можна викидати виключення і обробляти їх за допомогою інструкцій try...catch.

Типи винятків

В якості виключення у JavaScript можна викинути практично будь-який об'єкт. Тим не менш, не всі викинуті об'єкти створено однаковими. Позаяк доволі звичною практикою є викидати числа чи стрічки в якості помилок, часто краще використовувати якийсь із типів винятків, спеціально створених для цих потреб:

Оператор throw

Скористайтеся оператором throw, щоб викинути виняток. Коли викидаєте виняток, ви вказуєте вираз, що містить значення, яке викидається:

throw expression;

Ви можете викинути будь-який вираз, не тільки вирази окремих типів. Наступний код викидає кілька винятків різних типів:

throw 'Error2';   // тип String
throw 42;         // тип Number
throw true;       // тип Boolean
throw {toString: function() { return "Я об'єкт!"; } };
Примітка: Ви можете вказати об'єкт, коли викидаєте виняток. Після цього ви можете звертатися до властивостей об'єкта у блоці catch.
// Створити об'єкт UserException
function UserException(message) {
  this.message = message;
  this.name = 'UserException';
}

// Гарненько оформити виняток у разі використання в якості рядка
// (наприклад, у консолі помилок)
UserException.prototype.toString = function() {
  return this.name + ': "' + this.message + '"';
}

// Створити екземпляр об'єкта та викинути його
throw new UserException('Значення завелике');

Інструкція try...catch

Інструкція try...catch позначає блок інструкцій, які програма спробує виконати, і вказує одну чи більше реакцій на викинутий виняток. Коли викидається виняток, інструкція try...catch його ловить.

Інструкція try...catch складається з блока try, що містить одну чи більше інструкцій, і блоку catch, що містить інструкції до виконання у разі, якщо у блоці try буде викинуто виняток.

Інакше кажучи, ми хочемо, щоб блок try успішно виконався. Але якщо так не станеться, ми хочемо, щоб управління перейшло до блоку catch. Практично, якщо будь-яка з інструкцій всередині блоку try (або всередині функції, викликаної зсередини блоку try) викидає виняток, управління одразу передається до блоку catch. У разі, якщо блок try не викинув жодного винятку, блок catch пропускається. А блок finally виконується після виконання try та catch і до інструкцій, що слідують після Інструкції try...catch.

Наступний приклад містить інструкцію try...catch. У прикладі викликається функція, що повертає назву місяця з масиву, керуючись номером, переданим у функцію. Якщо число не відповідає номерові місяця (112), викидається виняток зі стрічкою "InvalidMonthNo" в якості значення, а інструкції, описані у блоці catch, встановлюють значення змінної monthName у 'unknown'.

function getMonthName(mo) {
  mo = mo - 1; // Коригуємо порядковий номер місяця для використання в якості
               // індекса масиву (1 = Jan, 12 = Dec)
  var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
                'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  if (months[mo]) {
    return months[mo];
  } else {
    throw 'InvalidMonthNo'; // тут застосуємо ключове слово throw
  }
}

try { // інструкції, які ми намагаємося виконати
  monthName = getMonthName(myMonth); // функція може викинути виняток
}
catch (e) {
  monthName = 'unknown';
  logMyErrors(e); // передаємо значення винятку в обробник помилок (наприклад, вашу власну функцію)
}

Блок catch

Блок catch можна використовувати для обробки всіх винятків, що можуть виникнути при виконання блоку try.

catch (catchID) {
  інструкції
}

Блок catch задає ідентифікатор (catchID у прикладі вище), що містить значення, вказане оператором throw. За допомогою цього ідентифікатора можна отримати інформацію про виняток, який було викинуто.

JavaScript створює цей ідентифікатор, як тільки управління переходить до блоку catch. Ідентифікатор існує доти, доки виконується catch. Як тільки блок catch завершує роботу, ідентифікатор знищується.

Наприклад, наступний код викидає виняток. Коли це відбувається, управління передається до блоку catch.

try {
  throw 'myException'; // створює виняток
}
catch (e) {
  // інструкції для обробки якихось винятків
  logMyErrors(e); // передає об'єкт винятку до обробника помилок
}

Найкращі практики: Під час запису помилок до консолі всередині блоку catch для потреб зневадження рекомендується використовувати console.error() замість console.log(). Це одразу форматує повідомлення як помилку, і також додає його до загального списку помилок, що трапилися на сторінці. 

Блок finally

Блок finally містить інструкції для виконання після завершення роботи блоку try...catch. Блок finally також виконається перед кодом, що слідує безпосередньо за оператором try…catch…finally.

Також важливо зауважити, що блок finally буде виконано незалежно від того, чи було викинуто виняток. Однак, якщо виняток таки було викинуто, інструкції блоку finally буде виконано, навіть якщо жоден блок catch не обробив викинутий виняток.

Блок finally можна використовувати, щоб заставити скрипт "м'яко впасти", коли трапляється виняток. Наприклад - для того, щоб звільнити ресурс, взятий скриптом.

Приклад, наведений далі, відкриває файл і виконує певні дії з ним. (JavaScript на сервері дозволяє отримувати доступ до файлів.) Якщо викидається виняток, поки файл було відкрито, блок finally закриває файл перед тим, як скрипт впаде. Застосування finally тут гарантує, що файл ні в якому разі не залишиться відкритим, навіть якщо трапиться помилка.

openMyFile();
try {
  writeMyFile(theData); // Це може викинути помилку
} catch(e) {  
  handleError(e); // Обробити помилку, якщо вона трапиться
} finally {
  closeMyFile(); // Завжди закривати ресурс
}

Якщо блок finally повертає якесь значення, це значення стає результатом роботи всього блоку try-catch-finally, незалежно від будь-яких інструкцій return всередині try чи catch:

function f() {
  try {
    console.log(0);
    throw 'bogus';
  } catch(e) {
    console.log(1);
    return true; // ця інструкція чекатиме 
                 // допоки не виконається блок finally
    console.log(2); // недосяжний код
 } finally {
    console.log(3);
    return false; // переписує попередній "return"
    console.log(4); // недосяжний код
  }
  // тепер виконується "return false"
  console.log(5); // недосяжний код
}
f(); // console 0, 1, 3; returns false

Переписування значень "return" блоком finally також стосується винятків, викинутих (можливо, повторно) всередині блоку catch :

function f() {
  try {
    throw 'bogus';
  } catch(e) {
    console.log('упіймано внутрішній "bogus"');
    throw e; // ця інструкція чекатиме,
             // допоки не виконається бльок finally
  } finally {
    return false; // переписує попередній "throw"
  }
  // тепер виконується "return false"
}

try {
  f();
} catch(e) {
  // цей код ніколи не виконається, бо "throw" 
  // усередині "catch" переписано 
  // "return"ом у "finally"
  console.log('упіймано зовнішній "bogus"');
}

// ВИВІД:
// упіймано внутрішній "bogus"

Вкладені інструкції try...catch

Можна вкладати одну чи більше інструкцій try...catch. Якщо внутрішня інструкція try...catch не має блоку catch, мусить бути блок finally, і блок catch зовнішної інструкції try...catch буде перевірено на збіг. За детальнішою інфорацією див. nested try-blocks на сторінці try...catch.

Обробка об'єктів Error

Залежно від типу помилки, ви зможете використати властивості 'name' і 'message' для покращеного повідомлення. 'name' переважно називає клас, похідний від класу Error (напр., 'DOMException' або 'Error'), а 'message' традиційно дає стисліше повідомлення, ніж те, що виходить після конвертування об'єкта помилки у string.

Якщо ви викидаєте влавні винятки, то для користі з цих властивостей (напр., щоб ваш блок catch відрізняв ваші винятки від системних) сожете скористатися конструктором Error. Наприклад:

function doSomethingErrorProne() {
  if (ourCodeMakesAMistake()) {
    throw (new Error('Повідомлення'));
  } else {
    doSomethingToGetAJavascriptError();
  }
}
....
try {
  doSomethingErrorProne();
} catch (e) {
  console.log(e.name); // logs 'Error'
  console.log(e.message); // logs 'Повідомлення' або JavaScript error message)
}

Обіцянки (Promises)

Починаючи з ECMAScript2015, JavaScript дістав підтримку об'єктів Promise, що дає змогу контролювати плин відкладених і асинхронних операцій.

Обіцянка (Promise) має один зі станів:

  • очікування (pending): початковий стан, не виконаний ані відхилений.
  • виконано (fulfilled): успішна операція
  • відхилено (rejected): операція провалилася.
  • вирішено (settled): виконано або відхилено, але не очікування.

Завантаження образу з допомогою XHR

Простий приклад використання Обіцянки (Promise) і XMLHttpRequest для завантаження образу міститься в репозиторії MDN GitHub js-examples repository. Ви також можете бачити її в дії: see it in action. Кожен крок прокоментовано, що дає змогу зблизька побачити архітектуру Обіцянок (Promises) і XHR. Тут наведено розкоментовану версію, що показує плин Обіцянки (Promise) , тож ви можете схопити ідею:

function imgLoad(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open('GET', url);
    request.responseType = 'blob';
    request.onload = function() {
      if (request.status === 200) {
        resolve(request.response);
      } else {
        reject(Error('Image didn\'t load successfully; error code:' 
                     + request.statusText));
      }
    };
    request.onerror = function() {
      reject(Error('There was a network error.'));
    };
    request.send();
  });
}

За деталями звертайтеся до сторінки Promise і посібника Using Promises.