Български
Тема
Български
Тема
Днес ще се научите да:
return;let / const замениха var;function (x) { ... } и като x => ...;versionBelow(a, b);var, вложени helper функции, callbacks function (x) { return ...; }, try / catch - и да го модернизирате стъпка по стъпка.Раздели, отбелязани с (advanced) или (optional)
В тази лекция има повече, отколкото се събира в едно занятие. Разделите, отбелязани с (advanced), навлизат по-дълбоко, а тези с (optional) добавят още един пример към тема, която вече знаете. В час може да ги пропуснем - прочетете ги вкъщи. Домашното не ги изисква.
Къде е мястото на тази тема в примерното правило за проверка - днешното правило е изградено от малки helper функции. Числото във всеки коментар е разделът от тази лекция:
// 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("; ")
});
}rule е кратко и всяка стъпка има име. versionBelow вече не е готова helper функция - пишем я в раздел 12."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-ът я извиква веднъж за всеки отчет.
Функцията е поредица от действия, които вършат една задача, събрани под едно име. Представете си рецепта: "палачинки" е името; стъпките са записани веднъж; готвите ги, когато поискате. Резултатът може да зависи от допълнителни входни данни: палачинки с мляко или палачинки с вода.
В програмирането функцията е малка програма в програмата. Дефинирате я веднъж и я извиквате (изпълнявате) колкото пъти ви трябва, с различни входни данни.
versionBelow(level, "4.2") се чете по-лесно от десет реда split и loops.Ето проблема, който функциите решават. Една и съща проверка на версия, копирана три пъти:
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 trueconst 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] - точно такъв бъг създава копирането. С функция логиката се пише веднъж:
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"));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 ще я направим надеждна.
Много езици различават два вида подпрограми:
y = f(x). Резултатът може да се използва в по-голям израз.В JavaScript и двете се пишат с function. В едно правило:
| Вид | Пример | Използва се като |
|---|---|---|
| процедура | emit(name, details), console.log(...) | statement на отделен ред |
| функция | versionBelow(a, b), deviceName(doc) | стойност: if (versionBelow(...)), const name = deviceName(doc); |
Добрите helper функции са най-вече от втория вид: те изчисляват и връщат, а правилото решава какво да прави с резултата.
MDN: function declaration
function <name>(<parameters>) {
<body>
}<name> - името на функцията. Същите правила като за имената на variables (вижте identifiers в MDN). Използвайте глагол или въпрос, защото функцията прави нещо: getSerial, deviceName, isSupported, hasFeature, versionBelow.<parameters> - нула или повече имена, разделени със запетаи. Кръглите скоби са задължителни, дори когато няма параметри.<body> - statements между { и }.} на декларацията не е нужна ; (но не пречи).// no parameters
function printLine() {
console.log("------------------------");
}
// two parameters
function printSum(x, y) {
console.log(x + y);
}
printLine();
printSum(2, 3); // 5
printLine();// 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):
<name>(<arguments>);<arguments> - стойностите, подадени на параметрите при това извикване.( (работи, но лесно се чете погрешно).greet е функцията, greet() я изпълнява.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 itfunction 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 също е функция - вградена. Извиквате я с аргумент, както своите функции. И една функция може да извиква други функции:
function square(n) {
return n * n;
}
function sumOfSquares(a, b) {
return square(a) + square(b);
}
console.log(sumOfSquares(2, 3)); // 4 + 9 = 13function 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 функцията получава документа като параметър:
function getModel(doc) {
return doc.overview.model;
}
function printModel(doc) {
console.log("The model is " + getModel(doc));
}
printModel(doc); // The model is XR-220function 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.
MDN: function expression
Функцията е и стойност. Можете да я създадете в израз и да я запишете във variable:
const <name> = function (<parameters>) {
<body>
};Самата функция тук няма име - тя е анонимна функция. Достигате я чрез variable-ата.
const greet = function (userName) {
console.log(`Hello ${userName}`);
};
greet("Ada");const greet = function (userName) {
console.log(`Hello ${userName}`);
};
greet("Ada");На реда const greet = function ... стават две неща: декларира се variable-ата greet, после се създава функция и се присвоява на нея - като const x = 5;. Затова function expression завършва с ;, както всяко друго присвояване.
Function expression може да има собствено име, но то се вижда само вътре във функцията (полезно за съобщенията за грешки и за функция, която извиква себе си). Отвън използвайте variable-ата:
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 ReferenceErrorconst 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 слепва двата реда. Пример:
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 morelet 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 moreJavaScript го чете като let foo = function () {...}[x, y] = [1, 2];: задава property на обекта-функция, x и y никога не получават стойност, а foo получава array-я. Извикване foo() след това е TypeError: foo is not a function. Слагайте ; след всяко присвояване - и след function expression - и бъгът не може да се случи.
И двата работят. В правилата и във файловете с helper функции най-честият избор е:
function versionBelow(a, b) { ... }) - четат се като списък от инструменти;Rule engine-ът извиква function rule(doc) - declaration, и този ред трябва да остане какъвто е.
function sum(x, y).sum(2, 3).Първият аргумент отива в първия параметър, вторият - във втория и т.н. - по позиция, а не по име:
function sum(x, y) {
// here the engine has already done: let x = 2, y = 3;
console.log(x + y);
}
sum(2, 3); // 5function 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;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 ignoredfunction 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 ignoredVariable, която изобщо не е декларирана, все пак е грешка:
function sum(x) {
console.log(x); // 2
console.log(x + y); // ReferenceError: y is not defined
}
sum(2, 3);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) и съдържа всички аргументи на извикването:
// 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// 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)); // 9arguments прилича на array, но не е (Array.isArray(arguments) е false). Модерният код пише вместо това rest параметър ...name. Той събира останалите аргументи в истински array:
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");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).
Функция rectangleArea(width, height), използвана за стаите на един апартамент:
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 30function 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 в числа. (+ не би го направил: би ги слепил.)
MDN: Default parameters
Стойност по подразбиране е стойността, която параметърът получава, когато за него не е подаден аргумент. От ES6 (2015) се пише в списъка с параметри:
function f(x = 1, y = 2, z = 3) {
console.log(x, y, z);
}
f(6, 7); // 6 7 3
f(); // 1 2 3function f(x = 1, y = 2, z = 3) {
console.log(x, y, z);
}
f(6, 7); // 6 7 3
f(); // 1 2 3По-старият код използва вместо това || (лекция 3, || за стойност по подразбиране):
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!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.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 1function 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) една до друга.
Стойността по подразбиране се изчислява при всяко извикване и може да използва параметрите преди нея:
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 SN0001function 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 SN0001return: връщане на резултат MDN: return
Всяко извикване на функция е израз - като 2 + 2, то дава стойност. Тази стойност се определя от return:
return <expression>;const res = sum(2, 3); става const res = 5;.return прекратява функцията веднага. Нито един statement след него не се изпълнява.return (или само с return;) връща undefined.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()); // undefinedfunction 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:
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")); // undefinedfunction 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 - но показва на читателя, че за "не е намерен" е помислено.
В лекция 3 правилото спираше рано с if (...) return;. Helper функциите използват същия модел. Първо проверете лошите случаи и върнете; основната работа идва накрая, без дълбоко влагане:
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 linefunction 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Много често срещан по-стар модел:
function isSupported(model) {
if (model === "XR-210" || model === "XR-220") {
return true;
} else {
return false;
}
}
console.log(isSupported("XR-220"));function isSupported(model) {
if (model === "XR-210" || model === "XR-220") {
return true;
} else {
return false;
}
}
console.log(isSupported("XR-220"));Условието вече е true или false. Върнете го:
function isSupported(model) {
return model === "XR-210" || model === "XR-220";
}
console.log(isSupported("XR-220"), isSupported("XR-300")); // true falsefunction 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:
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")); // truefunction 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 (или обект), който ги съдържа:
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.7function 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 е функция: връща стойност, която извикващият може да използва.
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 за упражнение.)
MDN: Scope
Scope на една variable е частта от кода, в която можете да я използвате. Има три вида:
{ и } на if, на loop или на обикновен блок, за let и const (от ES6).// 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// 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. Глобален и локален
Variable, декларирана извън всички функции, е глобална: всяка функция може да я чете и всяка функция може да я променя. В една страница в браузъра всички файлове със скриптове споделят един global scope - глобално име в един файл може да се сблъска със същото име в друг файл.
Пример: страницата зарежда две библиотеки и двете дефинират глобална variable със същото име. Втората печели:
// ---- 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// ---- 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).
Локална variable със същото име като външна засенчва (скрива) външната вътре във функцията (shadowing). Това са две различни variables:
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 changedlet 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:
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);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 функцията работи само с това, което получава.
Проверете се: преди да го изпълните, запишете трите реда, които ще отпечата.
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);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. Той търси навън и:
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 foovar 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"; на първия ред на файла (или на функцията):
"use strict";
function foo() {
leakedName2 = "Lovelace"; // ReferenceError in strict mode
}
foo();"use strict";
function foo() {
leakedName2 = "Lovelace"; // ReferenceError in strict mode
}
foo();Всички файлове за упражнения в този курс започват с "use strict";. Модулите и класовете винаги са strict. Дали rule engine-ът изпълнява правилата в strict mode, не знаем - затова винаги декларирайте variables и въпросът става без значение.
Анимация
Scope на variables: 4. Без деклариране
Функции могат да се пишат вътре в други функции. Вътрешната функция вижда своите variables, после тези на външната функция, после глобалните - както търсите име в стаята си, после в апартамента, после в сградата. Това е веригата от scopes (scope chain). Външната функция не вижда variables на вътрешната:
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();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):
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, Adalet 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Анимация
Scope на variables: 2. Веригата от scopes (scope chain), 7. Лексикален scope (advanced)
let, const и var Лекция 2 каза "използвайте const и let, а не var". Ето защо. Съществуващите правила използват var навсякъде, затова трябва да знаете какво прави.
let и const се виждат само в блока, в който са декларирани. var пренебрегва блоковете: принадлежи на цялата функция (или е глобална).
{
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{
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 herelet в блок е нова variable, дори със същото име като външна:
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 = nonelet 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 - не.
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); // undefinedfor (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:
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 declaredvar 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 declaredconst (advanced) const създава variable, на която не може да се присвои нова стойност. Грешката спира програмата, затова последният ред не се изпълнява:
const GOLDEN_RATIO = 1.618;
console.log(GOLDEN_RATIO); // 1.618
GOLDEN_RATIO = 2; // TypeError: Assignment to constant variable.
console.log(GOLDEN_RATIO); // not printedconst 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):
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.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, която правилото променя, пази стойността си от предишния отчет:
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) - wronglet 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 използвайте само за фиксирани настройки:
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" }); // 1const 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(...) вътре във функция пак би било промяна. "Никога не се променя" е правило, което вие спазвате.
MDN: Hoisting
Преди JavaScript да изпълни кода на един scope, той първо го преминава веднъж и регистрира всички декларации в него. Можете да мислите за две минавания:
Така декларациите са известни от началото на своя 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 |
console.log("x:", x); // x: undefined - the declaration is known, the value not yet
var x = 10;
console.log("x:", x); // x: 10console.log("x:", x); // x: undefined - the declaration is known, the value not yet
var x = 10;
console.log("x:", x); // x: 10Function declaration се издига заедно с тялото си, затова може да се извика преди своя ред:
console.log(foo()); // I am foo() and I work!
function foo() {
return "I am foo() and I work!";
}console.log(foo()); // I am foo() and I work!
function foo() {
return "I am foo() and I work!";
}Function expression е стойност във variable. Издига се само variable-ата:
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!";
};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: двете фази на изпълнение: 1. var и undefined, 2. Function declaration, 3. Function expression, 4. let / const и TDZ, 5. Hoisting във функция, 6. let в блок (advanced), 7. TDZ: време, не място (advanced)
Какво означава това за правилата:
rule - но указанията искат helper функциите преди rule. Спазвайте ги: файлът се чете отгоре надолу, а helper функциите, написани като const f = ..., и без това трябва да са първи.var, използвана преди своя ред, тихо дава undefined. let / const дава ясна грешка. Още една причина да предпочитате let / const.Проверете се: какво отпечатва това и защо?
var level = "global";
function f() {
console.log(level);
var level = "local";
console.log(level);
}
f();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 го показва стъпка по стъпка.)
Какво получава функцията, когато ѝ подадете стойност - самата стойност или копие?
null, undefined) се копира в параметъра. Промяната на параметъра не променя variable-ата на извикващия.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 unchangedfunction 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 unchangedfunction 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' ] - changedfunction 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:
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); // 0function 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 (или част от него), може да промени самия отчет - и всяка проверка след нея вижда променените данни:
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 objectfunction 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" - и дава грешен отговор. Поправката: изчислете и върнете, не пишете във входните данни:
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 - unchangedfunction 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):
majorOf, versionBelow, deviceName са чисти. shortFirmware не е (променя аргумента си), addModel не е, и първото rule от раздел 7 не е (променя глобална variable).
Чистите helper функции лесно се четат, тестват и използват повторно: за да разберете едно извикване, трябва да погледнете само аргументите му. Дръжте страничните ефекти на едно място - самото rule извиква emit. Всичко, което то извиква, трябва да е чисто.
// 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));// 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 функцията трябва да върне променен списък, тя създава нов:
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 unchangedfunction 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 го показват без функции; параметърът е просто още едно име.
В JavaScript функцията е стойност, като число или array. Можете да:
Затова функциите се наричат first-class citizens ("граждани от първа класа", first-class functions). Това е основата на почти всяка модерна JavaScript библиотека - и на методите на arrays от занятие 7.
Анимация
Функциите са стойности: 1. Като 5 и [1, 2, 3] - число, array и функция, едно до друго: едни и същи стъпки работят и за трите, а само функцията може да се извика.
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 namefunction 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. Функцията е стойност
Функция без име е анонимна функция. Използва се там, където функцията трябва само като стойност: присвоена на variable, сложена в array или обект, подадена на друга функция или върната.
// 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// 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 и обекти
Истинска употреба: таблица от проверки, всяка от тях - малка функция. Правилото ги изпълнява всичките:
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' ]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' ]MDN: Callback function
Функция, подадена като аргумент на друга функция, се нарича callback: другата функция я "извиква обратно", когато ѝ потрябва.
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);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);Обърнете внимание на разликата между подаването на функцията и подаването на резултата ѝ:
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 functionfunction 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
В лекция 4 писахте един и същ loop много пъти: "преброй enclosures, които не са normal", "преброй enclosures със стар firmware"... Променяше се само условието. С callback loop-ът се пише веднъж, а условието идва като функция:
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)); // 2function 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)); // 2countIf не знае какво брои - решава callback-ът. Тя извиква test(item) веднъж за всеки елемент и използва върнатото true / false.
Анимация
Функциите и данните: 5. Callback, извикване по извикване - всяко извикване на callback-а получава своя рамка.
Две класически упражнения с callbacks. customMap създава нов array с резултата от callback-а за всеки елемент; customFilter запазва само елементите, за които callback-ът връща true:
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 ]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 е за тях; днес трябва само да можете да ги четете.
function (x) { ... } Съществуващите правила подават анонимни функции направо в извикването:
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);
});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.
Функцията може да създаде функция и да я върне:
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();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 върху резултата ѝ:
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)); // 4function 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)); // 4MDN: Arrow function expressions
ES6 добави по-кратък начин за писане на function expression: arrow function (наричана и fat arrow; в други езици - Python, Java, C# - lambda). Тя спестява писането на function, return и фигурни скоби, което прави callbacks много по-кратки.
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.56const 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Пълната форма - параметри в скоби, =>, тяло във фигурни скоби:
(param1, param2, ..., paramN) => { statements }const pi = 3.14;
const circleArea = (r) => { return r * r * pi; };
console.log(`circleArea(2) = ${circleArea(2)}`);const pi = 3.14;
const circleArea = (r) => { return r * r * pi; };
console.log(`circleArea(2) = ${circleArea(2)}`);Един параметър: скобите около него могат да се пропуснат. Без параметри или с два и повече скобите са задължителни:
singleParam => { statements }
() => { statements }
(a, b) => { statements }Един израз като тяло: фигурните скоби и return могат да се пропуснат - връща се стойността на израза:
(param1, param2, ..., paramN) => expressionconst 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());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());Тези две не са едно и също:
x => expression връща стойността на израза;x => { expression } е тяло с един statement и без return - връща undefined.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) = undefinedconst 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
{ след => винаги започва тяло, затова обектният литерал трябва да е в кръгли скоби:
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])); // undefinedconst 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 блестят. По-старият callback и модерният, един до друг:
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")); // 2var 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 функция.
|| (advanced) Arrow function не може да стои направо след оператор като ||. Сложете я в скоби:
let cb;
cb = cb || function () {}; // ok
cb = cb || () => {}; // SyntaxError: Malformed arrow function parameter list
cb = cb || (() => {}); // okconst isHdd = enc => ... се показва като isHdd).arguments - използвайте rest параметър: (...values) => ....new - не могат да се използват като конструктори.this - вътре в arrow function this е this на кода около нея. Това е важно за методите на обектите, които идват в занятие 6: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 функции и за методите.
rule Много съществуващи правила дефинират helper функциите си вътре в rule. Първо идеята с малък пример: функция вътре в друга функция може да използва variables на външната функция.
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 3function 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 по същия начин:
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-220function 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
Добре ли е това или зле? Работи. Цената:
cmpVer във всяко правило);rule, затова трябва да прочетете цялото rule, за да я разберете;Указанията искат helper функциите преди rule, на най-горно ниво. Тогава те получават всичко като параметри и по-късно лесно се преместват в обща библиотека (занятие 9 и курсовият проект).
MDN: Closures
Вътрешната функция запазва достъпа си до variables на външната функция дори след като външната функция е върнала. Функцията заедно с variables, които помни, се нарича closure:
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()); // 1function 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: фабрика за функции. Тя получава настройка и връща готова проверка с вградена настройка:
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 ]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 (Immediately Invoked Function Expression):
(function () { ... })();(function () {
console.log("I am in nowhere, but I exist and work!");
})();
// with an arrow function
(() => console.log("me too"))();(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):
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 leakvar 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 в края)
Функцията може да извика себе си - това е рекурсия. Трябва ѝ базов случай: условие, при което спира. Всяко извикване получава своя рамка:
function factorial(n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // a smaller problem
}
console.log(factorial(3)); // 6function 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)
Чиста helper функция, която трябва да върне променен array, често първо го копира със синтаксиса spread [...arr] (лекция 4). Копието е плитко (shallow): нов е само външният array, вътрешните arrays все още са общи:
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 ]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 го показва, заедно с пълно (дълбоко) копие.
Файлът с правило има фиксирана форма. Указанията за правилата обикновено казват:
rule."const, а не var / let, и вашите функции не трябва да ги променят."function rule(doc) { - "никога не променяйте този ред": engine-ът търси точно него.Затова файлът с правило се чете отгоре надолу:
/*
* 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" | равни (тук суфиксът се пренебрегва) |
| число вместо string | 2.14 срещу "2.14" | равни |
| липсваща стойност | undefined срещу "1.0" | третира се като "", т.е. 0 - по-ниска |
Много правила пишат една helper функция, която отговаря на "по-ниска, равна или по-висока?" с -1, 0 или 1 (в съществуващите правила често се казва cmpVer). Въпросите с да / не тогава са по един ред:
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)}`);
}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 функция:
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));
}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 функция може да се провери самостоятелно.
Когато правилото стане дълго, потърсете частите, които отговарят на един въпрос или дават една стойност, и дайте име на всяка:
doc, подайте doc (или само частта, която ѝ трябва: doc.inventory.enclosures).deviceName(doc), firmwareVersion(doc), enclosureProblems(list); въпрос с да / не започва с is / has: isSupported(model).console.log (или с check във файловете за упражнения), включително странните случаи: липсваща стойност, празна, грешен тип.Правилото в началото на тази лекция е резултатът от този процес, приложен върху правилото от лекция 4.
Ето правило в стила на много съществуващи правила - helper функции, вложени в rule, var, callbacks, написани като function (x) { ... }, и try / catch около всичко. Първо го изпълнете:
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)");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)");Проверете се:
cmpVer? Може ли друго правило да я използва?2.10 - под 2.14. Защо няма alert за LAB-DEV-05?getLowestFirmware от какво пази list[i] || {}? А if (e.state && e.state !== "normal") continue; - кои enclosures пропуска и кои запазва?cmpVer променя параметрите си a и b (a = ... String(a)). Променя ли това нещо за извикващия? Чиста ли е cmpVer?rule (глобална - engine-ът я извиква), cmpVer и getLowestFirmware (локални за rule - само rule и функциите в нея могат да ги извикат) и два анонимни callbacks, подадени на map.cmpVer при всяко извикване на rule. Това струва малко, но никое друго правило не може да я използва: всяко правило има свое копие (и свои бъгове).extended_info, затова doc.inventory.extended_info.json хвърля TypeError. catch го хваща и връща тихо. Истински проблем е пропуснат и никой не вижда грешка. (Занятие 9 е за грешките; поправката тук е ?. и резервно име.)list[i] || {}: от елемент, който е null или undefined - e.type би хвърлило грешка. Редът със state пропуска enclosures, чието състояние е зададено и не е "normal"; запазва тези без състояние (undefined е falsy, затова continue не се изпълнява).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 ?? "").
Резултатът:
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)");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 по-горе).
Проверете се:
if (lowest !== undefined && cmpVer(lowest, expected) < 0) { ... } може да се напише и като guard clause: if (lowest === undefined || cmpVer(lowest, expected) >= 0) return;, а кодът за alert - след него. Защо това е същото условие, обърнато? (Лекция 3: De Morgan.)expected е фиксирана настройка на правилото. Къде бихте я сложили и как бихте я декларирали?lowest !== undefined е lowest === undefined, а обратното на < 0 е >= 0. Guard clause-ът спира рано, затова кодът за alert не е вложен в if.cmpVer и getLowestFirmware - те само четат аргументите си и връщат стойност. rule не е: извиква emit (това е работата ѝ).rule: const EXPECTED_FIRMWARE = "2.14";. Никога не се променя, затова е const, както искат указанията (раздел 7). Горе лесно се намира и обновява, когато се препоръча ново ниво, без да четете кода на rule.Във вашето копие на Homework repository първо вземете новите задачи:
git pull --no-rebase --no-edit upstream main
node 05-functions/practice/task1-predict.jsВсеки файл е разделен на части. Всяка част има описание, след него редовете your code - пишете кода си между тях - и веднага след тях нейните проверки, които отпечатват OK или FAIL.
| Задача | Файл | Какво правите |
|---|---|---|
| 1 | task1-predict.js | Предсказвате 20 резултата: scope, shadowing, стойности по подразбиране, return, hoisting, arrow functions |
| 2 | task2-versions.js | compareVersions, versionBelow и versionStatus - с всички странни случаи |
| 3 | task3-helpers.js | Малки чисти helper функции: asList, capitalize, lastPart, deviceName със стойност по подразбиране |
| 4 | task4-callbacks.js | customMap, customFilter, countIf, compose - и използването им с arrow functions върху enclosures |
| 5 | task5-classic-functions.js | Класически задачи като функции: лице, суми, най-голямо четно число, най-добър резултат, таблица |
| Тема | Запомнете |
|---|---|
| Функция | Именувана поредица от действия; дефинира се веднъж, извиква се много пъти; f е функцията, f() я извиква |
| Declaration / expression | function 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, а не обратното |
var | Scope на функцията, може да се декларира отново, издига се като undefined - четете го, пишете let / const |
| Hoisting | Декларациите са известни от началото на своя scope; function declarations могат да се извикат по-рано |
| Аргументи | Примитивите се копират; arrays и обектите са общи - не променяйте doc |
| Чиста функция | Резултатът зависи само от аргументите; не променя нищо отвън |
| Callback | Функция, подадена на функция: countIf(list, enc => ...); f, а не f() |
| Arrow function | x => expr връща expr; с { } ви трябва return; ({ ... }) за обект |
| Closure | Вътрешната функция пази живи variables на външната |
| Файл с правило | Коментар в началото, настройки с const, чисти helper функции, после function rule(doc) |
| Стар стил | Helper функции в rule, var, function (x) { return ...; }, try / catch, който хваща всичко - четете го, модернизирайте стъпка по стъпка |
05-functions/homework-small-functions.js (в homework repository): 20 малки функции, от square(x) до версия, разделена на числа. Всяка функция пишете сами, от думата function до return. Използват само раздели 1 - 7 от тази лекция и предишните лекции.05-functions/homework.js (в homework repository): най-високата версия на модул в отчет, фабрика за функции за проверка на версии и по-старо правило, разделено на helper функции - със същите alerts.OK.Следващото занятие: обекти. Дълбоки пътища и ключове с точки, речници с ключове id, едни и същи данни като array в един отчет и като обект в друг - и първата ви версия на get(doc, path, fallback) за курсовия проект.