Към съдържанието

3. Сравнения, логика, условия и loops ​

Занятие 3упражнения: 03-conditions-and-loops/ в homework repositorydoc: примерният JSON документ

Днес ще се научите да:

  • сравнявате стойности с ===, !==, <, >= ... и да избягвате капаните на ==;
  • разбирате защо "4.10" < "4.2" е true и какво означава това за версиите;
  • комбинирате условия с &&, || и !;
  • знаете кои стойности са truthy и falsy и да задавате стойности по подразбиране с || и ??;
  • вземате решения с if / else, switch и тернарния оператор ? :;
  • спирате правилото рано, когато липсват данни (guard clause);
  • повтаряте действия с for, while и do...while и да управлявате loops с break и continue;
  • четете съществуващи правила, написани в по-стар стил (var, ==, == false).

Къде е мястото на тази тема в примерното правило за проверка - след днешното занятие можете да го четете и пишете цялото. Всяка част използва някоя от днешните теми - числото в коментара е разделът от тази лекция:

js
function rule(doc) {
    // 6. guard clauses: stop early when data is missing
    if (!doc.overview || !doc.inventory) return;

    // 1, 3. ===, &&, ||, ! - "is it one of our two models?"
    const model = doc.overview.model;
    if (model !== "XR-210" && model !== "XR-220") return;

    // 5. defaults: || also replaces an empty name, ?? only a missing value
    const name = doc.inventory.extended_info?.json?.data?.system?.name
                 || doc.status.device_alias;
    const level = doc.inventory.details.firmware_level ?? "";

    // 10. a loop with break: the version is the text before the first space
    let version = "";
    for (const ch of level) {
        if (ch === " ") break;
        version += ch;
    }
    if (version === "") return;

    // 2, 6. versions are compared part by part; the strictest condition first
    let status;
    if (versionBelow(version, "4.2.0.0")) {
        status = "Below minimum";
    } else if (versionBelow(version, "4.2.1.7")) {
        status = "Below recommended";
    } else {
        return;                                   // OK - no alert
    }

    // 7. switch: one value, several fixed answers
    let action;
    switch (status) {
        case "Below minimum":
            action = "Upgrade as soon as possible.";
            break;
        case "Below recommended":
            action = "Plan an upgrade.";
            break;
    }

    // 4, 8. "no" is truthy, so compare; the ternary returns a value
    const login = doc.inventory.details.remote_login_enabled === "yes" ? "on" : "off";

    emit(name, { "Model": model, "Version": version, "Status": status,
                 "Action": action, "Remote login": login });
}
  • versionBelow(a, b) засега е готова helper функция - собствена ще напишем в занятие 5.
  • ?. (optional chaining) е поглед напред: спира при липсващо ниво, вместо да хвърли грешка. Повече в раздел 5 и в занятие 6.

Почти всяко правило е верига от решения като тези: "има ли данни?", "това ли е моделът?", "твърде стар ли е firmware-ът?". Точните условия са най-важното умение при писането на правила.

1. Оператори за сравнение ​

Сравнението задава въпрос за две стойности. Отговорът е винаги boolean: true или false.

ОператорЗначениеПримерРезултат
===строго равно (същият тип и стойност)5 === 5true
!==строго различно5 !== "5"true
==нестрого равно (първо преобразува типовете)5 == "5"true
!=нестрого различно5 != 3true
>по-голямо от10 > 5true
<по-малко от3 < 5true
>=по-голямо или равно5 >= 5true
<=по-малко или равно4 <= 5true
js
console.log(2 == 2);        // true
console.log(2 == "2");      // true  - the string is converted to a number
console.log(2 === "2");     // false - different types, no conversion

// booleans are converted to numbers: true is 1, false is 0
console.log(true == 1);     // true
console.log(false == 0);    // true
console.log(true > false);  // true

const model = doc.overview.model;
console.log(model === "XR-220");    // true
console.log(model !== "XR-210");    // true
Променете кода и го изпълнете отново

=== срещу == ​

MDN: Strict equality (===), Equality (==)

== първо преобразува двете стойности до един и същ тип и след това ги сравнява. Правилата за преобразуване са дълги и изненадващи:

js
console.log("" == 0);           // true  - "" becomes 0
console.log("0" == false);      // true  - both become 0
console.log("no" == false);     // false - "no" becomes NaN, false becomes 0
console.log(" \n " == 0);       // true  - a string of spaces becomes 0
console.log(null == 0);         // false
console.log(null > 0);          // false
console.log(null >= 0);         // true  - but >= converts null to 0!
Променете кода и го изпълнете отново

Използвайте === и !==

Те никога не преобразуват. Сравнява се точно това, което виждате. Всички сравнения в курса използват === / !==. Единственото ==, което си струва да знаете, е по-долу.

Анимация

Типове, сравнения и логика: 3. == и ===

Единственото полезно ==: null и undefined ​

null == undefined е true, а null / undefined не са нестрого равни на нищо друго. Затова value == null означава "value е null или undefined":

js
let value;                                   // undefined
console.log(value === undefined || value === null);   // true - the long way
console.log(value == null);                           // true - the short way

value = 0;
console.log(value == null);                           // false - 0 is a real value
Променете кода и го изпълнете отново

В съществуващите правила ще видите и двете форми, и двете са правилни. В нов код изберете едната форма и я използвайте навсякъде.

NaN не е равно на нищо ​

js
const slots = Number("abc");
console.log(slots);                 // NaN
console.log(slots === NaN);         // false - even NaN !== NaN
console.log(Number.isNaN(slots));   // true  - the right check
Променете кода и го изпълнете отново

Сравняване на дробни числа ​

js
console.log(0.1 + 0.2 === 0.3);                 // false (see session 2)
console.log(Math.abs(0.1 + 0.2 - 0.3) < 1e-9);  // true  - "close enough"
// Math.abs(n) is n without its sign: the distance between the two numbers
Променете кода и го изпълнете отново

Обектите се сравняват по референция

{} === {} и [] === [] са false: два обекта са равни само ако са един и същ обект. Повече в занятие 6.

Анимации

Числата в паметта: 4. 0.1 + 0.2, 6. NaN и Infinity

2. Сравняване на strings ​

Два strings се сравняват символ по символ, по кодовете на символите (Unicode). Това се нарича лексикографски ред - като в речник:

js
console.log("b" > "azzzz");     // true - "b" > "a", the rest does not matter
console.log("a" > "A");         // true - lowercase letters have bigger codes
console.log("Z" < "a");         // true - all uppercase letters come first
console.log("1" < "11");        // true - "1" is a prefix of "11"
console.log("2" > "19999");     // true - "2" > "1"
console.log("1199" > "12");     // false - "11" < "12"
console.log("1" == "12");       // false
Променете кода и го изпълнете отново

Ако единият string е начало на другия, по-късият е по-малък.

Защо "4.10" < "4.2" е true ​

Версиите са strings. Сравнени като strings, те дават грешни отговори:

js
console.log("4.10" < "4.2");       // true  - "1" < "2" at position 2
console.log("10.1.0" < "9.0.0");   // true  - "1" < "9" at position 0
console.log("4.2.1.7" < "4.2.1.10");   // false - "7" > "1"
Променете кода и го изпълнете отново

Версията трябва да се сравнява част по част, като числа: 4 = 4, после 10 > 2. Такава helper функция, versionBelow(a, b), ще напишем в занятие 5. Дотогава задачите за упражнение ви я дават готова.

Смесени типове

Когато едната страна е number, string-ът се преобразува в number: "10" > 9 е true, но "10" > "9" е false. А "abc" < 5 и "abc" >= 5 са false и двете (NaN). Първо преобразувайте, после сравнявайте.

Главни и малки букви ​

=== различава малки и главни букви. Стойностите в JSON не винаги са написани по един и същ начин, затова сравнявайте в един регистър. toLowerCase() дава същия текст с малки букви (повече методи на strings в занятие 8):

js
const raised = "Yes";
console.log(raised === "yes");                  // false
console.log(raised.toLowerCase() === "yes");    // true
Променете кода и го изпълнете отново

Естествен ред (поглед напред)

"4.10".localeCompare("4.2", undefined, { numeric: true }) дава 1 (по-голямо), защото чете групите от цифри като числа. Полезно е за сортиране (занятие 7), но нашите формати на версиите са твърде различни (4.2.1.7-B00, 42107-C00), затова за правилата пишем собствено сравнение.

Анимация

Типове, сравнения и логика: 4. Сравняване на strings

3. Логически оператори ​

MDN: Logical AND (&&), Logical OR (||), Logical NOT (!)

Логическите оператори комбинират условия:

ОператорИмеtrue, когато
a && bИ (AND)и двете са истина
a || bИЛИ (OR)поне едното е истина
!aНЕ (NOT)a е неистина

Таблицата на истинност (T - true, F - false):

aba && ba || b!a
TTTTF
TFFTF
FTFTT
FFFFT

&& и || имат два операнда; ! има един (той е унарен).

js
console.log(true && false);    // false
console.log(true || false);    // true
console.log(!true);            // false

const userAge = 19;
const userCountry = "BG";
console.log(userAge >= 18 && userCountry === "UK");   // false (true && false)

// the same with the report
const model = doc.overview.model;
const enabled = doc.inventory.details.remote_login_enabled;

console.log(model === "XR-220" && enabled === "yes");          // true
console.log(model === "XR-210" || model === "XR-220");         // true
console.log(!(model === "XR-210" || model === "XR-220"));      // false
Променете кода и го изпълнете отново

Приоритет и скоби ​

MDN: Operator precedence

Първо се прилага !, после &&, после || - както * преди +:

js
const a = true, b = false, c = false;
console.log(a || b && c);      // true  - same as a || (b && c)
console.log((a || b) && c);    // false
Променете кода и го изпълнете отново

Когато едно условие смесва && и ||, винаги слагайте скоби - заради читателя, не само заради JavaScript:

js
if (env === "hw" || (platform === "virtual" && version !== "")) { ... }

Обръщане на условие (De Morgan) ​

За да напишете "не (A и B)", обърнете всяка част и сменете && с ||:

Товае същото като
!(a && b)!a || !b
!(a || b)!a && !b

Това ви трябва за guard clauses (раздел 6): "спри, ако моделът не е нито XR-210, нито XR-220" е if (model !== "XR-210" && model !== "XR-220") return;.

4. Truthy и falsy ​

MDN: Truthy, Falsy

if (...), !, && и || приемат всякаква стойност, не само booleans. Тогава стойността се третира като true или false. Тези осем стойности са falsy:

ТипFalsy стойности
booleanfalse
number0, -0, NaN (и 0n, който е bigint)
string"" (празният string)
-undefined, null

Всичко останало е truthy - включително "0", "false", "no", " ", [] и {}.

js
console.log(Boolean(""));        // false
console.log(Boolean("0"));       // true  - a non-empty string
console.log(Boolean("no"));      // true
console.log(Boolean(0));         // false
console.log(Boolean(NaN));       // false
console.log(Boolean({}));        // true
console.log(!!"text");           // true - !! is a short way to write Boolean()
Променете кода и го изпълнете отново

"no" е truthy

Отчетът пази настройките да / не като текст: "remote_login_enabled": "yes", "remote_login_pwd_set": "no". if (doc.inventory.details.remote_login_pwd_set) е true и за "no" - всеки непразен string е truthy. Сравнявайте със стойността, която имате предвид: === "yes".

js
const enabled = "no";
if (enabled) {
    console.log("this runs - wrong!");
}
if (enabled === "yes") {
    console.log("this does not run - right");
}
Променете кода и го изпълнете отново

Truthy проверките са добри за един въпрос: "има ли изобщо стойност?" if (doc.overview) означава "документът има част overview".

Сравняване с true / false ​

В съществуващите правила често има редове като if (present == false) или if (list.includes(x) !== true) (includes за arrays идва в занятие 4). Те работят, когато стойността е истински boolean, но == false преобразува: съвпада и с 0, "" и дори с "0" - който е truthy!

js
console.log(Boolean("0"));       // true  - "0" is truthy
console.log("0" == false);       // true  - but == false matches it
console.log(null == false);      // false - and does NOT match null
console.log(!null);              // true
Променете кода и го изпълнете отново

Пишете го по-кратко и по-безопасно:

ВместоПишете
if (present == false)if (!present) или if (present === false)
if (isAffected == true)if (isAffected)
if (list.includes(x) !== true)if (!list.includes(x))

Когато модернизирате правило, проверете какво може да бъде стойността. !present е true и за null и undefined, а present == false не е. Обикновено !present е това, което правилото е имало предвид ("липсва или не е зададено"), но все пак е промяна.

Анимация

Типове, сравнения и логика: 5. Truthy и falsy

5. Short-circuit изчисляване и стойности по подразбиране ​

&& и || спират веднага щом отговорът е ясен - това се нарича short-circuit evaluation. И те не връщат true / false, а единия от двата операнда:

ИзразВръща
a || ba, ако a е truthy, иначе b
a && ba, ако a е falsy, иначе b
js
console.log("XR-220" || "Unknown");   // XR-220
console.log("" || "Unknown");         // Unknown
console.log(undefined || "Unknown");  // Unknown

console.log("XR-220" && "OK");        // OK
console.log(null && "OK");            // null

let userName;
console.log(userName || "Anonymous"); // Anonymous
Променете кода и го изпълнете отново

|| за стойности по подразбиране ​

Примерното правило използва || за fallback: ако името на системата липсва или е празно, използва alias-а на устройството:

js
const name = doc.inventory.extended_info?.json?.data?.system?.name
             || doc.status.device_alias;
console.log(name);                   // LAB-DEV-01

const noName = "" || doc.status.device_alias;
console.log(noName);                 // the alias
Променете кода и го изпълнете отново

Съществуващите правила го използват постоянно:

js
var current = String(version || "");      // never undefined, always a string
var count = parseInt(text, 10) || 0;      // NaN becomes 0
var customer = String(alias || "Unknown");

&& като пазач (guard) ​

Дясната страна се изпълнява само ако лявата е truthy. По-старите правила го използват, за да проверят всяко ниво на един път, преди да прочетат следващото:

js
const report = { overview: { model: "XR-220" } };

console.log(report.overview && report.overview.model);    // XR-220
console.log(report.status && report.status.device_alias); // undefined - no error

// the modern form (ES2020), more in session 6:
console.log(report.status?.device_alias);                 // undefined
Променете кода и го изпълнете отново

Капанът: || заменя и 0, и "" ​

|| заменя всяка falsy стойност. Понякога 0, "" или false е истински отговор:

js
const failedDrives = 0;
console.log(failedDrives || "Not reported");   // Not reported - wrong!
console.log(failedDrives ?? "Not reported");   // 0 - right
Променете кода и го изпълнете отново

?? - nullish coalescing операторът (ES2020) ​

MDN: Nullish coalescing operator (??)

a ?? b връща b само когато a е null или undefined:

aa || "x"a ?? "x"
undefined"x""x"
null"x""x"
"""x"""
0"x"0
false"x"false
"no""no""no"
  • Използвайте ??, когато 0, "" или false са валидни стойности.
  • Използвайте ||, когато и празна стойност трябва да получи стойността по подразбиране.

ВНИМАНИЕ

a || b ?? c е синтактична грешка. Смесването на ?? с || или && изисква скоби: (a || b) ?? c.

Логическо присвояване (ES2021) ​

MDN: Logical OR assignment (||=), Nullish coalescing assignment (??=)

js
let status;
status ??= "Not reported";     // status = status ?? "Not reported"
console.log(status);

let name = "";
name ||= "Unknown";            // name = name || "Unknown"
console.log(name);
Променете кода и го изпълнете отново

ES2021+

Проверете дали вашият rule engine поддържа ??=, ||= и &&=, преди да ги използвате в правило. Самият ?? е ES2020, като ?..

Анимация

Типове, сравнения и логика: 6. Short-circuit: && и ||

6. Условни конструкции ​

MDN: if...else

if ​

js
if (condition) {
    // runs only if condition is truthy
}
js
const age = 19;
let status = "unknown";
if (age > 18) {
    status = "adult";
}
console.log(status);

const version = doc.overview.version;
if (version === "4.2.1.7") {
    console.log("exactly the recommended level");
}
Променете кода и го изпълнете отново

if...else ​

js
if (condition) {
    // runs if condition is truthy
} else {
    // runs otherwise
}
js
const x = 5;
if (x % 2) {                 // 1 is truthy, 0 is falsy
    console.log("Odd");
} else {
    console.log("Even");
}

const enabled = doc.inventory.details.remote_login_enabled;
if (enabled === "yes") {
    console.log("remote login is on");
} else {
    console.log("remote login is off");
}
Променете кода и го изпълнете отново

x % 2 работи, но x % 2 !== 0 казва какво имаме предвид. Предпочитайте явни условия. (Не x % 2 === 1: за отрицателни числа -3 % 2 е -1.)

if...else if...else ​

js
if (condition1) {
    // condition1 is true
} else if (condition2) {
    // condition1 is false, condition2 is true
} else {
    // none of the above
}
  • Винаги започва с if.
  • След това нула или повече блока else if.
  • else е последен. Не е задължителен, но обикновено е добра идея: хваща случая, за който не сте се сетили.
  • Изпълнява се само първият блок, чието условие е изпълнено.

Намерете бъга - кодът е валиден JavaScript, но дава грешни отговори:

js
const age = 25;
let status;
if (age > 12) {
    status = "teen";
} else if (age > 18) {
    status = "adult";
} else {
    status = "child";
}
console.log(status);
Променете кода и го изпълнете отново
Отговор

Редът. 25 е > 12, затова се изпълнява първият блок и до "adult" никога не се стига. Проверявайте първо най-строгото условие: age > 18, после age > 12.

Същото важи и за firmware levels. Ниво под минималното е и под препоръчителното, затова първо се проверява минималното:

js
// a ready-made helper for today - compares part by part, as numbers.
// You will understand how it works in sessions 4-5.
function versionBelow(a, b) {
    const pa = String(a).split(".").map(Number);
    const pb = String(b).split(".").map(Number);
    for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
        const x = pa[i] || 0;
        const y = pb[i] || 0;
        if (x !== y) return x < y;
    }
    return false;
}

const current = "4.1.9.3";
const minimum = "4.2.0.0";
const recommended = "4.2.1.7";

let status;
if (versionBelow(current, minimum)) {
    status = "Below minimum";
} else if (versionBelow(current, recommended)) {
    status = "Below recommended";
} else {
    status = "OK";
}
console.log(status);
Променете кода и го изпълнете отново

Сменете current на "4.2.0.5" и на "4.10.0.0" и изпълнете отново.

Анимация

Loops стъпка по стъпка: 1. if / else if

Две условия, четири отговора ​

Чест случай: два факта да / не дават четири съобщения. Проверете първо "и двете":

js
const hasTimeServer = doc.inventory.details.ntp_ip_address !== "";
const hasBandwidth = doc.inventory.details.bandwidth !== "";

let message;
if (hasTimeServer && hasBandwidth) {
    message = "time server and bandwidth reported";
} else if (hasTimeServer) {
    message = "time server reported / bandwidth not reported";
} else if (hasBandwidth) {
    message = "bandwidth reported / time server not reported";
} else {
    message = "time server and bandwidth not reported";
}
console.log(message);
Променете кода и го изпълнете отново

Липсващ ключ не е ""

Ако в отчета изобщо няма ключ ntp_ip_address, стойността е undefined, а undefined !== "" е true - кодът по-горе казва "reported". Помислете кои стойности означават "не е докладвано" (undefined, null, ""?) и проверете всички. Домашното пита точно това.

Блокове и фигурни скоби ​

{ } групира statements в блок. При един statement скобите могат да се пропуснат и ще го видите в правилата:

js
if (!doc) return;
if (version === "") return;

Това е добре за кратък return на същия ред. Навсякъде другаде пишете скобите - добавянето на втори ред по-късно без тях е класически бъг:

js
if (isAffected)
    console.log("affected");
    emit(alias, details);      // NOT part of the if - always runs!

Guard clauses: спрете рано ​

Едно правило не прави нищо за повечето документи. Вместо да влагате if в if, проверете първо какво не е наред и направете return (спрете правилото) веднага:

js
// nested - the real work is deep inside
function rule(doc) {
    if (doc.overview) {
        if (doc.overview.model === "XR-220") {
            if (doc.overview.version !== "") {
                // ... the real work
            }
        }
    }
}

// guard clauses - flat and easy to read
function rule(doc) {
    if (!doc.overview) return;
    if (doc.overview.model !== "XR-220") return;
    if (doc.overview.version === "") return;

    // ... the real work
}

return приключва функцията. Функциите идват в занятие 5 - засега четете return в правило като "спри тук, без alert".

js
function emit(id, details) {          // a fake emit, to see the result here
    console.log("ALERT for " + id + ": " + JSON.stringify(details));
}

function rule(doc) {
    if (!doc || !doc.overview) return;

    const model = doc.overview.model;
    if (model !== "XR-210" && model !== "XR-220") return;

    const enabled = doc.inventory.details.remote_login_enabled;
    if (enabled !== "yes") return;

    emit(doc.status.device_alias, { model: model, check: "remote login enabled" });
}

rule(doc);                          // the example JSON document: an alert
rule({ overview: { model: "XR-100" } });   // other model: nothing
rule({});                           // no overview: nothing, and no error
Променете кода и го изпълнете отново

7. switch ​

MDN: switch

switch сравнява една стойност с няколко фиксирани стойности. Това е алтернатива на дълга верига if...else if от проверки с ===:

js
const fruit = "apple";
switch (fruit) {
    case "apple":
        console.log("Apple is red.");
        break;
    case "banana":
        console.log("Banana is yellow.");
        break;
    default:
        console.log("Unknown fruit.");
}
Променете кода и го изпълнете отново
  • Стойността се сравнява с всеки case чрез === (строго).
  • При съвпадение кодът се изпълнява до break.
  • default се изпълнява, когато нито един case не съвпада. Пишете го последен.
js
const loginType = doc.inventory.details.remote_login_type;
let description;

switch (loginType) {
    case "central":
    case "domain":                   // two cases, one block
        description = "central user accounts";
        break;
    case "local":
        description = "local users only";
        break;
    case "":
    case "none":
        description = "no remote login";
        break;
    default:
        description = "unknown type: " + loginType;
}
console.log(description);
Променете кода и го изпълнете отново

Забравен break

Без break кодът "пропада" в следващия case:

js
const status = "Below minimum";
switch (status) {
    case "Below minimum":
        console.log("Upgrade as soon as possible.");
        // no break!
    case "Below recommended":
        console.log("Plan an upgrade.");
        break;
    default:
        console.log("No action required.");
}
Променете кода и го изпълнете отново

switch е строг

switch ("1") { case 1: ... } не съвпада: "1" === 1 е false. Първо преобразувайте string-а от JSON: switch (Number(count)).

Анимация

Loops стъпка по стъпка: 7. switch без break (advanced)

8. Условният (тернарен) оператор ​

MDN: Conditional (ternary) operator

js
condition ? valueIfTrue : valueIfFalse

Единственият оператор с три операнда. За разлика от if, той връща стойност, затова може да се присвои, да се подаде или да се сложи в template string. Тези два варианта са еднакви:

js
let x = 42;
let numType = x % 2 === 0 ? "even" : "odd";
console.log(numType);

let numType2;
if (x % 2 === 0) {
    numType2 = "even";
} else {
    numType2 = "odd";
}
console.log(numType2);
Променете кода и го изпълнете отново

С отчета:

js
const env = "hw";
const recommended = env === "hw" ? "4.2.1.7" : "4.1.0.0";
console.log(recommended);

const enabled = doc.inventory.details.remote_login_enabled === "yes";
console.log(`remote login: ${enabled ? "on" : "off"}`);
Променете кода и го изпълнете отново

Един тернарен оператор е добре, два са твърде много

a ? b : c ? d : e е валидно, но трудно за четене. Вместо това използвайте if...else if.

Анимация

Типове, сравнения и логика: 7. Тернарен оператор (advanced)

9. Statements и expressions ​

MDN: Statements and declarations, Expressions and operators

Кодът на JavaScript се състои от два вида части:

  • Expression произвежда стойност. Може да стои навсякъде, където се очаква стойност.
  • Statement прави нещо, но няма стойност. Такива са декларациите, if, switch, return и loops от следващия раздел (for, while).
js
let x, y;                 // statement (declaration)
if (x) { x = 0; }         // statement
switch (x) { }            // statement

2 + 3;                    // expression: 5
x = 5;                    // expression: assignment has a value too (5)
x ** 6 - x;               // expression
x = y = 5;                // (y = 5) is an expression, its value 5 goes into x
model === "XR-220";       // expression: true or false
enabled ? "on" : "off";   // expression

Ето защо тернарният оператор работи в template string, а if - не:

js
const enabled = true;
console.log(`auth: ${enabled ? "on" : "off"}`);     // fine
// console.log(`auth: ${if (enabled) {"on"}}`);     // syntax error
Променете кода и го изпълнете отново

Анимация

Как се изчислява един израз: 6. Statement срещу expression (advanced)

10. Loops ​

MDN: Loops and iteration, for, while

Loop повтаря блок от код. В JavaScript за loops използваме:

  • for (най-често)
  • while и do...while (рядко)
  • for...of (от ES2015) - за arrays и strings; ще го разгледаме в занятие 4, раздел 8.

for ​

js
for (init; condition; update) {
    // repeated while condition is true
}

Стъпките:

  1. init се изпълнява веднъж, най-напред. Обикновено декларира variable, например let i = 0, с която loop-ът итерира.
  2. Проверява се condition: ако е true, блокът се изпълнява; ако е false, loop-ът приключва.
  3. update се изпълнява след блока (обикновено i++).
  4. Обратно към стъпка 2.
js
for (let i = 0; i < 5; i++) {
    console.log(`i=${i}`);
}
Променете кода и го изпълнете отново

Анимация

Loops стъпка по стъпка: 2. Loop-ът for

Четните числа от 2 до 10:

js
for (let i = 2; i <= 10; i += 2) {
    console.log(`i=${i}`);
}
Променете кода и го изпълнете отново

Съставяне на имена на слотове или дискове:

js
for (let slot = 1; slot <= 4; slot++) {
    console.log(`drive_f01_slot_${slot}`);
}
Променете кода и го изпълнете отново

Обратно броене:

js
for (let tries = 5; tries > 0; tries--) {
    console.log(`Try: ${tries}`);
}
Променете кода и го изпълнете отново

var в стари правила

Съществуващите правила често пишат for (var i = 0; ...). Тук работи по същия начин, но i продължава да съществува след loop-а. В нов код пишете let; разликата е обяснена в занятие 5.

Loops върху string ​

Един string има дължина (length) и всеки символ има позиция (индекс), като се брои от 0:

js
const version = "4.2.1.7";
console.log(version.length);     // 7
console.log(version[0]);         // 4
console.log(version[1]);         // .
console.log(version[version.length - 1]);   // 7 - the last character

for (let i = 0; i < version.length; i++) {
    console.log(i, version[i]);
}
Променете кода и го изпълнете отново

String-ът все пак е примитив

В лекция 2 string беше примитив - една проста стойност, а не обект. А тук има .length и [0], като обект с properties. JavaScript прави това вместо вас, зад кулисите; как работи е обяснено в лекция 8 ("Обвивки на примитивите"). Засега: можете да четете символите по индекс, но не можете да ги променяте - version[0] = "5" не прави нищо.

Пребройте частите на една версия (точките + 1):

js
const version = "11.7.23.0";
let dots = 0;
for (let i = 0; i < version.length; i++) {
    if (version[i] === ".") {
        dots++;
    }
}
console.log(`${dots + 1} parts`);
Променете кода и го изпълнете отново

for...of (ES2015) дава директно символите, когато не ви трябва индексът:

js
let digits = 0;
for (const ch of "4.2.1.7") {
    if (ch >= "0" && ch <= "9") {      // string comparison: is it a digit?
        digits++;
    }
}
console.log(digits);
Променете кода и го изпълнете отново

Рисуване на елха: вложени loops ​

Loop в loop. "*".repeat(n) дава n звездички:

js
// half tree
const treeDeep = 5;
for (let i = 1; i <= treeDeep; i++) {
    console.log("*".repeat(i));
}
Променете кода и го изпълнете отново
js
// full tree
const crownHeight = 6;
const boleHeight = 2;
const boleWidth = 3;
let spaceCount;

// the crown:
for (let i = 1; i <= crownHeight; i++) {
    spaceCount = crownHeight - i;
    console.log(" ".repeat(spaceCount) + "*".repeat(i * 2 - 1));
}

// the bole:
spaceCount = Math.floor((crownHeight * 2 - 1 - boleWidth) / 2);
for (let i = 1; i <= boleHeight; i++) {
    console.log(" ".repeat(spaceCount) + "*".repeat(boleWidth));
}
Променете кода и го изпълнете отново

Същото с два истински вложени loops - всеки ред съставя звездичките си една по една:

js
for (let row = 1; row <= 4; row++) {
    let line = "";
    for (let col = 1; col <= row; col++) {
        line += "*";
    }
    console.log(line);
}
Променете кода и го изпълнете отново

Анимация

Loops стъпка по стъпка: 6. Вложени loops (advanced)

while ​

js
while (condition) {
    // repeated while condition is true
}

Условието се проверява преди всеки кръг. Нещо в блока трябва да го промени, иначе loop-ът никога не свършва:

js
let userTries = 5;
while (userTries > 0) {
    console.log(`Try: ${userTries}`);
    userTries--;            // without this line the loop is endless!
}
Променете кода и го изпълнете отново

Този пример е по-добър като for (вижте по-горе): броят на кръговете е известен. Използвайте while, когато не знаете предварително колко кръга ще трябват:

js
// Math.random() gives a random number from 0 to 1 (Math: session 8).
// The machine "thinks of" a number:
const machineNumber = Math.round(Math.random() * 10);
console.log(`machineNumber: ${machineNumber}`);

// The user "enters" numbers until they guess it:
let userNumber = Math.round(Math.random() * 10);
console.log(`userNumber: ${userNumber}`);
while (userNumber !== machineNumber) {
    userNumber = Math.round(Math.random() * 10);
    console.log(`new userNumber: ${userNumber}`);
}
Променете кода и го изпълнете отново

Проверете се: бихте ли написали същата логика с for loop?

Отговор

Да. Първото предположение отива в init, новото предположение - в update:

js
for (let n = Math.round(Math.random() * 10);
     n !== machineNumber;
     n = Math.round(Math.random() * 10)) {
    console.log(`new userNumber: ${n}`);
}

Всеки while може да се напише като for с празни init и update: for (; condition; ) { ... }. Изберете този, който се чете по-добре - тук това е while.

С отчета - намерете всички точки с indexOf, който връща позицията на текст в string или -1, ако текстът го няма:

js
const version = doc.overview.version;
let pos = version.indexOf(".");
let count = 0;

while (pos !== -1) {
    count++;
    console.log(`dot at position ${pos}`);
    pos = version.indexOf(".", pos + 1);   // search again after this dot
}
console.log(`${count} dots`);
Променете кода и го изпълнете отново

indexOf(...) !== -1 в стари правила

platform.indexOf("virtual") !== -1 означава "platform съдържа virtual". Модерната форма е platform.includes("virtual") (ES2015). А model.indexOf("XR-") === 0 означава "започва с" - днес model.startsWith("XR-").

Анимация

Loops стъпка по стъпка: 3. while и безкрайният loop

do...while ​

Като while, но условието се проверява след блока - затова блокът се изпълнява поне веднъж:

js
do {
    // runs once, then again while condition is true
} while (condition);
js
const machineNumber = Math.round(Math.random() * 10);
console.log(`machineNumber: ${machineNumber}`);

let userNumber;
do {
    userNumber = Math.round(Math.random() * 10);
    console.log(`userNumber: ${userNumber}`);
} while (userNumber !== machineNumber);
Променете кода и го изпълнете отново

do...while се среща рядко в правилата; разпознавайте го, когато го видите.

Анимация

Loops стъпка по стъпка: 4. do...while

break и continue ​

  • break - излез от loop-а веднага.
  • continue - прескочи останалата част от този кръг и мини към следващия.

Изрежете версията от firmware level - спрете при първия интервал:

js
const level = doc.inventory.details.firmware_level;   // "4.2.1.7 (build ...)"
let version = "";

for (let i = 0; i < level.length; i++) {
    if (level[i] === " ") {
        break;                  // the version ends here
    }
    version += level[i];
}
console.log(level);
console.log(version);
Променете кода и го изпълнете отново

Запазете само цифрите и точките - прескочете всичко останало:

js
const text = "v4.2-1.7";
let clean = "";

for (const ch of text) {
    const isDigit = ch >= "0" && ch <= "9";
    if (!isDigit && ch !== ".") {
        continue;               // skip this character
    }
    clean += ch;
}
console.log(clean);             // 4.21.7 - careful, "-" was a separator!
Променете кода и го изпълнете отново

Резултатът показва, че почистването на данни иска внимание: прескачането на - слепи две части. В занятие 8 ще направим това както трябва, с regular expressions.

Безкрайни loops

Ако условието никога не стане false, страницата или правилото увисва. В браузъра затворете таба; в терминала натиснете Ctrl+C. Преди да пуснете while, запитайте се: какво променя условието?

Анимация

Loops стъпка по стъпка: 5. break и continue

11. Четене на съществуващи правила ​

Много съществуващи правила са написани в по-стар стил (отпреди ES2015). Те използват само това, което знаете от тази лекция и от лекция 2 - просто написано по друг начин. Ето едно кратко правило в този стил. Тук emit е фалшив: само отпечатва alert-а.

js
function emit(id, text) {          // fake: prints instead of creating an alert
    console.log("ALERT for " + id + ": " + text);
}

function rule(doc) {
    if (!doc.overview || !doc.inventory || !doc.inventory.details) return;

    var model = doc.overview.model;
    if (model != "XR-210" && model != "XR-220") return;

    var details = doc.inventory.details;
    var loginOn = details.remote_login_enabled == "yes";
    if (loginOn == false) return;

    var level = details.firmware_level || "";
    var version = "";
    for (var i = 0; i < level.length; i++) {
        if (level[i] == " ") break;
        version = version + level[i];
    }

    var name = doc.status && doc.status.device_alias || "unknown device";
    emit(name, "remote login is on, firmware " + version);
}

rule(doc);
Променете кода и го изпълнете отново

Старите форми и какво пишете днес:

Стар стилОзначаваДнес
var x = ...variableconst (или let, ако се променя)
a != b, a == bсравнение с преобразуване на типа!==, ===
if (loginOn == false)"не е включено"if (!loginOn)
x || ""стойност по подразбиране за всяка falsy стойностx ?? "" (само за null / undefined)
a && a.ba.b, само ако a съществуваa?.b
s = s + xдобавя в краяs += x
"text " + xсъединява strings`text ${x}`

Същото правило в модерен стил:

js
function emit(id, text) {          // fake: prints instead of creating an alert
    console.log(`ALERT for ${id}: ${text}`);
}

function rule(doc) {
    if (!doc.overview || !doc.inventory?.details) return;

    const model = doc.overview.model;
    if (model !== "XR-210" && model !== "XR-220") return;

    const details = doc.inventory.details;
    if (details.remote_login_enabled !== "yes") return;

    const level = details.firmware_level ?? "";
    let version = "";
    for (let i = 0; i < level.length; i++) {
        if (level[i] === " ") break;
        version += level[i];
    }

    const name = doc.status?.device_alias ?? "unknown device";
    emit(name, `remote login is on, firmware ${version}`);
}

rule(doc);
Променете кода и го изпълнете отново

Проверете се:

  1. Каква е стойността на version за примерния JSON документ и защо loop-ът спира там?
  2. device_alias е "" (празен). Кое име използва всяко от правилата?
  3. remote_login_enabled липсва. Създава ли някое от правилата alert?
Отговор
  1. "4.2.1.7". Нивото на firmware е "4.2.1.7 (build ...)", а break прекратява loop-а при първия интервал.
  2. Старото правило: "unknown device" - || заменя всяка falsy стойност, също и "". Модерното правило: "" - ?? заменя само null и undefined. Когато модернизирате, решете кое от двете е нужно на правилото (раздел 5).
  3. Не. undefined == "yes" е false, затова loginOn е false и старото правило спира. undefined !== "yes" е true, затова и модерното правило спира.

12. Упражнения ​

Във вашето копие на Homework repository първо вземете новите задачи:

bash
git pull --no-rebase --no-edit upstream main
node 03-conditions-and-loops/practice/task1-predict.js

Всеки файл обяснява задачата в началото си. Пишете кода си между редовете your code; проверките най-долу извеждат OK или FAIL.

ЗадачаФайлКакво правите
1task1-predict.jsПредвидете 18 сравнения и логически изрази, после проверете
2task2-firmware-status.jsКласифицирайте firmware level (if...else if) и изберете текста на действието (switch)
3task3-rule-condition.jsУсловието на примерното правило, с guard clauses за липсващи данни
4task4-version-loops.jsИзрязване, броене и почистване на версии с loops, break и continue
5task5-defaults.js|| или ?? - показване на липсващи стойности, без да се губят 0 и false

Обобщение ​

ТемаЗапомнете
СравненияВинаги boolean. Използвайте === / !==; == null само за "null или undefined"
StringsСравняват се символ по символ: "4.10" < "4.2" е true. Сравнявайте версиите част по част
Логика&&, ||, !; скоби при смесване; !(a && b) е !a || !b
Truthy / falsyFalsy: false, 0, NaN, "", null, undefined. "no" и "0" са truthy
Стойности по подразбиранеa || b заменя всяка falsy стойност; a ?? b - само null / undefined
ifПърво най-строгото условие; изпълнява се само първото съвпадение; guard clauses с return
switchЕдна стойност, ===, break след всеки case, default накрая
Тернарен операторcond ? a : b връща стойност; само едно ниво
Loopsfor, когато броят е известен, while, когато не е, do...while - поне веднъж
break / continueИзлизане от loop-а / прескачане към следващия кръг
Стар стилvar, ==, == false, a && a.b - четете го, пишете const / let, ===, !, ?.

Домашно ​

  1. 03-conditions-and-loops/homework.js (в homework repository): текст за състоянието на две настройки и първата позиция, на която две версии се различават.
  2. Задачите за упражнение, които не сте довършили в час - препоръчително, защото следващите задачи и курсовият проект ги използват отново. Всяка проверка трябва да отпечата OK.
  3. По желание: вземете едно от собствените си правила и избройте всяко условие в него. За всяко: кои документи пропуска? Има ли ==, || "" или == false, които биха могли да ви изненадат?

Още за четене ​

Следващото занятие: arrays - списъци от модели, дискове и записи. model !== "XR-210" && model !== "XR-220" от днешното правило става allow-list: !["XR-210", "XR-220"].includes(model).

Създаден за