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

5. Функции и четене на по-стар код ​

Занятие 5упражнения: 05-functions/ в homework repositorydoc: примерният JSON документ

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

  • пишете функция като declaration, като expression и като arrow function и да я извиквате;
  • подавате аргументи на параметрите, да давате на параметрите стойности по подразбиране и да връщате резултат с return;
  • знаете къде се вижда една variable (scope), какво е shadowing и защо let / const замениха var;
  • обяснявате hoisting - защо някои функции могат да се извикат преди своя ред, а други - не;
  • знаете какво става, когато функцията получи array или обект, и да пишете чисти helper функции, които не променят отчета;
  • подавате функция на друга функция (callback) - написана като function (x) { ... } и като x => ...;
  • пишете helper функции вътре в други функции и да знаете какво е closure;
  • изграждате правило от малки helper функции, в реда, който rule engine-ът очаква, и да напишете собствена versionBelow(a, b);
  • четете по-старо правило - var, вложени helper функции, callbacks function (x) { return ...; }, try / catch - и да го модернизирате стъпка по стъпка.

Раздели, отбелязани с (advanced) или (optional)

В тази лекция има повече, отколкото се събира в едно занятие. Разделите, отбелязани с (advanced), навлизат по-дълбоко, а тези с (optional) добавят още един пример към тема, която вече знаете. В час може да ги пропуснем - прочетете ги вкъщи. Домашното не ги изисква.

Къде е мястото на тази тема в примерното правило за проверка - днешното правило е изградено от малки helper функции. Числото във всеки коментар е разделът от тази лекция:

js
// 7, 12. global constants: const, and no function changes them
const MODELS = ["XR-210", "XR-220"];
const MIN_FIRMWARE = "2.14";

// 2, 5, 12. our own helper: compare two versions part by part
function compareVersions(a, b) {
    const pa = String(a).split(".");
    const pb = String(b).split(".");
    for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
        const x = parseInt(pa[i], 10) || 0;
        const y = parseInt(pb[i], 10) || 0;
        if (x !== y) return x < y ? -1 : 1;     // 5. return ends the function
    }
    return 0;
}

// 2. one helper built on another
function versionBelow(a, b) {
    return compareVersions(a, b) < 0;
}

// 4. a default value for a parameter
function deviceName(doc, fallback = "unknown device") {
    return doc.inventory?.extended_info?.json?.data?.system?.name
        || doc.status?.device_alias
        || fallback;
}

// 10. an arrow function: one parameter, one expression
const isProblem = enc => enc.state !== "normal" || versionBelow(enc.firmware, MIN_FIRMWARE);

// 9. a function that gets functions (callbacks)
function collect(list, test, describe) {
    const out = [];
    for (const item of list) {
        if (test(item)) out.push(describe(item));
    }
    return out;
}

function rule(doc) {          // 12. the engine calls rule(doc) - keep this line
    if (!MODELS.includes(doc.overview?.model)) return;      // 5. stop early
    const enclosures = doc.inventory?.enclosures;
    if (!Array.isArray(enclosures)) return;

    // 9, 10. two arrow functions as arguments
    const problems = collect(enclosures, isProblem,
        enc => `${enc.id}: ${enc.state}, firmware ${enc.firmware}`);
    if (problems.length === 0) return;

    emit(deviceName(doc), {
        "Model": doc.overview.model,
        "Problems": problems.length,
        "Details": problems.join("; ")
    });
}
  • Правилото е същото като в лекция 4. Разликата: rule е кратко и всяка стъпка има име. versionBelow вече не е готова helper функция - пишем я в раздел 12.
  • В раздел 13 ще прочетете по-старо правило, написано в стила на много съществуващи правила, и ще го модернизирате.

"Really understanding functions in JavaScript is the single most important weapon you can wield." ("Истинското разбиране на функциите в JavaScript е най-важното оръжие, с което разполагате.") - J. Resig, B. Bibeault, J. Maras, Secrets of the JavaScript Ninja, 2-ро издание (Manning, 2016)

За правилата това е буквално вярно: правилото е функция, rule(doc). Engine-ът я извиква веднъж за всеки отчет.

1. Какво е функция? ​

MDN: Functions (ръководство)

Функцията е поредица от действия, които вършат една задача, събрани под едно име. Представете си рецепта: "палачинки" е името; стъпките са записани веднъж; готвите ги, когато поискате. Резултатът може да зависи от допълнителни входни данни: палачинки с мляко или палачинки с вода.

В програмирането функцията е малка програма в програмата. Дефинирате я веднъж и я извиквате (изпълнявате) колкото пъти ви трябва, с различни входни данни.

Защо да използваме функции? ​

  • Без повтарящ се код (принципът DRY - "Don't Repeat Yourself", "не се повтаряй"). Поправяте бъг на едно място, а не в пет копия.
  • Свързаните действия на едно място, с име, което казва какво правят: versionBelow(level, "4.2") се чете по-лесно от десет реда split и loops.
  • По-добра структура: дългото правило става няколко кратки стъпки.
  • По-лесно тестване: малка функция с ясни входни данни и един резултат може да се провери самостоятелно (занятие 10).

Ето проблема, който функциите решават. Една и съща проверка на версия, копирана три пъти:

js
const level = "4.2.1.7";

// copy 1: below 4.2.0.0?
let a = level.split(".");
let below1 = Number(a[0]) < 4 || (Number(a[0]) === 4 && Number(a[1]) < 2);

// copy 2: below 4.3? - copied and changed by hand... with a bug
let b = level.split(".");
let below2 = Number(b[0]) < 4 || (Number(b[0]) === 4 && Number(a[1]) < 3);

// copy 3: below 5.0?
let c = level.split(".");
let below3 = Number(c[0]) < 5;

console.log(below1, below2, below3);    // false true true
Променете кода и го изпълнете отново

Отговорите случайно са верни, но копие 2 чете a[1] вместо b[1] - точно такъв бъг създава копирането. С функция логиката се пише веднъж:

js
function versionBelow(a, b) {
    const pa = a.split(".");
    const pb = b.split(".");
    for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
        const x = Number(pa[i] ?? 0);
        const y = Number(pb[i] ?? 0);
        if (x !== y) return x < y;
    }
    return false;
}

const level = "4.2.1.7";
console.log(versionBelow(level, "4.2"), versionBelow(level, "4.3"), versionBelow(level, "5.0"));
Променете кода и го изпълнете отново

Не се тревожете за подробностите засега - до края на лекцията ще можете да прочетете всеки ред, а в раздел 12 ще я направим надеждна.

Функции или процедури? ​

Много езици различават два вида подпрограми:

  • процедурата само прави нещо (отпечатва, изпраща alert) и не връща резултат;
  • функцията получава входни данни и връща резултат, като функция в математиката: y = f(x). Резултатът може да се използва в по-голям израз.

В JavaScript и двете се пишат с function. В едно правило:

ВидПримерИзползва се като
процедураemit(name, details), console.log(...)statement на отделен ред
функцияversionBelow(a, b), deviceName(doc)стойност: if (versionBelow(...)), const name = deviceName(doc);

Добрите helper функции са най-вече от втория вид: те изчисляват и връщат, а правилото решава какво да прави с резултата.

2. Дефиниране и извикване на функция ​

Вариант 1: function declaration ​

MDN: function declaration

text
function <name>(<parameters>) {
    <body>
}
  • <name> - името на функцията. Същите правила като за имената на variables (вижте identifiers в MDN). Използвайте глагол или въпрос, защото функцията прави нещо: getSerial, deviceName, isSupported, hasFeature, versionBelow.
  • <parameters> - нула или повече имена, разделени със запетаи. Кръглите скоби са задължителни, дори когато няма параметри.
  • <body> - statements между { и }.
  • След затварящата } на декларацията не е нужна ; (но не пречи).
js
// no parameters
function printLine() {
    console.log("------------------------");
}

// two parameters
function printSum(x, y) {
    console.log(x + y);
}

printLine();
printSum(2, 3);         // 5
printLine();
Променете кода и го изпълнете отново

Извикване на функция ​

Дефинирането на функция не я изпълнява. Тялото се изпълнява едва когато функцията бъде извикана (call, invoke, execute):

text
<name>(<arguments>);
  • <arguments> - стойностите, подадени на параметрите при това извикване.
  • Не оставяйте интервал между името и ( (работи, но лесно се чете погрешно).
  • Кръглите скоби правят извикването: greet е функцията, greet() я изпълнява.
js
function greet(userName) {
    console.log(`Hello ${userName}. Nice to see you.`);
}

// only now do the actions in greet run
greet("Ada");
greet("John");

console.log(typeof greet);      // function - the name alone does not call it
Променете кода и го изпълнете отново

Анимация

Стекът на извикванията: 1. Извикване на функция - всяко извикване получава нова рамка (frame) със свои variables.

console.log също е функция - вградена. Извиквате я с аргумент, както своите функции. И една функция може да извиква други функции:

js
function square(n) {
    return n * n;
}

function sumOfSquares(a, b) {
    return square(a) + square(b);
}

console.log(sumOfSquares(2, 3));      // 4 + 9 = 13
Променете кода и го изпълнете отново

Анимация

Стекът на извикванията: 4. Вложени извиквания - стекът расте при всяко извикване и намалява при всяко връщане.

В примерния JSON документ helper функцията получава документа като параметър:

js
function getModel(doc) {
    return doc.overview.model;
}

function printModel(doc) {
    console.log("The model is " + getModel(doc));
}

printModel(doc);        // The model is XR-220
Променете кода и го изпълнете отново

Параметърът и тук се казва doc, но не е задължително: параметърът е ново име, валидно само вътре във функцията. Повече в раздел 6.

Вариант 2: function expression ​

MDN: function expression

Функцията е и стойност. Можете да я създадете в израз и да я запишете във variable:

text
const <name> = function (<parameters>) {
    <body>
};

Самата функция тук няма име - тя е анонимна функция. Достигате я чрез variable-ата.

js
const greet = function (userName) {
    console.log(`Hello ${userName}`);
};

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

На реда const greet = function ... стават две неща: декларира се variable-ата greet, после се създава функция и се присвоява на нея - като const x = 5;. Затова function expression завършва с ;, както всяко друго присвояване.

Function expression може да има собствено име, но то се вижда само вътре във функцията (полезно за съобщенията за грешки и за функция, която извиква себе си). Отвън използвайте variable-ата:

js
const a = function b() {
    console.log("I am in b!");
};

a();                    // I am in b!
console.log(typeof b);  // undefined - b is not visible here; b() would be a ReferenceError
Променете кода и го изпълнете отново

Забравена ; (advanced) ​

Пропускането на ; след function expression обикновено не вреди, защото JavaScript я слага вместо вас в края на реда (automatic semicolon insertion). Но не и когато следващият ред започва с ( или [. Тогава JavaScript слепва двата реда. Пример:

js
let x, y;

let foo = function () {
    console.log("Foo");
}                       // you must have ';' here

[x, y] = [1, 2];

console.log(x, y);      // undefined undefined - not 1 2!
console.log(foo);       // [ 1, 2 ] - and foo is not a function any more
Променете кода и го изпълнете отново

JavaScript го чете като let foo = function () {...}[x, y] = [1, 2];: задава property на обекта-функция, x и y никога не получават стойност, а foo получава array-я. Извикване foo() след това е TypeError: foo is not a function. Слагайте ; след всяко присвояване - и след function expression - и бъгът не може да се случи.

Кой да използваме? ​

И двата работят. В правилата и във файловете с helper функции най-честият избор е:

  • function declarations за именуваните helper функции на най-горно ниво във файла (function versionBelow(a, b) { ... }) - четат се като списък от инструменти;
  • arrow functions (раздел 10) за кратки функции, които се подават на други функции.

Rule engine-ът извиква function rule(doc) - declaration, и този ред трябва да остане какъвто е.

3. Параметри и аргументи ​

  • Параметрите са имената в дефиницията: function sum(x, y).
  • Аргументите са стойностите при извикването: sum(2, 3).

Първият аргумент отива в първия параметър, вторият - във втория и т.н. - по позиция, а не по име:

js
function sum(x, y) {
    // here the engine has already done: let x = 2, y = 3;
    console.log(x + y);
}

sum(2, 3);      // 5
Променете кода и го изпълнете отново

Параметрите са локални variables на функцията. Не ги декларирате с let: engine-ът ги създава при всяко извикване и им дава стойностите на аргументите. Те съществуват само докато функцията се изпълнява.

Твърде малко или твърде много аргументи ​

JavaScript не проверява броя на аргументите. Няма грешка:

  • параметър без аргумент е undefined;
  • аргумент без параметър се пренебрегва.
js
function sum(x, y) {
    console.log(x, y, x + y);
}

sum(2);         // 2 undefined NaN  - 2 + undefined is NaN
sum(2, 3, 4);   // 2 3 5            - 4 is ignored
Променете кода и го изпълнете отново

Variable, която изобщо не е декларирана, все пак е грешка:

js
function sum(x) {
    console.log(x);         // 2
    console.log(x + y);     // ReferenceError: y is not defined
}

sum(2, 3);
Променете кода и го изпълнете отново

Анимация

Стекът на извикванията: 2. Аргументи - липсващ аргумент и излишен, който отскача.

Внимавайте с реда

Аргументите се съпоставят по позиция. versionBelow("4.2", level) задава обратния въпрос на versionBelow(level, "4.2"). Давайте ясни имена на параметрите (current, minimum) и проверявайте реда, когато извиквате helper функция.

Произволен брой аргументи: arguments и ...rest (advanced) ​

MDN: The arguments object, Rest parameters

Понякога функцията трябва да приема произволен брой стойности. В по-стар код ще видите специалния обект arguments. Той съществува във всяка function (но не и в arrow function) и съдържа всички аргументи на извикването:

js
// older style
function maxOf() {
    var max = arguments[0];
    for (var i = 1; i < arguments.length; i++) {
        if (arguments[i] > max) max = arguments[i];
    }
    return max;
}

console.log(maxOf(3, 9, 4));        // 9
Променете кода и го изпълнете отново

arguments прилича на array, но не е (Array.isArray(arguments) е false). Модерният код пише вместо това rest параметър ...name. Той събира останалите аргументи в истински array:

js
function maxOf(...values) {
    let max = values[0];
    for (const v of values) {
        if (v > max) max = v;
    }
    return max;
}

console.log(maxOf(3, 9, 4));        // 9

// a rest parameter goes last; the other parameters work as usual
function logAll(prefix, ...messages) {
    for (const m of messages) console.log(prefix + m);
}
logAll("[CHK-0132] ", "firmware below 4.2", "remote login enabled");
Променете кода и го изпълнете отново

Math.max(3, 9, 4) е вградена функция от същия вид (занятие 8).

Пример: лице на правоъгълник (optional) ​

Функция rectangleArea(width, height), използвана за стаите на един апартамент:

js
function rectangleArea(width, height) {
    return width * height;
}

const kitchen = rectangleArea(3, 4);
const bedroom = rectangleArea(4, 4.5);
console.log(kitchen, bedroom, kitchen + bedroom);      // 12 18 30
Променете кода и го изпълнете отново

Проверете се: какво връща rectangleArea(3) и защо? Какво връща rectangleArea("3", "4")?

Отговор

rectangleArea(3) е NaN: height е undefined, а 3 * undefined е NaN. rectangleArea("3", "4") е 12 - числото 12: * преобразува двата strings в числа. (+ не би го направил: би ги слепил.)

4. Стойности по подразбиране на параметрите ​

MDN: Default parameters

Стойност по подразбиране е стойността, която параметърът получава, когато за него не е подаден аргумент. От ES6 (2015) се пише в списъка с параметри:

js
function f(x = 1, y = 2, z = 3) {
    console.log(x, y, z);
}

f(6, 7);        // 6 7 3
f();            // 1 2 3
Променете кода и го изпълнете отново

По-старият код използва вместо това || (лекция 3, || за стойност по подразбиране):

js
function f(x, y, z) {
    x = x || 1;     // no let - x is already a local variable
    y = y || 2;
    z = z || 3;
    console.log(x, y, z);
}

f(6, 7);        // 6 7 3
f(0, "");       // 1 2 3 - 0 and "" are lost!
Променете кода и го изпълнете отново

Двете форми не са еднакви:

  • стойността по подразбиране се използва само когато аргументът е undefined (липсва);
  • x || 1 заменя всяка falsy стойност: 0, "", false, null, NaN.
js
function withDefault(x = 1) { return x; }
function withOr(x) { return x || 1; }

console.log(withDefault(0), withOr(0));             // 0 1
console.log(withDefault(""), withOr(""));           // "" -> prints nothing, then 1
console.log(withDefault(null), withOr(null));       // null 1
console.log(withDefault(undefined), withOr(undefined));     // 1 1
Променете кода и го изпълнете отново

Обърнете внимание на null: стойността по подразбиране не се използва за null. В отчетите липсваща стойност може да дойде като null. Когато и null трябва да получи стойността по подразбиране, използвайте ?? в тялото: x = x ?? 1; (ES2020).

let x = x || 1 е грешка

Честа грешка е да се пише let x = x || 1; вътре във функцията. Параметърът x вече е деклариран, затова това е SyntaxError: Identifier 'x' has already been declared. Пишете x = x || 1; - или по-добре стойност по подразбиране.

Анимация

Стекът на извикванията: 5. Стойности по подразбиране - f(0) и fOld(0) една до друга.

Стойността по подразбиране се изчислява при всяко извикване и може да използва параметрите преди нея:

js
function deviceName(doc, fallback = "device " + doc.overview.serial_number) {
    return doc.inventory.extended_info?.json?.data?.system?.name ?? fallback;
}

console.log(deviceName(doc));                           // LAB-DEV-01
console.log(deviceName({ overview: { serial_number: "SN0001" }, inventory: {} }));
// device SN0001
Променете кода и го изпълнете отново

5. return: връщане на резултат ​

MDN: return

Всяко извикване на функция е израз - като 2 + 2, то дава стойност. Тази стойност се определя от return:

text
return <expression>;
  • Изразът се изчислява и стойността му заменя извикването: const res = sum(2, 3); става const res = 5;.
  • return прекратява функцията веднага. Нито един statement след него не се изпълнява.
  • Функция без return (или само с return;) връща undefined.
js
function sum(x, y) {
    return x + y;
}

function f() {
    console.log("start");
    return true;
    console.log("end");     // never runs
}

const res = sum(2, 3);
console.log(res);           // 5
console.log(sum(sum(1, 2), 3));     // a call inside a call: 6

const ok = f();             // prints start
console.log(ok);            // true

function noReturn() {
    const x = 5;
}
console.log(noReturn());    // undefined
Променете кода и го изпълнете отново

Анимация

Стекът на извикванията: 3. return

return в loop: спиране на търсенето ​

return прекратява функцията, и отвътре на loop. Това прави helper функциите за търсене кратки - без break, без допълнителна variable:

js
function findEnclosure(list, id) {
    for (const enc of list) {
        if (enc.id === id) return enc;      // found: stop here
    }
    return undefined;                       // the loop ended: not found
}

console.log(findEnclosure(doc.inventory.enclosures, "ENC-03"));
console.log(findEnclosure(doc.inventory.enclosures, "ENC-99"));     // undefined
Променете кода и го изпълнете отново

Последният ред return undefined; не е задължителен - функцията и без него връща undefined - но показва на читателя, че за "не е намерен" е помислено.

Спиране рано: guard clauses ​

В лекция 3 правилото спираше рано с if (...) return;. Helper функциите използват същия модел. Първо проверете лошите случаи и върнете; основната работа идва накрая, без дълбоко влагане:

js
function firmwareVersion(doc) {
    const level = doc.inventory?.details?.firmware_level;
    if (typeof level !== "string") return "";       // no level
    if (level.trim() === "") return "";             // an empty level
    return level.split(" ")[0];                     // "4.2.1.7 (build ...)" -> "4.2.1.7"
}

console.log(firmwareVersion(doc));                  // 4.2.1.7
console.log(firmwareVersion({}));                   // "" - prints an empty line
Променете кода и го изпълнете отново

Връщайте условието направо ​

Много често срещан по-стар модел:

js
function isSupported(model) {
    if (model === "XR-210" || model === "XR-220") {
        return true;
    } else {
        return false;
    }
}
console.log(isSupported("XR-220"));
Променете кода и го изпълнете отново

Условието вече е true или false. Върнете го:

js
function isSupported(model) {
    return model === "XR-210" || model === "XR-220";
}
console.log(isSupported("XR-220"), isSupported("XR-300"));     // true false
Променете кода и го изпълнете отново

(Внимавайте с && / || върху стойности, които не са boolean: return name && name.trim(); връща string или undefined, а не true / false. Това е наред, когато извикващият го използва като стойност; обвийте го в Boolean(...), когато ви трябва истински boolean.)

return и нов ред ​

Стойността на return трябва да е на същия ред. На нов ред JavaScript слага ; след return и функцията връща undefined:

js
function isSupported(model) {
    return
        model === "XR-210" || model === "XR-220";
}
console.log(isSupported("XR-220"));     // undefined!

function isSupportedOk(model) {
    return model === "XR-210" ||
        model === "XR-220";
}
console.log(isSupportedOk("XR-220"));   // true
Променете кода и го изпълнете отново

Дълъг израз може да продължи на следващите редове - стига да започва на реда с return. Същото важи за обект: пишете return { с фигурната скоба на реда с return.

Няколко резултата: върнете array или обект ​

Функцията връща една стойност. Когато ви трябват повече, върнете array (или обект), който ги съдържа:

js
function splitVersion(version) {
    const parts = version.split(".");
    // [major, minor, rest]
    return [Number(parts[0]), Number(parts[1] ?? 0), parts.slice(2).join(".")];
}

const v = splitVersion("4.2.1.7");
console.log(v[0], v[1], v[2]);      // 4 2 1.7
Променете кода и го изпълнете отново

(Занятие 6 показва кратък начин да го разопаковате: const [major, minor, rest] = splitVersion(...).)

Проверете се: logEvenElements(arr) отпечатва четните елементи на array, а findMaxEven(arr) връща най-големия четен елемент. Коя е процедура и коя функция? Напишете findMaxEven - какво трябва да връща за [1, 3, 5]?

Отговор

logEvenElements е процедура: отпечатва и връща undefined. findMaxEven е функция: връща стойност, която извикващият може да използва.

js
function findMaxEven(arr) {
    let max;                    // undefined: nothing found yet
    for (const x of arr) {
        if (x % 2 === 0 && (max === undefined || x > max)) max = x;
    }
    return max;
}

За [1, 3, 5] връща undefined - "няма четен елемент". Не започвайте с max = 0: за [-4, -2] отговорът е -2, а не 0. (Задача 5 за упражнение.)

6. Scope: къде се вижда една variable ​

MDN: Scope

Scope на една variable е частта от кода, в която можете да я използвате. Има три вида:

  • global scope - извън всички функции и блокове;
  • function (local) scope - тялото на една функция;
  • block scope - кодът между { и } на if, на loop или на обикновен блок, за let и const (от ES6).
js
// x is declared in the global scope
let x = 5;

function foo() {
    // y is local to foo
    let y = 10;
    console.log(`x in foo: ${x}`);      // x in foo: 5 - a global is seen everywhere
    console.log(`y in foo: ${y}`);      // y in foo: 10
}

foo();
console.log(`x in global: ${x}`);       // x in global: 5
console.log(`y in global: ${y}`);       // ReferenceError: y is not defined
Променете кода и го изпълнете отново

Параметрите също са локални. Когато функцията приключи, локалните ѝ variables изчезват; следващото извикване създава нови.

Анимация

Scope на variables: 1. Глобален и локален

Global scope ​

Variable, декларирана извън всички функции, е глобална: всяка функция може да я чете и всяка функция може да я променя. В една страница в браузъра всички файлове със скриптове споделят един global scope - глобално име в един файл може да се сблъска със същото име в друг файл.

Пример: страницата зарежда две библиотеки и двете дефинират глобална variable със същото име. Втората печели:

js
// ---- lib.js ----
var count = 100;
function libInfo() {
    console.log(`lib count: ${count}`);
}

// ---- app.js (loaded after lib.js, same global scope) ----
var count = 0;          // no error: var allows the same name again
libInfo();              // lib count: 0 - the library now sees app's value
Променете кода и го изпълнете отново

Файловете в Node.js (и модулите от занятие 9) имат всеки свой scope, затова там този сблъсък не се случва. Как rule engine-ът зарежда правилата и библиотеките, не знаем - безопасният навик е един и същ навсякъде: малко глобални variables, ясни имена и никоя функция да не променя глобална variable (повече в раздел 7).

Анимация

Scope на variables: 6. IIFE (advanced) - същият сблъсък и старото решение (раздел 11).

Shadowing ​

Локална variable със същото име като външна засенчва (скрива) външната вътре във функцията (shadowing). Това са две различни variables:

js
let firstName = "Ada";

function foo() {
    let firstName = "Turing";       // a NEW local variable
    console.log("foo() firstName:", firstName);     // Turing
}

foo();
console.log("global firstName:", firstName);        // Ada - not changed
Променете кода и го изпълнете отново

Анимация

Scope на variables: 3. Засенчване (shadowing)

Параметърът също е локална променлива, затова засенчва по същия начин. Ще го виждате често в съществуващите правила: helper функция вътре в rule има собствен параметър doc, който скрива doc на rule:

js
function rule(doc) {
    function getModel(doc) {            // this doc hides the doc of rule
        return doc.overview.model;      // the parameter - what the call passed in
    }

    console.log(getModel(doc));                                 // XR-220
    console.log(getModel({ overview: { model: "XR-210" } }));   // XR-210
}

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

Това е наред: вътре в getModel doc винаги е стойността от извикването, затова helper функцията работи само с това, което получава.

Проверете се: преди да го изпълните, запишете трите реда, които ще отпечата.

js
let x = 1;

let foo = function () {
    let x = 2;

    let bar = function (x) {
        console.log("x in bar =", x);
    };
    bar(99);

    console.log("x in foo =", x);
};

foo();
console.log("x in main =", x);
Променете кода и го изпълнете отново
Отговор

x in bar = 99, x in foo = 2, x in main = 1. Има три различни variables с име x: глобалната, локалната на foo и параметърът на bar. Всяка функция вижда най-близката.

Бъг: без деклариране ​

Ако присвоите стойност на име, което никога не е декларирано (без let, const или var), JavaScript не създава локална variable. Той търси навън и:

  • ако съществува външна variable с това име, тя се променя;
  • ако няма такава, се създава нова глобална (в стария режим, "sloppy" mode).
js
var firstName = "Ada";

function foo() {
    firstName = "Turing";           // no let: this is the GLOBAL firstName
    leakedName = "Lovelace";        // never declared: a new global!
}

foo();
console.log(firstName);             // Turing - the global was changed
console.log(leakedName);            // Lovelace - it leaked out of foo
Променете кода и го изпълнете отново

И двете са бъгове, които трудно се откриват. Strict mode превръща втория в грешка. Сложете "use strict"; на първия ред на файла (или на функцията):

js
"use strict";

function foo() {
    leakedName2 = "Lovelace";       // ReferenceError in strict mode
}

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

Всички файлове за упражнения в този курс започват с "use strict";. Модулите и класовете винаги са strict. Дали rule engine-ът изпълнява правилата в strict mode, не знаем - затова винаги декларирайте variables и въпросът става без значение.

Анимация

Scope на variables: 4. Без деклариране

Scope chain ​

Функции могат да се пишат вътре в други функции. Вътрешната функция вижда своите variables, после тези на външната функция, после глобалните - както търсите име в стаята си, после в апартамента, после в сградата. Това е веригата от scopes (scope chain). Външната функция не вижда variables на вътрешната:

js
let a = "global";

function outer() {
    let b = "outer";

    function inner() {
        let c = "inner";
        console.log(a, b, c);       // global outer inner
    }

    inner();
    console.log(typeof c);          // undefined - c is not visible here
}

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

Веригата се определя от мястото, където функцията е написана в кода, а не от мястото, откъдето е извикана (lexical scope):

js
let firstName = "Ada";

function greet() {
    console.log(`Hello, ${firstName}`);     // the global firstName
}

function caller() {
    let firstName = "Turing";       // greet does not see this one
    greet();
}

caller();       // Hello, Ada
Променете кода и го изпълнете отново

7. let, const и var ​

MDN: var, let, const

Лекция 2 каза "използвайте const и let, а не var". Ето защо. Съществуващите правила използват var навсякъде, затова трябва да знаете какво прави.

Block scope ​

let и const се виждат само в блока, в който са декларирани. var пренебрегва блоковете: принадлежи на цялата функция (или е глобална).

js
{
    var x = 2;              // var: visible outside the block
    let y = 4;              // only in this block
    const alpha = 2.34;     // only in this block
}

console.log(`x = ${x}`);                // x = 2
console.log(typeof y, typeof alpha);    // undefined undefined - not visible here
Променете кода и го изпълнете отново

let в блок е нова variable, дори със същото име като външна:

js
let status = "none";
const userAge = 32;

if (userAge >= 18) {
    // not the outer status: a new variable, visible only in this block
    let status = "Adult";
}

console.log(`status = ${status}`);      // status = none
Променете кода и го изпълнете отново

Това е отговорът на въпроса от лекция 3 за for (var i ...): с var i все още съществува след loop-а; с let - не.

js
for (var i = 0; i < 3; i++) { }
console.log("var i after the loop:", i);    // 3

for (let j = 0; j < 3; j++) { }
console.log("let j after the loop:", typeof j);     // undefined
Променете кода и го изпълнете отново

Анимация

Scope на variables: 5. Block scope: let и var

Повторно деклариране на същото име ​

С var повторното деклариране на същото име в същия scope е позволено - и тихо използва същата variable. С let и const е грешка, която хваща рано бъговете от copy-paste:

js
var level = "4.2";
var level = "3.9";          // no error: the same variable
console.log(level);         // 3.9

let model = "XR-220";
// let model = "XR-210";    // SyntaxError: Identifier 'model' has already been declared
Променете кода и го изпълнете отново

Особености на const (advanced) ​

const създава variable, на която не може да се присвои нова стойност. Грешката спира програмата, затова последният ред не се изпълнява:

js
const GOLDEN_RATIO = 1.618;
console.log(GOLDEN_RATIO);      // 1.618

GOLDEN_RATIO = 2;               // TypeError: Assignment to constant variable.
console.log(GOLDEN_RATIO);      // not printed
Променете кода и го изпълнете отново

Тя трябва да получи стойността си при декларирането: const GOLDEN_RATIO; е SyntaxError: Missing initializer in const declaration.

const не прави стойността истинска константа. Когато стойността е array или обект, все още можете да променяте това, което е вътре - фиксирана е само стрелката от името към стойността (лекция 4, раздел 4):

js
const someArray = [3.14];
someArray[0] = 99;              // not a re-assignment: a change inside the array
console.log(someArray[0]);      // 99

const someObject = { pi: 3.14 };
someObject.pi = 4;              // a change inside the object
console.log(someObject.pi);     // 4

// someArray = 4;               // TypeError: Assignment to constant variable.
// someObject = {};             // TypeError: Assignment to constant variable.
Променете кода и го изпълнете отново

Анимация

Variables в паметта: 5. const

Защо указанията за правилата искат глобални const, които никога не се променят ​

Указанията за правилата често казват: "глобалните variables трябва да са декларирани като const и вашите функции не трябва да ги променят". Причината: engine-ът може да зареди правилото веднъж и да извиква rule(doc) за много отчети. Глобална variable, която правилото променя, пази стойността си от предишния отчет:

js
let problems = [];              // a global that the rule changes - a bug

function rule(doc) {
    if (doc.state !== "normal") problems.push(doc.id);
    console.log(doc.id + ": " + problems.length + " problem(s)");
}

// the engine calls the rule for three reports, one after the other
rule({ id: "LAB-DEV-01", state: "failed" });    // 1 problem(s)
rule({ id: "LAB-DEV-02", state: "normal" });    // 1 problem(s) - from DEV-01!
rule({ id: "LAB-DEV-03", state: "failed" });    // 2 problem(s) - wrong
Променете кода и го изпълнете отново

Поправката: пазете всичко, което принадлежи на един отчет, локално в rule (или в helper функция), а глобалните variables използвайте само за фиксирани настройки:

js
const BAD_STATES = ["failed", "degraded"];      // fixed, never changed

function rule(doc) {
    const problems = [];                        // new for every report
    if (BAD_STATES.includes(doc.state)) problems.push(doc.id);
    console.log(doc.id + ": " + problems.length + " problem(s)");
}

rule({ id: "LAB-DEV-01", state: "failed" });    // 1
rule({ id: "LAB-DEV-02", state: "normal" });    // 0
rule({ id: "LAB-DEV-03", state: "failed" });    // 1
Променете кода и го изпълнете отново

И помнете, че const не замразява array: BAD_STATES.push(...) вътре във функция пак би било промяна. "Никога не се променя" е правило, което вие спазвате.

Hoisting (advanced) ​

MDN: Hoisting

Преди JavaScript да изпълни кода на един scope, той първо го преминава веднъж и регистрира всички декларации в него. Можете да мислите за две минавания:

  1. създаване - всяка декларация в scope-а се регистрира;
  2. изпълнение - statements се изпълняват ред по ред.

Така декларациите са известни от началото на своя scope, където и да са написани. Това се нарича hoisting ("издигане"). Какво съдържа едно име преди своя ред зависи от това как е декларирано:

Декларирано сПреди своя ред
var x = 10;съществува, стойност undefined
function f() {...}съществува, цялата функция - можете да я извикате
let / constсъществува, но не можете да я докоснете: ReferenceError (temporal dead zone, TDZ)
var f = function () {...};съществува, стойност undefined - извикването ѝ е TypeError: f is not a function
js
console.log("x:", x);       // x: undefined - the declaration is known, the value not yet
var x = 10;
console.log("x:", x);       // x: 10
Променете кода и го изпълнете отново

Function declaration се издига заедно с тялото си, затова може да се извика преди своя ред:

js
console.log(foo());         // I am foo() and I work!

function foo() {
    return "I am foo() and I work!";
}
Променете кода и го изпълнете отново

Function expression е стойност във variable. Издига се само variable-ата:

js
console.log(foo);           // undefined - the variable is known, the function not yet
console.log(foo());         // TypeError: foo is not a function

var foo = function () {
    return "I am foo() and I work!";
};
Променете кода и го изпълнете отново

С let или const грешката е ReferenceError: variable-ата е в TDZ, докато редът ѝ не се изпълни.

Какво означава това за правилата:

  • Заради hoisting правилото би работило, дори ако декларациите на helper функциите му са след rule - но указанията искат helper функциите преди rule. Спазвайте ги: файлът се чете отгоре надолу, а helper функциите, написани като const f = ..., и без това трябва да са първи.
  • var, използвана преди своя ред, тихо дава undefined. let / const дава ясна грешка. Още една причина да предпочитате let / const.

Проверете се: какво отпечатва това и защо?

js
var level = "global";

function f() {
    console.log(level);
    var level = "local";
    console.log(level);
}

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

undefined, после local. Вътре в f има локална var level, издигната в началото на f. Тя засенчва глобалната още от първия ред на f, докато стойността ѝ е все още undefined. Глобалната "global" никога не се вижда във f. (Анимацията Hoisting - 5 го показва стъпка по стъпка.)

8. Arrays и обекти като аргументи; чисти функции ​

Какво получава функцията, когато ѝ подадете стойност - самата стойност или копие?

  • Примитивът (number, string, boolean, null, undefined) се копира в параметъра. Промяната на параметъра не променя variable-ата на извикващия.
  • Array или обект не се копира. Параметърът получава копие на референцията - стрелка към същия array или обект. Промените вътре в него се виждат от извикващия.
js
function addOne(n) {
    n = n + 1;          // changes only the local copy
    return n;
}

let count = 5;
const more = addOne(count);
console.log(count, more);       // 5 6 - count is unchanged
Променете кода и го изпълнете отново
js
function addModel(list, model) {
    list.push(model);   // changes THE array the caller has
}

const models = ["XR-210"];
addModel(models, "XR-220");
console.log(models);            // [ 'XR-210', 'XR-220' ] - changed
Променете кода и го изпълнете отново

Анимация

Функциите и данните: 1. Копие на стойността, 2. Същият array

Промяна или замяна ​

Има разлика между промяната на array-я, към който сочи параметърът, и замяната на параметъра с нов array:

js
function clearList(list) {
    list = [];          // the local arrow now points to a NEW array; the caller's is untouched
}

function emptyList(list) {
    list.length = 0;    // changes the caller's array
}

const a = ["x", "y"];
clearList(a);
console.log(a.length);  // 2
emptyList(a);
console.log(a.length);  // 0
Променете кода и го изпълнете отново

Анимация

Функциите и данните: 3. Промяна или замяна

Не променяйте отчета ​

doc е обект, затова helper функция, която получава doc (или част от него), може да промени самия отчет - и всяка проверка след нея вижда променените данни:

js
function shortFirmware(enc) {
    enc.firmware = enc.firmware.split(".")[0];      // changes the report!
    return enc.firmware;
}

const enc = doc.inventory.enclosures[1];
console.log(enc.firmware);              // 2.10
console.log(shortFirmware(enc));        // 2
console.log(enc.firmware);              // 2 - the report has changed
console.log(doc.inventory.enclosures[1].firmware);  // 2 - also here: the same object
Променете кода и го изпълнете отново

Едно по-късно versionBelow(enc.firmware, "2.14") вече сравнява "2" - и дава грешен отговор. Поправката: изчислете и върнете, не пишете във входните данни:

js
function majorOf(firmware) {
    return firmware.split(".")[0];      // a new string; the input is not touched
}

const enc = doc.inventory.enclosures[1];
console.log(majorOf(enc.firmware));     // 2
console.log(enc.firmware);              // 2.10 - unchanged
Променете кода и го изпълнете отново

Анимация

Функциите и данните: 4. Промяна на отчета

Чисти функции ​

Чиста функция (pure function):

  1. връща резултат, който зависи само от аргументите ѝ - едни и същи аргументи винаги дават един и същ резултат;
  2. няма странични ефекти (side effects) - не променя нищо отвън: нито глобална variable, нито аргумент, не отпечатва, не изпраща alerts.

majorOf, versionBelow, deviceName са чисти. shortFirmware не е (променя аргумента си), addModel не е, и първото rule от раздел 7 не е (променя глобална variable).

Чистите helper функции лесно се четат, тестват и използват повторно: за да разберете едно извикване, трябва да погледнете само аргументите му. Дръжте страничните ефекти на едно място - самото rule извиква emit. Всичко, което то извиква, трябва да е чисто.

js
// not pure: reads a global that can change, and writes into it
var lastModel;
function rememberModel(doc) {
    lastModel = doc.overview.model;
    return lastModel;
}

// pure: everything it needs comes in as an argument, the result goes out with return
function modelOf(doc) {
    return doc.overview?.model ?? "";
}

console.log(rememberModel(doc), modelOf(doc));
Променете кода и го изпълнете отново

Когато helper функцията трябва да върне променен списък, тя създава нов:

js
function withModel(list, model) {
    if (list.includes(model)) return list.slice();  // a copy, unchanged
    return [...list, model];                        // a new array with one more model
}

const MODELS = ["XR-210", "XR-220"];
const more = withModel(MODELS, "XR-230");
console.log(MODELS, more);      // the original is unchanged
Променете кода и го изпълнете отново

Същите стрелки като в лекция 4

Това отново е картината с "референциите": две имена, един array. Анимациите Arrays в паметта - 5 и Variables в паметта - 6 го показват без функции; параметърът е просто още едно име.

9. Функциите са стойности; callbacks ​

В JavaScript функцията е стойност, като число или array. Можете да:

  • я запишете във variable, в array или в обект;
  • я подадете като аргумент на друга функция;
  • я върнете от функция.

Затова функциите се наричат first-class citizens ("граждани от първа класа", first-class functions). Това е основата на почти всяка модерна JavaScript библиотека - и на методите на arrays от занятие 7.

Анимация

Функциите са стойности: 1. Като 5 и [1, 2, 3] - число, array и функция, едно до друго: едни и същи стъпки работят и за трите, а само функцията може да се извика.

js
function greet() {
    console.log("Hello!");
}

const hello = greet;            // no (): the function itself, not its result
console.log(typeof greet);      // function
console.log(greet);             // [Function: greet]
hello();                        // Hello! - the same function, another name
Променете кода и го изпълнете отново

Анимация

Функциите са стойности: 2. Функцията е стойност

Анонимни функции (optional) ​

Функция без име е анонимна функция. Използва се там, където функцията трябва само като стойност: присвоена на variable, сложена в array или обект, подадена на друга функция или върната.

js
// an anonymous function as an element of an array
const foobar = [
    function () { console.log("Foo"); },
    function () { console.log("Bar"); }
];
foobar[0]();        // Foo
foobar[1]();        // Bar

// as a property of an object (a method - session 6)
const obj = {
    foo: function () { console.log("Foo"); },
    bar: function () { console.log("Bar"); }
};
obj.foo();          // Foo
obj.bar();          // Bar
Променете кода и го изпълнете отново

Какво прави foobar[2]()? foobar[2] е undefined, а undefined() е TypeError: foobar[2] is not a function. Същата грешка идва от правописна грешка в името на helper функция в обект.

Анимация

Функциите са стойности: 3. В arrays и обекти

Истинска употреба: таблица от проверки, всяка от тях - малка функция. Правилото ги изпълнява всичките:

js
const checks = [
    function (d) { return d.inventory.details.remote_login_enabled === "yes" ? "remote login enabled" : ""; },
    function (d) { return d.inventory.details.power_saving === "on" ? "power saving on" : ""; },
    function (d) { return d.inventory.details.ntp_ip_address ? "" : "no NTP server"; }
];

const found = [];
for (const check of checks) {
    const message = check(doc);
    if (message !== "") found.push(message);
}
console.log(found);     // [ 'remote login enabled', 'power saving on' ]
Променете кода и го изпълнете отново

Callbacks ​

MDN: Callback function

Функция, подадена като аргумент на друга функция, се нарича callback: другата функция я "извиква обратно", когато ѝ потрябва.

js
function caller(callback) {
    console.log("caller will call the callback:");
    callback();
}

function callback1() {
    console.log("I'm the callback1 function!");
}

function callback2() {
    console.log("I'm the callback2 function!");
}

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

Обърнете внимание на разликата между подаването на функцията и подаването на резултата ѝ:

js
function caller(f) {
    console.log("1");
    f();
}

function callback() {
    console.log("2");
}

console.log("Feeding the caller() with the function:");
caller(callback);           // 1 2

console.log("Feeding the caller() with the result of a call:");
caller(callback());         // 2 1 - then TypeError: f is not a function
Променете кода и го изпълнете отново

При второто извикване callback() се изпълнява първо (аргументът се изчислява преди извикването), отпечатва 2 и връща undefined. caller получава undefined и f() гърми. Без () - функцията; с () - резултатът ѝ.

Анимация

Функциите са стойности: 4. Callbacks

Защо callbacks: един loop, много въпроси ​

В лекция 4 писахте един и същ loop много пъти: "преброй enclosures, които не са normal", "преброй enclosures със стар firmware"... Променяше се само условието. С callback loop-ът се пише веднъж, а условието идва като функция:

js
function countIf(list, test) {
    let n = 0;
    for (const item of list) {
        if (test(item)) n++;        // call the callback for every element
    }
    return n;
}

function notNormal(enc) {
    return enc.state !== "normal";
}

function isHdd(enc) {
    return enc.type.startsWith("HDD");
}

const encs = doc.inventory.enclosures;
console.log(countIf(encs, notNormal));      // 1
console.log(countIf(encs, isHdd));          // 2
Променете кода и го изпълнете отново

countIf не знае какво брои - решава callback-ът. Тя извиква test(item) веднъж за всеки елемент и използва върнатото true / false.

Анимация

Функциите и данните: 5. Callback, извикване по извикване - всяко извикване на callback-а получава своя рамка.

Две класически упражнения с callbacks. customMap създава нов array с резултата от callback-а за всеки елемент; customFilter запазва само елементите, за които callback-ът връща true:

js
function customMap(arr, f) {
    const mapped = [];
    for (const element of arr) {
        mapped.push(f(element));
    }
    return mapped;
}

function customFilter(arr, f) {
    const filtered = [];
    for (const element of arr) {
        if (f(element)) filtered.push(element);
    }
    return filtered;
}

function square(x) { return x * x; }
function isPositive(x) { return x > 0; }

console.log(customMap([1, 2, 3, 4], square));             // [ 1, 4, 9, 16 ]
console.log(customFilter([-1, 1, -2, 3, 4], isPositive)); // [ 1, 3, 4 ]
Променете кода и го изпълнете отново

Arrays в JavaScript ги имат вградени: arr.map(f) и arr.filter(f) - и forEach, find, some, every, reduce, sort... Всички те приемат callback. Занятие 7 е за тях; днес трябва само да можете да ги четете.

По-стари callbacks в правилата: function (x) { ... } ​

Съществуващите правила подават анонимни функции направо в извикването:

js
var level = "4.2.1.7-B00";

var parts = level.split(".").map(function (x) {
    return parseInt(x, 10) || 0;
});
console.log(parts);     // [ 4, 2, 1, 7 ]

doc.inventory.enclosures.forEach(function (enc) {
    console.log(enc.id + " " + enc.state);
});
Променете кода и го изпълнете отново

Четете го отвътре навън:

  • function (x) { return parseInt(x, 10) || 0; } е анонимна функция: "вземи една част, върни я като число или 0".
  • .map(...) я извиква за всяка част на версията и събира резултатите в нов array.
  • .forEach(...) извиква своята функция за всеки enclosure и не събира нищо.

parseInt("7-B00", 10) е 7: parseInt чете цифри от началото и спира при първия символ, който не е цифра. || 0 превръща NaN (изобщо няма цифри) в 0.

Функция, която връща функция ​

Функцията може да създаде функция и да я върне:

js
function foo() {
    return function () {
        console.log("I'm the returned function");
    };
}

foo()();            // call foo, then call what it returned

// foo()() is the same as:
const bar = foo();
bar();
Променете кода и го изпълнете отново

Анимация

Функциите са стойности: 5. Функция, която връща функция

Полезен пример: compose(f, g) връща нова функция, която извиква f, после g върху резултата ѝ:

js
function compose(f, g) {
    return function (x) {
        return g(f(x));
    };
}

function double(x) { return x * 2; }
function increment(x) { return x + 1; }

const doubleThenIncrement = compose(double, increment);
console.log(doubleThenIncrement(3));        // 7

// on the report: first the version, then its major part
function firmwareVersion(d) { return d.inventory.details.firmware_level.split(" ")[0]; }
function majorOf(v) { return v.split(".")[0]; }
const firmwareMajor = compose(firmwareVersion, majorOf);
console.log(firmwareMajor(doc));            // 4
Променете кода и го изпълнете отново

10. Arrow functions ​

MDN: Arrow function expressions

ES6 добави по-кратък начин за писане на function expression: arrow function (наричана и fat arrow; в други езици - Python, Java, C# - lambda). Тя спестява писането на function, return и фигурни скоби, което прави callbacks много по-кратки.

js
const pi = 3.14;

// a function expression
const circleAreaExp = function (r) {
    return r * r * pi;
};

// the same as an arrow function
const circleAreaArrow = r => r * r * pi;

console.log(circleAreaExp(2), circleAreaArrow(2));      // 12.56 12.56
Променете кода и го изпълнете отново

Синтаксисът, стъпка по стъпка ​

Пълната форма - параметри в скоби, =>, тяло във фигурни скоби:

text
(param1, param2, ..., paramN) => { statements }
js
const pi = 3.14;
const circleArea = (r) => { return r * r * pi; };
console.log(`circleArea(2) = ${circleArea(2)}`);
Променете кода и го изпълнете отново

Един параметър: скобите около него могат да се пропуснат. Без параметри или с два и повече скобите са задължителни:

text
singleParam => { statements }
() => { statements }
(a, b) => { statements }

Един израз като тяло: фигурните скоби и return могат да се пропуснат - връща се стойността на израза:

text
(param1, param2, ..., paramN) => expression
js
const pi = 3.14;
const circleArea = r => r * r * pi;
console.log(`circleArea(2) = ${circleArea(2)}`);    // 12.56

const sum = (a, b) => a + b;
const now = () => "2026-10-23";
console.log(sum(2, 3), now());
Променете кода и го изпълнете отново

Фигурните скоби означават "без автоматичен return" ​

Тези две не са едно и също:

  • x => expression връща стойността на израза;
  • x => { expression } е тяло с един statement и без return - връща undefined.
js
const pi = 3.14;

// braces, no return: the result is undefined
const circleArea = r => { r * r * pi };
console.log(`circleArea(2) = ${circleArea(2)}`);    // circleArea(2) = undefined
Променете кода и го изпълнете отново

Това е най-честият бъг с arrow functions. Ако добавите фигурни скоби, добавете и return.

Анимация

Функциите са стойности: 6. Arrow functions

Връщане на обект ​

{ след => винаги започва тяло, затова обектният литерал трябва да е в кръгли скоби:

js
const toRow = enc => ({ "Enclosure": enc.id, "State": enc.state });
console.log(toRow(doc.inventory.enclosures[2]));    // { Enclosure: 'ENC-03', State: 'degraded' }

const broken = enc => { Enclosure: enc.id };        // a body with a label - no error, no result
console.log(broken(doc.inventory.enclosures[2]));   // undefined
Променете кода и го изпълнете отново

Arrow functions като callbacks ​

Тук arrow functions блестят. По-старият callback и модерният, един до друг:

js
var parts1 = "4.2.1.7".split(".").map(function (x) { return parseInt(x, 10) || 0; });
const parts2 = "4.2.1.7".split(".").map(x => parseInt(x, 10) || 0);
console.log(parts1, parts2);

function countIf(list, test) {
    let n = 0;
    for (const item of list) if (test(item)) n++;
    return n;
}

const encs = doc.inventory.enclosures;
console.log(countIf(encs, enc => enc.state !== "normal"));     // 1
console.log(countIf(encs, enc => enc.firmware === "2.10"));    // 2
Променете кода и го изпълнете отново
По-старМодерен
function (x) { return parseInt(x, 10) || 0; }x => parseInt(x, 10) || 0
function (enc) { return enc.state !== "normal"; }enc => enc.state !== "normal"
function (a, b) { return a + b; }(a, b) => a + b
function () { return []; }() => []
function (enc) { console.log(enc.id); }enc => { console.log(enc.id); }

Дълъг callback с няколко statements остава четим като arrow function с фигурни скоби и return - или, още по-добре, става именувана helper функция.

Капан: arrow function след || (advanced) ​

Arrow function не може да стои направо след оператор като ||. Сложете я в скоби:

js
let cb;
cb = cb || function () {};      // ok
cb = cb || () => {};            // SyntaxError: Malformed arrow function parameter list
cb = cb || (() => {});          // ok

Какво нямат arrow functions (advanced) ​

  • Нямат собствено име - винаги са анонимни. Когато са присвоени на variable, взимат нейното име за съобщенията за грешки (const isHdd = enc => ... се показва като isHdd).
  • Нямат arguments - използвайте rest параметър: (...values) => ....
  • Нямат hoisting - те са изрази във variable, затова ги дефинирайте преди да ги използвате.
  • Нямат new - не могат да се използват като конструктори.
  • Нямат свой this - вътре в arrow function this е this на кода около нея. Това е важно за методите на обектите, които идват в занятие 6:
js
const device = {
    name: "LAB-DEV-01",
    hello: function () { console.log(this.name); },     // this = device
    helloArrow: () => { console.log(this.name); }       // this = the outer this, not device
};
device.hello();         // LAB-DEV-01
device.helloArrow();    // not LAB-DEV-01 (in Node.js: undefined)

Правилото на палеца: arrow functions за callbacks и кратки helper функции; function declarations за именуваните helper функции и за методите.

11. Функции във функции; closures ​

Helper функции вътре в rule ​

Много съществуващи правила дефинират helper функциите си вътре в rule. Първо идеята с малък пример: функция вътре в друга функция може да използва variables на външната функция.

js
function checkFirmware(version) {
    function major() {                          // no parameters
        return Number(version.split(".")[0]);   // version of checkFirmware
    }

    if (major() < 4) {
        console.log(version + ": too old, major " + major());
    } else {
        console.log(version + ": OK");
    }
}

checkFirmware("4.2.1.7");       // 4.2.1.7: OK
checkFirmware("3.9.0.1");       // 3.9.0.1: too old, major 3
Променете кода и го изпълнете отново

Какво се случва:

  • major е локална variable на checkFirmware. Нова функция major се създава при всяко извикване на checkFirmware и изчезва, когато checkFirmware върне.
  • major използва version, без да я получава като параметър: намира я в scope-а на checkFirmware, през scope chain (раздел 6). При първото извикване вижда "4.2.1.7", при второто - "3.9.0.1".

В правило външната функция е rule, а helper функциите използват doc по същия начин:

js
function rule(doc) {
    function model() {
        return doc.overview.model;      // doc of rule - not a parameter of model
    }

    if (model() === "XR-220") {
        console.log("check model " + model());
    }
}

rule(doc);      // check model XR-220
Променете кода и го изпълнете отново

Анимация

Функциите и данните: 6. Helper функция в rule

Добре ли е това или зле? Работи. Цената:

  • helper функцията може да се използва само в това правило - друго правило я копира (същият cmpVer във всяко правило);
  • тя може да чете и променя всяка variable на rule, затова трябва да прочетете цялото rule, за да я разберете;
  • не може да се тества самостоятелно.

Указанията искат helper функциите преди rule, на най-горно ниво. Тогава те получават всичко като параметри и по-късно лесно се преместват в обща библиотека (занятие 9 и курсовият проект).

Closures ​

MDN: Closures

Вътрешната функция запазва достъпа си до variables на външната функция дори след като външната функция е върнала. Функцията заедно с variables, които помни, се нарича closure:

js
function makeCounter() {
    let count = 0;
    return () => ++count;       // uses count of makeCounter
}

const counter = makeCounter();      // makeCounter has finished...
console.log(counter());     // 1    ...but count is still alive
console.log(counter());     // 2
console.log(counter());     // 3

const other = makeCounter();        // a new call: a new count
console.log(other());       // 1
Променете кода и го изпълнете отново

Анимация

Функциите са стойности: 7. Closure (advanced)

Практичен closure: фабрика за функции. Тя получава настройка и връща готова проверка с вградена настройка:

js
function above(limit) {
    return value => value > limit;      // remembers limit
}

const tooHot = above(40);       // a temperature limit, in degrees
const tooFull = above(90);      // a used-space limit, in percent

console.log(tooHot(45), tooHot(35));        // true false
console.log(tooFull(95), tooFull(80));      // true false

const temps = [31, 44, 37, 42];
const hot = [];
for (const t of temps) {
    if (tooHot(t)) hot.push(t);
}
console.log(hot);       // [ 44, 42 ]
Променете кода и го изпълнете отново

Всяко извикване на above има свой limit: tooHot помни 40, tooFull помни 90. В домашното пишете olderThan(minimum) - същата идея, но за версии.

IIFE: изпълнете функция веднага (advanced) ​

Единственият начин да изпълните анонимна функция, която не е записана никъде, е да я извикате точно там, където е написана - IIFE (Immediately Invoked Function Expression):

text
(function () { ... })();
js
(function () {
    console.log("I am in nowhere, but I exist and work!");
})();

// with an arrow function
(() => console.log("me too"))();
Променете кода и го изпълнете отново

Първата двойка скоби превръща функцията в израз (statement, който започва с function, би бил declaration, а declaration трябва да има име). () в края я извиква.

Преди да съществуват модулите, IIFE беше начинът variables да се пазят извън global scope: всичко, декларирано вътре, е локално за IIFE. Старите библиотеки са написани така - IIFE се изпълнява веднъж и връща функция, а тази функция помни скритата (private) variable (closure):

js
var nextAlertId = (function () {
    var count = 0;                      // private: not visible outside
    return function () {
        count++;
        return "ALERT-" + count;
    };
})();

console.log(nextAlertId());     // ALERT-1
console.log(nextAlertId());     // ALERT-2
console.log(typeof count);      // undefined - it did not leak
Променете кода и го изпълнете отново

Често IIFE връща няколко такива функции заедно, в обект (обекти: занятие 6).

Днес файловете, които са модули (занятие 9), имат свой scope и IIFE рядко е нужна. Все още ще я срещнете в по-стар код.

Анимация

Scope на variables: 6. IIFE (advanced); Функциите са стойности: 5. Функция, която връща функция (IIFE в края)

Функция, която извиква себе си (advanced) ​

Функцията може да извика себе си - това е рекурсия. Трябва ѝ базов случай: условие, при което спира. Всяко извикване получава своя рамка:

js
function factorial(n) {
    if (n <= 1) return 1;               // base case
    return n * factorial(n - 1);        // a smaller problem
}
console.log(factorial(3));              // 6
Променете кода и го изпълнете отново

Рекурсията е подходяща за вложени данни с неизвестна дълбочина - например търсене на ключ на всяко ниво на отчета (занятие 6). Без базов случай стекът прелива: RangeError: Maximum call stack size exceeded.

Анимация

Стекът на извикванията: 6. Рекурсия (advanced), 7. Препълване на стека (advanced)

Плитко копие (advanced) ​

Чиста helper функция, която трябва да върне променен array, често първо го копира със синтаксиса spread [...arr] (лекция 4). Копието е плитко (shallow): нов е само външният array, вътрешните arrays все още са общи:

js
function addReading(table, row, value) {
    const copy = [...table];        // a new outer array with the same rows
    copy[row].push(value);          // but copy[row] is the SAME inner array!
    return copy;
}

const temps = [[31, 34], [40]];     // the readings of two enclosures
const temps2 = addReading(temps, 0, 37);
console.log(temps[0]);              // [ 31, 34, 37 ] - the original changed too
console.log(temps2 === temps, temps2[0] === temps[0]);     // false true

// pure: copy the row you change too
function addReadingPure(table, row, value) {
    const copy = [...table];
    copy[row] = [...table[row], value];     // a new inner array
    return copy;
}
const temps3 = addReadingPure(temps, 1, 42);
console.log(temps[1], temps3[1]);   // [ 40 ] [ 40, 42 ]
Променете кода и го изпълнете отново

Копие на обект с { ...obj } е плитко по същия начин - занятие 6 го показва, заедно с пълно (дълбоко) копие.

12. Структурата на едно правило ​

Файлът с правило има фиксирана форма. Указанията за правилата обикновено казват:

  • "Ако са нужни допълнителни функции, те трябва да са преди функцията rule."
  • "Глобалните variables (дефинирани извън функциите) трябва да са декларирани като const, а не var / let, и вашите функции не трябва да ги променят."
  • function rule(doc) { - "никога не променяйте този ред": engine-ът търси точно него.

Затова файлът с правило се чете отгоре надолу:

js
/*
 * CHK-0211 - enclosure firmware below the recommended level.
 * Models XR-210 and XR-220 only. Source of the levels: <link>, checked 2026-10-23.
 */

// 1. fixed settings: const, never changed
const MODELS = ["XR-210", "XR-220"];
const MIN_FIRMWARE = "2.14";

// 2. helpers: small, pure, everything comes in as a parameter
function compareVersions(a, b) { /* ... */ }
function versionBelow(a, b) { /* ... */ }
function deviceName(doc, fallback = "unknown device") { /* ... */ }

// 3. the rule: the steps, in order; the only place with side effects (emit)
function rule(doc) {  // never change this line
    /* ... */
}

Нашата собствена versionBelow ​

Лекция 3 показа защо версиите не могат да се сравняват като strings ("4.10" < "4.2" е true). Сега пишем helper функцията. Първо изискванията - с какво трябва да се справя?

СлучайПримерОчаквано
част по част, като числа"4.10" срещу "4.2"4.10 е по-висока
различен брой части"4.2" срещу "4.2.0.0"равни
суфикс на една част"4.2.1.7-B00" срещу "4.2.1.7"равни (тук суфиксът се пренебрегва)
число вместо string2.14 срещу "2.14"равни
липсваща стойностundefined срещу "1.0"третира се като "", т.е. 0 - по-ниска

Много правила пишат една helper функция, която отговаря на "по-ниска, равна или по-висока?" с -1, 0 или 1 (в съществуващите правила често се казва cmpVer). Въпросите с да / не тогава са по един ред:

js
function compareVersions(a, b) {
    const pa = String(a ?? "").split(".");
    const pb = String(b ?? "").split(".");
    const n = Math.max(pa.length, pb.length);
    for (let i = 0; i < n; i++) {
        const x = parseInt(pa[i], 10) || 0;     // "7-B00" -> 7, undefined -> 0
        const y = parseInt(pb[i], 10) || 0;
        if (x < y) return -1;
        if (x > y) return 1;
    }
    return 0;
}

function versionBelow(current, minimum) {
    return compareVersions(current, minimum) < 0;
}

function versionAtLeast(current, minimum) {
    return compareVersions(current, minimum) >= 0;
}

// test it on the cases of the table
const cases = [
    ["4.10", "4.2"],
    ["4.2", "4.2.0.0"],
    ["4.2.1.7-B00", "4.2.1.7"],
    [2.14, "2.14"],
    [undefined, "1.0"],
    ["4.2.1.7", "4.2.1.10"]
];
for (const [a, b] of cases) {
    console.log(`${a} vs ${b}: ${compareVersions(a, b)}, below: ${versionBelow(a, b)}`);
}
Променете кода и го изпълнете отново

(for (const [a, b] of cases) взима двата елемента на всеки вътрешен array - кратка форма на const a = pair[0], b = pair[1];. Занятие 6 го обяснява.)

Суфиксът (-B00), версии като 42107-C00 или XR420_117 и другите формати идват в занятие 8. С една обща helper функция тази поправка ще се направи веднъж - а не във всяко правило.

Helper функции, които връщат текст (optional) ​

Съществуващите правила често превръщат едно сравнение в статус и текст за действие, с по една малка helper функция:

js
function compareVersions(a, b) {
    const pa = String(a ?? "").split(".");
    const pb = String(b ?? "").split(".");
    for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
        const x = parseInt(pa[i], 10) || 0;
        const y = parseInt(pb[i], 10) || 0;
        if (x !== y) return x < y ? -1 : 1;
    }
    return 0;
}

function versionStatus(current, minimum, recommended) {
    if (compareVersions(current, minimum) < 0) return "Below minimum";
    if (compareVersions(current, recommended) < 0) return "Below recommended";
    return "OK";
}

function actionText(status) {
    if (status === "Below minimum") return "Upgrade as soon as possible.";
    if (status === "Below recommended") return "Plan an upgrade.";
    return "No action required.";
}

for (const level of ["4.1.0.3", "4.2.0.9", "4.2.1.7"]) {
    const status = versionStatus(level, "4.2.0.0", "4.2.1.0");
    console.log(level, "|", status, "|", actionText(status));
}
Променете кода и го изпълнете отново

Сравнете това с лекция 3, където същата логика беше верига if / else if и switch вътре в правилото. Сега всеки въпрос има име и всяка helper функция може да се провери самостоятелно.

Разделяне на дълго правило на helper функции ​

Когато правилото стане дълго, потърсете частите, които отговарят на един въпрос или дават една стойност, и дайте име на всяка:

  1. Намерете блок, който изчислява стойност - "името на устройството", "версията на firmware", "списъкът с проблеми". Той става helper функция, която връща тази стойност.
  2. Всичко, което чете, става параметър. Ако чете doc, подайте doc (или само частта, която ѝ трябва: doc.inventory.enclosures).
  3. Наречете я според това, което връща: deviceName(doc), firmwareVersion(doc), enclosureProblems(list); въпрос с да / не започва с is / has: isSupported(model).
  4. Не променяйте входните данни - върнете нова стойност (раздел 8).
  5. Тествайте я самостоятелно с няколко реда console.log (или с check във файловете за упражнения), включително странните случаи: липсваща стойност, празна, грешен тип.

Правилото в началото на тази лекция е резултатът от този процес, приложен върху правилото от лекция 4.

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

Ето правило в стила на много съществуващи правила - helper функции, вложени в rule, var, callbacks, написани като function (x) { ... }, и try / catch около всичко. Първо го изпълнете:

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

function rule(doc) {  // never change this line

    function cmpVer(a, b) {
        a = (a === undefined || a === null) ? "" : String(a);
        b = (b === undefined || b === null) ? "" : String(b);

        var as = a.split(".").map(function (x) { return parseInt(x, 10) || 0; });
        var bs = b.split(".").map(function (x) { return parseInt(x, 10) || 0; });

        var n = Math.max(as.length, bs.length);
        for (var i = 0; i < n; i++) {
            var ai = (i < as.length) ? as[i] : 0;
            var bi = (i < bs.length) ? bs[i] : 0;
            if (ai < bi) return -1;
            if (ai > bi) return 1;
        }
        return 0;
    }

    function getLowestFirmware(list, type) {
        var lowest;
        for (var i = 0; i < list.length; i++) {
            var e = list[i] || {};
            if (e.type !== type) continue;
            if (e.state && e.state !== "normal") continue;
            if (lowest === undefined || cmpVer(e.firmware, lowest) < 0) lowest = e.firmware;
        }
        return lowest;
    }

    try {
        var model = doc.overview.model;
        if (!model || String(model).indexOf("XR-2") !== 0) return;

        var expected = "2.14";
        var lowest = getLowestFirmware(doc.inventory.enclosures, "SSD-24");

        if (lowest !== undefined && cmpVer(lowest, expected) < 0) {
            var output = new Object();
            output["Device"] = doc.inventory.extended_info.json.data.system.name;
            output["Model"] = model;
            output["Level"] = doc.inventory.details.firmware_level;
            output["SSD-24 firmware"] = lowest + " (expected >= " + expected + ")";
            emit(doc.status.device_alias, { "check": "CHK-0211", "details": [output] });
        }
    } catch (err) {
        return;
    }
}

console.log("example JSON document:");
rule(doc);

console.log("a second report:");
rule({
    overview: { model: "XR-220" },
    status: { device_alias: "LAB-DEV-05" },
    inventory: {
        details: { firmware_level: "4.2.1.7" },
        enclosures: [{ id: "ENC-01", type: "SSD-24", firmware: "2.10", state: "normal" }]
    }
});
console.log("(end)");
Променете кода и го изпълнете отново

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

  1. Какви функции има и откъде може да се извика всяка?
  2. Engine-ът извиква това правило за 1000 отчета. Колко пъти се създава cmpVer? Може ли друго правило да я използва?
  3. Вторият отчет има SSD-24 enclosure с firmware 2.10 - под 2.14. Защо няма alert за LAB-DEV-05?
  4. В getLowestFirmware от какво пази list[i] || {}? А if (e.state && e.state !== "normal") continue; - кои enclosures пропуска и кои запазва?
  5. cmpVer променя параметрите си a и b (a = ... String(a)). Променя ли това нещо за извикващия? Чиста ли е cmpVer?
Отговор
  1. rule (глобална - engine-ът я извиква), cmpVer и getLowestFirmware (локални за rule - само rule и функциите в нея могат да ги извикат) и два анонимни callbacks, подадени на map.
  2. 1000 пъти - нова cmpVer при всяко извикване на rule. Това струва малко, но никое друго правило не може да я използва: всяко правило има свое копие (и свои бъгове).
  3. Отчетът няма extended_info, затова doc.inventory.extended_info.json хвърля TypeError. catch го хваща и връща тихо. Истински проблем е пропуснат и никой не вижда грешка. (Занятие 9 е за грешките; поправката тук е ?. и резервно име.)
  4. list[i] || {}: от елемент, който е null или undefined - e.type би хвърлило грешка. Редът със state пропуска enclosures, чието състояние е зададено и не е "normal"; запазва тези без състояние (undefined е falsy, затова continue не се изпълнява).
  5. Не: a и b са strings (примитиви), параметрите са локални копия. cmpVer е чиста - само чете аргументите си и връща число.

Модернизиране, стъпка по стъпка ​

Променяйте по едно нещо и изпълнявайте правилото след всяка стъпка - alerts трябва да останат същите (освен при истински поправки на бъгове, които правите нарочно).

Стъпка 1. Изнесете helper функциите извън rule. Те вече получават всичко като параметри, затова могат да се преместят както са. Сега се създават веднъж и могат да се копират в обща библиотека.

Стъпка 2. var -> const / let. const за всичко, на което не се присвоява нова стойност; let за броячите на loops и за стойностите, които се променят (lowest).

Стъпка 3. Callbacks -> arrow functions и по-прости проверки. function (x) { return parseInt(x, 10) || 0; } става x => parseInt(x, 10) || 0. (a === undefined || a === null) ? "" : String(a) става String(a ?? "").

Резултатът:

js
function emit(id, details) {
    console.log("ALERT for " + id + ": " + JSON.stringify(details));
}

function cmpVer(a, b) {
    const as = String(a ?? "").split(".").map(x => parseInt(x, 10) || 0);
    const bs = String(b ?? "").split(".").map(x => parseInt(x, 10) || 0);

    const n = Math.max(as.length, bs.length);
    for (let i = 0; i < n; i++) {
        const ai = (i < as.length) ? as[i] : 0;
        const bi = (i < bs.length) ? bs[i] : 0;
        if (ai < bi) return -1;
        if (ai > bi) return 1;
    }
    return 0;
}

function getLowestFirmware(list, type) {
    let lowest;
    for (let i = 0; i < list.length; i++) {
        const e = list[i] || {};
        if (e.type !== type) continue;
        if (e.state && e.state !== "normal") continue;
        if (lowest === undefined || cmpVer(e.firmware, lowest) < 0) lowest = e.firmware;
    }
    return lowest;
}

function rule(doc) {  // never change this line
    try {
        const model = doc.overview.model;
        if (!model || String(model).indexOf("XR-2") !== 0) return;

        const expected = "2.14";
        const lowest = getLowestFirmware(doc.inventory.enclosures, "SSD-24");

        if (lowest !== undefined && cmpVer(lowest, expected) < 0) {
            const output = new Object();
            output["Device"] = doc.inventory.extended_info.json.data.system.name;
            output["Model"] = model;
            output["Level"] = doc.inventory.details.firmware_level;
            output["SSD-24 firmware"] = lowest + " (expected >= " + expected + ")";
            emit(doc.status.device_alias, { "check": "CHK-0211", "details": [output] });
        }
    } catch (err) {
        return;
    }
}

console.log("example JSON document:");
rule(doc);

console.log("a second report:");
rule({
    overview: { model: "XR-220" },
    status: { device_alias: "LAB-DEV-05" },
    inventory: {
        details: { firmware_level: "4.2.1.7" },
        enclosures: [{ id: "ENC-01", type: "SSD-24", firmware: "2.10", state: "normal" }]
    }
});
console.log("(end)");
Променете кода и го изпълнете отново

Alerts са същите като преди. Вторият отчет все още не получава alert: try все още скрива липсващия extended_info (въпрос 3 по-горе).

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

  1. if (lowest !== undefined && cmpVer(lowest, expected) < 0) { ... } може да се напише и като guard clause: if (lowest === undefined || cmpVer(lowest, expected) >= 0) return;, а кодът за alert - след него. Защо това е същото условие, обърнато? (Лекция 3: De Morgan.)
  2. Кои от функциите са чисти?
  3. expected е фиксирана настройка на правилото. Къде бихте я сложили и как бихте я декларирали?
Отговор
  1. "Спри, ако не (A и B)" е "спри, ако (не A) или (не B)": обратното на lowest !== undefined е lowest === undefined, а обратното на < 0 е >= 0. Guard clause-ът спира рано, затова кодът за alert не е вложен в if.
  2. cmpVer и getLowestFirmware - те само четат аргументите си и връщат стойност. rule не е: извиква emit (това е работата ѝ).
  3. В началото на файла, извън rule: const EXPECTED_FIRMWARE = "2.14";. Никога не се променя, затова е const, както искат указанията (раздел 7). Горе лесно се намира и обновява, когато се препоръча ново ниво, без да четете кода на rule.

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

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

bash
git pull --no-rebase --no-edit upstream main
node 05-functions/practice/task1-predict.js

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

ЗадачаФайлКакво правите
1task1-predict.jsПредсказвате 20 резултата: scope, shadowing, стойности по подразбиране, return, hoisting, arrow functions
2task2-versions.jscompareVersions, versionBelow и versionStatus - с всички странни случаи
3task3-helpers.jsМалки чисти helper функции: asList, capitalize, lastPart, deviceName със стойност по подразбиране
4task4-callbacks.jscustomMap, customFilter, countIf, compose - и използването им с arrow functions върху enclosures
5task5-classic-functions.jsКласически задачи като функции: лице, суми, най-голямо четно число, най-добър резултат, таблица

Обобщение ​

ТемаЗапомнете
ФункцияИменувана поредица от действия; дефинира се веднъж, извиква се много пъти; f е функцията, f() я извиква
Declaration / expressionfunction f() {} (издига се) / const f = function () {}; (завършва с ;)
ПараметриЛокални variables, попълват се по позиция; липсващ -> undefined, излишен -> пренебрегва се; ...rest за произволен брой
Стойности по подразбиранеfunction f(x = 1) - само за undefined; x = x || 1 заменя и 0, "", false, null
returnПрекратява функцията и дава стойността; без return -> undefined; дръжте стойността на същия ред
ScopeГлобален, на функцията, на блока (let / const); вътрешните функции виждат външните variables, а не обратното
varScope на функцията, може да се декларира отново, издига се като undefined - четете го, пишете let / const
HoistingДекларациите са известни от началото на своя scope; function declarations могат да се извикат по-рано
АргументиПримитивите се копират; arrays и обектите са общи - не променяйте doc
Чиста функцияРезултатът зависи само от аргументите; не променя нищо отвън
CallbackФункция, подадена на функция: countIf(list, enc => ...); f, а не f()
Arrow functionx => expr връща expr; с { } ви трябва return; ({ ... }) за обект
ClosureВътрешната функция пази живи variables на външната
Файл с правилоКоментар в началото, настройки с const, чисти helper функции, после function rule(doc)
Стар стилHelper функции в rule, var, function (x) { return ...; }, try / catch, който хваща всичко - четете го, модернизирайте стъпка по стъпка

Домашно ​

  1. Започнете с 05-functions/homework-small-functions.js (в homework repository): 20 малки функции, от square(x) до версия, разделена на числа. Всяка функция пишете сами, от думата function до return. Използват само раздели 1 - 7 от тази лекция и предишните лекции.
  2. 05-functions/homework.js (в homework repository): най-високата версия на модул в отчет, фабрика за функции за проверка на версии и по-старо правило, разделено на helper функции - със същите alerts.
  3. Задачите за упражнение, които не сте довършили в час - препоръчително, защото следващите задачи и курсовият проект ги използват отново. Всяка проверка трябва да отпечата OK.
  4. По желание: вземете едно от вашите правила. Избройте функциите му: къде е дефинирана всяка, какво получава, какво връща, чиста ли е? Има ли helper функция, чието копие имат и други правила? Коя бихте преместили първо в обща библиотека?

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

Следващото занятие: обекти. Дълбоки пътища и ключове с точки, речници с ключове id, едни и същи данни като array в един отчет и като обект в друг - и първата ви версия на get(doc, path, fallback) за курсовия проект.

Създаден за