English
Appearance
English
Appearance
Today you will learn to:
return;let / const replaced var;function (x) { ... } and as x => ...;versionBelow(a, b);var, nested helpers, function (x) { return ...; } callbacks, try / catch - and modernise it step by step.Sections marked (advanced) or (optional)
This lecture has more than fits into one session. Sections marked (advanced) go deeper, and sections marked (optional) add one more example of a topic you already know. In class we may skip them - read them at home. The homework does not need them.
Where this fits in the sample check rule - today's rule is built from small helper functions. The number in each comment is the section of this lecture:
// 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 is short, and each step has a name. versionBelow is no longer a ready-made helper - we write it in section 12."Really understanding functions in JavaScript is the single most important weapon you can wield." - J. Resig, B. Bibeault, J. Maras, Secrets of the JavaScript Ninja, 2nd edition (Manning, 2016)
For rules this is literally true: a rule is a function, rule(doc). The engine calls it once for every report.
MDN: Functions (guide)
A function is a sequence of actions that does one task, kept together under a name. Think of a recipe: "pancakes" is the name; the steps are written once; you cook them any time you want. The result can depend on extra inputs: pancakes with milk, or pancakes with water.
In programming, a function is a small program inside the program. You define it once, and call it (run it) as many times as you need, with different inputs.
versionBelow(level, "4.2") reads better than ten lines of split and loops.Here is the problem functions solve. The same version check, copied three times:
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 trueThe answers happen to be right, but copy 2 reads a[1] instead of b[1] - the kind of bug copying creates. With a function, the logic is written once:
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"));Do not worry about the details yet - by the end of this lecture you can read every line, and in section 12 we make it robust.
Many languages separate two kinds of sub-programs:
y = f(x). You can use the result inside a bigger expression.In JavaScript, both are written with function. In a rule:
| Kind | Example | Used as |
|---|---|---|
| procedure | emit(name, details), console.log(...) | a statement on its own line |
| function | versionBelow(a, b), deviceName(doc) | a value: if (versionBelow(...)), const name = deviceName(doc); |
Good helpers are mostly of the second kind: they compute and return, and the rule decides what to do with the result.
MDN: function declaration
function <name>(<parameters>) {
<body>
}<name> - the name of the function. The same rules as for variable names (see identifiers on MDN). Use a verb or a question, because a function does something: getSerial, deviceName, isSupported, hasFeature, versionBelow.<parameters> - zero or more names, separated by commas. The round brackets are required even when there are no parameters.<body> - the statements between { and }.; is needed after the closing } of a declaration (one does no harm).// 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();Defining a function does not run it. The body runs only when the function is called (also: invoked, executed):
<name>(<arguments>);<arguments> - the values passed to the parameters in this call.( (it works, but it is easy to misread).greet is the function, greet() runs it.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 itAnimation
The call stack: 1. Calling a function - each call gets a new frame with its own variables.
console.log is a function too - a built-in one. You call it with an argument, like your own functions. And a function can call other functions:
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 = 13Animation
The call stack: 4. Nested calls - the stack grows on every call and shrinks on every return.
On the example JSON document, a helper takes the document as a parameter:
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-220The parameter is called doc here too, but it does not have to be: a parameter is a new name, valid only inside the function. More in section 6.
MDN: function expression
A function is also a value. You can create it in an expression and store it in a variable:
const <name> = function (<parameters>) {
<body>
};The function itself has no name here - it is an anonymous function. You reach it through the variable.
const greet = function (userName) {
console.log(`Hello ${userName}`);
};
greet("Ada");const greet = function (userName) {
console.log(`Hello ${userName}`);
};
greet("Ada");Two things happen in the line const greet = function ...: the variable greet is declared, and then a function is created and assigned to it - like const x = 5;. That is why a function expression ends with ;, like any other assignment.
A function expression can have a name of its own, but that name is visible only inside the function (useful for error messages and for a function that calls itself). Outside, use the 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) Leaving out the ; after a function expression usually does no harm, because JavaScript inserts it for you at the end of the line (automatic semicolon insertion). But not when the next line starts with ( or [. Then JavaScript joins the two lines. An example:
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 reads it as let foo = function () {...}[x, y] = [1, 2];: it sets a property on the function object, x and y are never set, and foo gets the array. A call foo() after it is TypeError: foo is not a function. Put ; after every assignment - also after a function expression - and the bug cannot happen.
Both work. In rules and helper files, the most common choice is:
function versionBelow(a, b) { ... }) - they read like a list of tools;The rule engine calls function rule(doc) - a declaration, and that line must stay as it is.
function sum(x, y).sum(2, 3).The first argument goes to the first parameter, the second to the second, and so on - by position, not by name:
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); // 5Parameters are local variables of the function. You do not declare them with let: the engine creates them on every call and gives them the values of the arguments. They exist only while the function runs.
JavaScript does not check the number of arguments. There is no error:
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 ignoredA variable that is not declared at all is still an error:
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);Animation
The call stack: 2. Arguments - a missing argument, and an extra one that bounces off.
Mind the order
Arguments are matched by position. versionBelow("4.2", level) asks the opposite question to versionBelow(level, "4.2"). Give parameters clear names (current, minimum), and check the order when you call a helper.
arguments and ...rest (advanced) MDN: The arguments object, Rest parameters
Sometimes a function should take any number of values. In older code you see the special arguments object. It exists in every function (but not in an arrow function) and holds all the arguments of the call:
// 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 looks like an array, but it is not one (Array.isArray(arguments) is false). Modern code writes a rest parameter ...name instead. It collects the remaining arguments into a real 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) is a built-in function of the same kind (session 8).
A function rectangleArea(width, height), used for the rooms of a flat:
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 30Check yourself: what does rectangleArea(3) return, and why? What does rectangleArea("3", "4") return?
rectangleArea(3) is NaN: height is undefined, and 3 * undefined is NaN. rectangleArea("3", "4") is 12 - the number 12: * converts both strings to numbers. (+ would not: it would join them.)
MDN: Default parameters
A default value is the value a parameter gets when no argument is passed for it. Since ES6 (2015) you write it in the parameter list:
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 3Older code uses || instead (lecture 3, || as a default):
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!The two forms are not the same:
undefined (missing);x || 1 replaces every falsy value: 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 1Note null: a default value is not used for null. In reports, a missing value can come as null. When null must also get the default, use ?? in the body: x = x ?? 1; (ES2020).
let x = x || 1 is an error
A common mistake is to write let x = x || 1; inside the function. The parameter x is already declared, so this is SyntaxError: Identifier 'x' has already been declared. Write x = x || 1; - or better, a default value.
Animation
The call stack: 5. Default values - f(0) and fOld(0) side by side.
A default value is calculated on every call, and it can use the parameters before it:
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: giving back a result MDN: return
Every function call is an expression - like 2 + 2, it gives a value. That value is decided by return:
return <expression>;const res = sum(2, 3); becomes const res = 5;.return ends the function at once. No statement after it runs.return (or with return; alone) returns 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()); // undefinedAnimation
The call stack: 3. return
return in a loop: stop searching return ends the function, also from inside a loop. That makes search helpers short - no break, no extra 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")); // undefinedThe last line return undefined; is optional - the function returns undefined anyway - but it shows the reader that "not found" was thought of.
In lecture 3 the rule stopped early with if (...) return;. Helpers use the same pattern. Check the bad cases first, and return; the main work comes last, without deep nesting:
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 lineA very common older pattern:
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"));The condition is already true or false. Return it:
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(Careful with && / || on non-boolean values: return name && name.trim(); returns a string or undefined, not true / false. That is fine when the caller uses it as a value; wrap it in Boolean(...) when you need a real boolean.)
return and a new line return must have its value on the same line. On a new line, JavaScript inserts a ; after return and the function returns 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")); // trueA long expression can continue on the next lines - as long as it starts on the return line. The same holds for an object: write return { with the brace on the return line.
A function returns one value. When you need more, return an array (or an object) that holds them:
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(Session 6 shows a short way to unpack it: const [major, minor, rest] = splitVersion(...).)
Check yourself: logEvenElements(arr) prints the even elements of an array, and findMaxEven(arr) returns the largest even element. Which one is a procedure and which a function? Write findMaxEven - what should it return for [1, 3, 5]?
logEvenElements is a procedure: it prints and returns undefined. findMaxEven is a function: it returns a value the caller can use.
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;
}For [1, 3, 5] it returns undefined - "there is no even element". Do not start with max = 0: for [-4, -2] the answer is -2, not 0. (Practice task 5.)
MDN: Scope
The scope of a variable is the part of the code where you can use it. There are three kinds:
{ and } of an if, a loop or a plain block, for let and const (since 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 definedParameters are local too. When the function ends, its local variables are gone; the next call creates new ones.
Animation
Variable scope: 1. Global and local
A variable declared outside every function is global: every function can read it, and every function can change it. In a browser page, all script files share one global scope - a global name in one file can clash with the same name in another file.
An example: a page loads two libraries, and both define a global with the same name. The second one wins:
// ---- 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 valueNode.js files (and the modules of session 9) each have their own scope, so this clash does not happen there. How the rule engine loads rules and libraries is not known to us - the safe habit is the same everywhere: few globals, clear names, and no function changes a global (more in section 7).
Animation
Variable scope: 6. IIFE (advanced) - the same clash, and the old fix (section 11).
A local variable with the same name as an outer one shadows (hides) the outer one inside the function. They are two different 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 changedAnimation
Variable scope: 3. Shadowing
A parameter is a local variable too, so it shadows in the same way. You will see this often in existing rules: a helper inside rule has its own parameter named doc, which hides the doc of 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);This is fine: inside getModel, doc is always the value of the call, so the helper works only with what it gets.
Check yourself: before you run it, write down the three lines it prints.
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. There are three different variables named x: the global one, the local one of foo, and the parameter of bar. Each function sees the nearest one.
If you assign to a name that was never declared (no let, const or var), JavaScript does not create a local variable. It looks outside, and:
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 fooBoth are bugs that are hard to find. Strict mode turns the second one into an error. Put "use strict"; as the first line of a file (or of a function):
"use strict";
function foo() {
leakedName2 = "Lovelace"; // ReferenceError in strict mode
}
foo();"use strict";
function foo() {
leakedName2 = "Lovelace"; // ReferenceError in strict mode
}
foo();The practice files of this course all start with "use strict";. Modules and classes are always strict. Whether the rule engine runs rules in strict mode is not known - so always declare your variables, and the question does not matter.
Animation
Variable scope: 4. No declaration
Functions can be written inside other functions. The inner function sees its own variables, then those of the outer function, then the globals - like looking for a name in your room, then in the flat, then in the building. This is the scope chain. An outer function can not see the variables of an inner one:
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();The chain is decided by where the function is written in the code, not by where it is called from (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, AdaAnimation
Variable scope: 2. The scope chain, 7. Lexical scope (advanced)
let, const and var Lecture 2 said "use const and let, not var". Here is why. Existing rules use var everywhere, so you need to know what it does.
let and const are visible only in the block where they are declared. var ignores blocks: it belongs to the whole function (or is global).
{
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 hereA let in a block is a new variable, even with the same name as an outer one:
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 = noneThat is the answer to lecture 3's question about for (var i ...): with var, i is still there after the loop; with let, it is not.
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); // undefinedAnimation
Variable scope: 5. Block scope: let and var
With var, declaring the same name again in the same scope is allowed - and silently reuses the variable. With let and const it is an error, which catches copy-paste bugs early:
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 creates a variable that cannot be assigned again. The error stops the program, so the last line does not run:
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 printedIt must get its value in the declaration: const GOLDEN_RATIO; is SyntaxError: Missing initializer in const declaration.
const does not make the value a real constant. When the value is an array or an object, you can still change what is inside - only the arrow from the name to the value is fixed (lecture 4, section 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.Animation
Variables in memory: 5. const
const globals that are never changed Rule guidelines often say: "global variables must be declared as const, and your functions must not modify them". The reason: the engine can load a rule once and call rule(doc) for many reports. A global that a rule changes keeps its value from the previous report:
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) - wrongThe fix: keep everything that belongs to one report local to rule (or to a helper), and use globals only for fixed settings:
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" }); // 1And remember const does not freeze an array: BAD_STATES.push(...) inside a function would still be a change. "Never changed" is a rule you follow.
MDN: Hoisting
Before JavaScript runs the code of a scope, it first goes through it once and registers all declarations in it. You can think of two passes:
So declarations are known from the start of their scope, wherever they are written. This is called hoisting ("lifting up"). What a name holds before its line depends on how it was declared:
| Declared with | Before its line |
|---|---|
var x = 10; | exists, value undefined |
function f() {...} | exists, the whole function - you can call it |
let / const | exists, but you may not touch it: ReferenceError (the temporal dead zone, TDZ) |
var f = function () {...}; | exists, value undefined - calling it is 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: 10A function declaration is hoisted with its body, so it can be called before its line:
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!";
}A function expression is a value in a variable. Only the variable is hoisted:
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!";
};With let or const the error is a ReferenceError instead: the variable is in the TDZ until its line has run.
Animation
Hoisting: the two phases of execution:1. var and undefined, 2. Function declaration, 3. Function expression, 4. let / const and the TDZ, 5. Hoisting in a function, 6. let in a block (advanced), 7. TDZ: time, not place (advanced)
What it means for rules:
rule - but the guidelines ask for helpers before rule. Follow them: the file reads top-down, and helpers written as const f = ... must come first anyway.var used before its line gives undefined silently. A let / const gives a clear error. One more reason to prefer let / const.Check yourself: what does this print, and why?
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, then local. Inside f there is a local var level, hoisted to the start of f. It shadows the global from the first line of f, while its value is still undefined. The global "global" is never seen in f. (The animation Hoisting - 5 shows it step by step.)
What does a function get when you pass it a value - the value itself, or a copy?
null, undefined) is copied into the parameter. Changing the parameter does not change the caller's 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' ] - changedAnimation
Functions and data: 1. A copy of the value, 2. The same array
There is a difference between changing the array the parameter points to and replacing the parameter with a new 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); // 0Animation
Functions and data: 3. Change or replace
doc is an object, so a helper that gets doc (or a part of it) can change the report itself - and every check after it sees the changed data:
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 objectA later versionBelow(enc.firmware, "2.14") now compares "2" - and gives a wrong answer. The fix: compute and return, do not write into the input:
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 - unchangedAnimation
Functions and data: 4. Changing the report
A pure function:
majorOf, versionBelow, deviceName are pure. shortFirmware is not (it changes its argument), addModel is not, and the first rule of section 7 is not (it changes a global).
Pure helpers are easy to read, to test and to reuse: to understand a call you only need to look at its arguments. Keep the side effects in one place - rule itself calls emit. Everything it calls should be pure.
// 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));When a helper must return a changed list, it builds a new one:
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 unchangedThe same arrows as in lecture 4
This is the "references" picture again: two names, one array. The animations Arrays in memory - 5 and Variables in memory - 6 show it without functions; a parameter is just one more name.
In JavaScript a function is a value, like a number or an array. You can:
Because of this, functions are called first-class citizens (first-class functions). This is the basis of almost every modern JavaScript library - and of the array methods in session 7.
Animation
Functions are values: 1. Like 5 and [1, 2, 3] - a number, an array and a function, side by side: the same steps work for all three, and only the function can be called.
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 nameAnimation
Functions are values: 2. A function is a value
A function without a name is an anonymous function. It is used where the function is needed only as a value: assigned to a variable, put into an array or an object, passed to another function, or returned.
// 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(); // BarWhat does foobar[2]() do? foobar[2] is undefined, and undefined() is TypeError: foobar[2] is not a function. The same error comes from a typo in a helper name inside an object.
Animation
Functions are values: 3. In arrays and objects
A real use: a table of checks, each one a small function. The rule runs them all:
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
A function passed as an argument to another function is called a callback: the other function "calls it back" when it needs it.
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);Note the difference between passing the function and passing its result:
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 functionIn the second call, callback() runs first (an argument is calculated before the call), prints 2 and returns undefined. caller gets undefined, and f() fails. Without () - the function; with () - its result.
Animation
Functions are values: 4. Callbacks
In lecture 4 you wrote the same loop many times: "count the enclosures that are not normal", "count the enclosures with old firmware"... Only the condition changed. With a callback, the loop is written once, and the condition comes in as a function:
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 does not know what it counts - the callback decides. It calls test(item) once per element and uses the returned true / false.
Animation
Functions and data: 5. A callback, call by call - every call of the callback gets its own frame.
Two classic exercises with callbacks. customMap builds a new array with the result of the callback for every element; customFilter keeps only the elements for which the callback returns 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 ]JavaScript arrays have these built in: arr.map(f) and arr.filter(f) - and forEach, find, some, every, reduce, sort... They all take a callback. Session 7 is about them; today you only need to read them.
function (x) { ... } Existing rules pass anonymous functions directly in the call:
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);
});Read it inside out:
function (x) { return parseInt(x, 10) || 0; } is an anonymous function: "take a part, return it as a number, or 0"..map(...) calls it for every part of the version, and collects the results in a new array..forEach(...) calls its function for every enclosure, and collects nothing.parseInt("7-B00", 10) is 7: parseInt reads digits from the start and stops at the first non-digit. The || 0 turns NaN (no digits at all) into 0.
A function can create a function and return it:
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();Animation
Functions are values: 5. A function returns a function
A useful example: compose(f, g) returns a new function that calls f, then g on its result:
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 added a shorter way to write a function expression: the arrow function (also called fat arrow; in other languages - Python, Java, C# - a lambda). It saves typing function, return and braces, which makes callbacks much shorter.
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.56The full form - parameters in brackets, =>, a body in braces:
(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)}`);One parameter: the brackets around it can be left out. With no parameters, or two or more, the brackets are required:
singleParam => { statements }
() => { statements }
(a, b) => { statements }One expression as the body: the braces and return can be left out - the value of the expression is returned:
(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());These two are not the same:
x => expression returns the value of the expression;x => { expression } is a body with one statement, and no return - it returns 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) = undefinedThis is the most common bug with arrow functions. If you add braces, add return.
Animation
Functions are values: 6. Arrow functions
{ after => always starts a body, so an object literal must be put in round brackets:
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])); // undefinedThis is where arrows shine. The older callback and the modern one, side by side:
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| Older | Modern |
|---|---|
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); } |
A long callback with several statements stays readable as an arrow with braces and return - or, better, becomes a named helper.
|| (advanced) An arrow function cannot stand directly after an operator like ||. Put it in brackets:
let cb;
cb = cb || function () {}; // ok
cb = cb || () => {}; // SyntaxError: Malformed arrow function parameter list
cb = cb || (() => {}); // okconst isHdd = enc => ... shows as isHdd).arguments - use a rest parameter: (...values) => ....new - they cannot be used as constructors.this of their own - inside an arrow, this is the this of the code around it. This matters for methods of objects, which come in session 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)The rule of thumb: arrows for callbacks and short helpers; function declarations for named helpers and methods.
rule Many existing rules define their helpers inside rule. First the idea on a small example: a function inside another function can use the variables of the outer function.
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 3What happens:
major is a local variable of checkFirmware. A new major function is created on every call of checkFirmware, and it disappears when checkFirmware returns.major uses version without getting it as a parameter: it finds it in the scope of checkFirmware, through the scope chain (section 6). In the first call it sees "4.2.1.7", in the second call "3.9.0.1".In a rule, the outer function is rule, and the helpers use doc in the same way:
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-220Animation
Functions and data: 6. A helper inside rule
Is this good or bad? It works. Its costs:
cmpVer in every rule);rule, so you must read all of rule to understand it;The guidelines ask for helpers before rule, at the top level. Then they get everything as parameters, and they are easy to move into a shared library later (session 9 and the course project).
MDN: Closures
An inner function keeps access to the variables of the outer function even after the outer function has returned. The function and the variables it remembers together are called a 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()); // 1Animation
Functions are values: 7. Closure (advanced)
A practical closure: a function factory. It takes a setting and returns a ready test with the setting built in:
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 ]Every call of above has its own limit: tooHot remembers 40, tooFull remembers 90. In the homework you write olderThan(minimum) - the same idea for versions.
The only way to run an anonymous function that is not stored anywhere is to call it right where it is written - an 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"))();The first pair of brackets turns the function into an expression (a statement that starts with function would be a declaration, and a declaration needs a name). The () at the end calls it.
Before modules existed, an IIFE was the way to keep variables out of the global scope: everything declared inside is local to the IIFE. Old libraries are written like this - the IIFE runs once and returns a function, and that function remembers the private variable (a 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 leakOften the IIFE returns several such functions together, in an object (objects: session 6).
Today, files that are modules (session 9) have their own scope, and the IIFE is rarely needed. You still meet it in older code.
Animation
Variable scope: 6. IIFE (advanced); Functions are values: 5. A function returns a function (the IIFE at the end)
A function can call itself - this is recursion. It needs a base case: a condition under which it stops. Each call gets its own frame:
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)); // 6Recursion fits nested data of unknown depth - for example, finding a key at any level of a report (session 6). Without a base case the stack overflows: RangeError: Maximum call stack size exceeded.
Animation
The call stack: 6. Recursion (advanced), 7. Stack overflow (advanced)
A pure helper that must return a changed array often copies it first with the spread syntax [...arr] (lecture 4). The copy is shallow: only the outer array is new, the inner arrays are still shared:
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 ]A copy of an object with { ...obj } is shallow in the same way - session 6 shows it, together with a full (deep) copy.
A rule file has a fixed shape. Rule guidelines usually say:
rule."const, not var / let, and your functions must not modify them."function rule(doc) { - "never change this line": the engine looks for it.So a rule file reads top-down:
/*
* 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 Lecture 3 showed why versions cannot be compared as strings ("4.10" < "4.2" is true). Now we write the helper. First, the requirements - what must it handle?
| Case | Example | Expected |
|---|---|---|
| part by part, as numbers | "4.10" vs "4.2" | 4.10 is higher |
| different number of parts | "4.2" vs "4.2.0.0" | equal |
| a suffix on a part | "4.2.1.7-B00" vs "4.2.1.7" | equal (the suffix is ignored here) |
| a number instead of a string | 2.14 vs "2.14" | equal |
| a missing value | undefined vs "1.0" | treated as "", i.e. 0 - lower |
Many rules write one helper that answers "lower, equal or higher?" with -1, 0 or 1 (in existing rules it is often called cmpVer). The yes / no questions are then one line each:
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) takes the two elements of each inner array - a short form of const a = pair[0], b = pair[1];. Session 6 explains it.)
The suffix (-B00), versions like 42107-C00 or XR420_117, and other formats come in session 8. With one shared helper, that fix will be made once - not in every rule.
Existing rules often turn a comparison into a status and an action text, with one small helper each:
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));
}Compare this with lecture 3, where the same logic was an if / else if chain and a switch inside the rule. Now each question has a name, and each helper can be checked alone.
When a rule gets long, look for the parts that answer one question or produce one value, and give each a name:
doc, pass doc (or just the part it needs: doc.inventory.enclosures).deviceName(doc), firmwareVersion(doc), enclosureProblems(list); a yes / no question starts with is / has: isSupported(model).console.log lines (or with check in the practice files), including the odd cases: missing, empty, wrong type.The rule at the start of this lecture is the result of this process applied to the rule of lecture 4.
Here is a rule in the style of many existing rules - helpers nested in rule, var, callbacks written as function (x) { ... }, and a try / catch around everything. Run it first:
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)");Check yourself:
cmpVer created? Can another rule use it?2.10 - below 2.14. Why is there no alert for LAB-DEV-05?getLowestFirmware, what does list[i] || {} protect against? And if (e.state && e.state !== "normal") continue; - which enclosures does it skip, and which does it keep?cmpVer changes its parameters a and b (a = ... String(a)). Does that change anything for the caller? Is cmpVer pure?rule (global - the engine calls it), cmpVer and getLowestFirmware (local to rule - only rule and the functions inside it can call them), and two anonymous callbacks passed to map.cmpVer on every call of rule. That costs little, but no other rule can use it: each rule has its own copy (and its own bugs).extended_info, so doc.inventory.extended_info.json throws a TypeError. The catch catches it and returns silently. A real problem is missed, and nobody sees an error. (Session 9 is about errors; the fix here is ?. and a fallback name.)list[i] || {}: an element that is null or undefined - e.type would throw. The state line skips enclosures whose state is set and is not "normal"; it keeps those without a state (undefined is falsy, so the continue does not run).a and b are strings (primitives), the parameters are local copies. cmpVer is pure - it only reads its arguments and returns a number.Change one thing at a time, and run the rule after each step - the alerts must stay the same (except for real bug fixes, which you make on purpose).
Step 1. Move the helpers out of rule. They already get everything as parameters, so they can move as they are. Now they are created once, and can be copied into a shared library.
Step 2. var -> const / let. const for everything that is not assigned again; let for loop counters and for values that change (lowest).
Step 3. Callbacks -> arrow functions, and simpler checks.function (x) { return parseInt(x, 10) || 0; } becomes x => parseInt(x, 10) || 0. (a === undefined || a === null) ? "" : String(a) becomes String(a ?? "").
The result:
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)");The alerts are the same as before. The second report still gets no alert: the try still hides the missing extended_info (question 3 above).
Check yourself:
if (lowest !== undefined && cmpVer(lowest, expected) < 0) { ... } can also be written as a guard clause: if (lowest === undefined || cmpVer(lowest, expected) >= 0) return;, and the alert code after it. Why is it the same condition, turned around? (Lecture 3: De Morgan.)expected is a fixed setting of the rule. Where would you put it, and how would you declare it?lowest !== undefined is lowest === undefined, and not < 0 is >= 0. The guard clause stops early, so the alert code is not nested in an if.cmpVer and getLowestFirmware - they only read their arguments and return a value. rule is not: it calls emit (that is its job).rule: const EXPECTED_FIRMWARE = "2.14";. It never changes, so const, as the guidelines ask (section 7). At the top it is easy to find and to update when a new level is recommended, without reading the code of rule.In your copy of the Homework repository, first get the new tasks:
git pull --no-rebase --no-edit upstream main
node 05-functions/practice/task1-predict.jsEach file is split into parts. A part has its description, then the your code lines - write your code between them - and right after them its checks, which print OK or FAIL.
| Task | File | What you do |
|---|---|---|
| 1 | task1-predict.js | Predict 20 results: scope, shadowing, defaults, return, hoisting, arrow functions |
| 2 | task2-versions.js | compareVersions, versionBelow and versionStatus - with all the odd cases |
| 3 | task3-helpers.js | Small pure helpers: asList, capitalize, lastPart, deviceName with a default |
| 4 | task4-callbacks.js | customMap, customFilter, countIf, compose - and use them with arrow functions on the enclosures |
| 5 | task5-classic-functions.js | Classic tasks as functions: area, sums, largest even number, best score, a table |
| Topic | Remember |
|---|---|
| Function | A named sequence of actions; defined once, called many times; f is the function, f() calls it |
| Declaration / expression | function f() {} (hoisted) / const f = function () {}; (ends with ;) |
| Parameters | Local variables, filled by position; missing -> undefined, extra -> ignored; ...rest for any number |
| Defaults | function f(x = 1) - only for undefined; x = x || 1 also replaces 0, "", false, null |
return | Ends the function and gives the value; no return -> undefined; keep the value on the same line |
| Scope | Global, function, block (let / const); inner functions see outer variables, not the other way |
var | Function-scoped, can be declared again, hoisted as undefined - read it, write let / const |
| Hoisting | Declarations are known from the start of their scope; function declarations can be called early |
| Arguments | Primitives are copied; arrays and objects are shared - do not change doc |
| Pure function | Result depends only on the arguments; changes nothing outside |
| Callback | A function passed to a function: countIf(list, enc => ...); f not f() |
| Arrow function | x => expr returns expr; with { } you need return; ({ ... }) for an object |
| Closure | An inner function keeps the variables of the outer one alive |
| Rule file | Header comment, const settings, pure helpers, then function rule(doc) |
| Old style | Helpers inside rule, var, function (x) { return ...; }, catch-all try / catch - read it, modernise step by step |
05-functions/homework-small-functions.js (in the homework repository): 20 small functions, from square(x) to a version split into numbers. You write each function yourself, from the word function to return. They use only sections 1 - 7 of this lecture and the earlier ones.05-functions/homework.js (in the homework repository): the best version of a module in a report, a function factory for version checks, and an older rule split into helpers - with the same alerts.OK.Next session: objects. Deep paths and keys with dots, dictionaries keyed by ids, the same data as an array in one report and as an object in another - and your first version of get(doc, path, fallback) for the course project.