JavaScript · lesson 5 of 24
Beginner
11- 1 Introduction to JavaScript
- 2 Setting Up JavaScript Development Environment
- 3 Variables in JavaScript: var, let, and const
- 4 JavaScript Data Types
- 5 JavaScript Operators
- 6 Control Flow in JavaScript
- 7 Functions in JavaScript
- 8 Working with Strings in JavaScript
- 9 Arrays in JavaScript
- 10 Objects in JavaScript
- 18 DOM Manipulation in JavaScript
Intermediate
10- 11 Error Handling in JavaScript
- 12 Object-Oriented Programming in JavaScript
- 13 Prototypes and Prototype Chain in JavaScript
- 14 Closures in JavaScript
- 15 JavaScript Event Loop Explained
- 16 Asynchronous JavaScript: Callbacks, Promises, and Async/Await
- 17 JavaScript Modules: ESM and CommonJS
- 19 Browser Web APIs in JavaScript
- 20 Testing JavaScript with Jest and Vitest
- 24 JavaScript Interview Preparation: Top 35 Questions
Advanced
3JavaScript Operators
Master JavaScript operators — arithmetic, comparison, logical, nullish coalescing, optional chaining, spread, and ternary — with real-world examples.
By SkillByExample · Updated
Operators combine values into new ones. Most behave as they do in any language; the interesting parts of JavaScript’s are the ones that convert types on the way — +, == — and the ones that return values rather than plain booleans — &&, ||, ??. Every example is a complete file run with node.
Arithmetic
The usual operators, plus ** for powers and % for the remainder.
console.log(10 + 3, 10 - 3, 10 * 3, 10 / 3);
console.log(10 % 3, -10 % 3, 10 ** 3, 2 ** 0.5);
console.log(7 / 0, -7 / 0, 0 / 0);
console.log(0.1 + 0.2, 0.1 + 0.2 === 0.3);
let x = 5;
console.log(x++, x);
console.log(++x, x);
$ node arith.js
13 7 30 3.3333333333333335
1 -1 1000 1.4142135623730951
Infinity -Infinity NaN
0.30000000000000004 false
5 6
7 7
Division always produces a floating-point number — there is no integer division (use Math.trunc(10 / 3)). % takes the sign of the left operand, so -10 % 3 is -1, not 2. Dividing by zero gives Infinity, and 0 / 0 gives NaN — no exceptions. 0.1 + 0.2 shows binary floating point: compare decimals with a tolerance, or work in integers. x++ returns the old value, then increments; ++x increments, then returns.
+ Also Concatenates
If either operand of + is a string, the other is converted and the result is a string. It’s evaluated left to right.
console.log("5" + 3, typeof ("5" + 3));
console.log(5 + 3 + "px");
console.log("px" + 5 + 3);
console.log("5" - 3, "5" * "2", "abc" * 2);
console.log(+"42", +"", +"4 2", Number("42px"), parseInt("42px"));
$ node plus.js
53 string
8px
px53
2 10 NaN
42 0 NaN NaN 42
5 + 3 + "px" adds first (8) and then concatenates; "px" + 5 + 3 becomes a string at the first step, so the 3 is glued on too. Only + concatenates — -, * and / always convert to numbers, which is why "5" - 3 is 2. Unary + is a compact Number(); note that +"" is 0, while parseInt reads leading digits and stops, so "42px" gives 42 where Number gives NaN.
Assignment and Logical Assignment
Compound operators update a variable in place; ??=, ||= and &&= assign only when a condition holds.
let n = 10;
n += 5; n -= 3; n *= 2;
console.log(n);
n /= 4; n **= 2; n %= 10;
console.log(n);
const settings = { retries: 0, theme: "", name: null };
settings.retries ||= 3;
settings.theme ??= "dark";
settings.name ??= "Anonymous";
console.log(settings);
let token = "abc";
token &&= token.toUpperCase();
let missing = null;
missing &&= missing.toUpperCase();
console.log(token, missing);
$ node assign.js
24
6
{ retries: 3, theme: '', name: 'Anonymous' }
ABC null
||= treated the legitimate 0 retries as “missing” and overwrote it with 3 — the same trap as || below. ??= only fills in null/undefined, so the empty-string theme survived and the null name was replaced. &&= is the mirror image: it only assigns when the current value is truthy, which makes it a safe “transform if present”.
=== vs ==
=== compares without converting types. == converts first, following rules few people remember.
const pairs = [
[1, "1"], [0, false], [0, ""], ["", false], ["0", false],
[null, undefined], [null, 0], [NaN, NaN], [[1], "1"],
];
for (const [a, b] of pairs) {
const show = (v) => (typeof v === "string" ? JSON.stringify(v) : Array.isArray(v) ? `[${v}]` : String(v));
console.log(`${show(a)} vs ${show(b)}`.padEnd(22), "== ", String(a == b).padEnd(5), "=== ", a === b);
}
console.log(Object.is(NaN, NaN), Number.isNaN(NaN));
$ node equality.js
1 vs "1" == true === false
0 vs false == true === false
0 vs "" == true === false
"" vs false == true === false
"0" vs false == true === false
null vs undefined == true === false
null vs 0 == false === false
NaN vs NaN == false === false
[1] vs "1" == true === false
true true
0 == "" and "0" == false are both true, yet "" == "0" would be false — loose equality isn’t even transitive. NaN isn’t equal to anything, itself included, under either operator; test it with Number.isNaN or Object.is. The rule: always use ===. The one widely accepted exception is value == null, which is true for exactly null and undefined — and nothing else, as null == 0 shows.
Relational Operators and Strings
<, >, <=, >= compare numbers numerically — but two strings are compared character by character.
console.log(10 > 9, "10" > "9", "10" > 9);
console.log("banana" > "apple", "Zebra" < "apple");
console.log(["10", "9", "1", "100"].sort());
console.log(["10", "9", "1", "100"].sort((a, b) => a - b));
console.log(["Zebra", "apple", "Äpfel"].sort((a, b) => a.localeCompare(b)));
$ node relational.js
true false true
true true
[ '1', '10', '100', '9' ]
[ '1', '9', '10', '100' ]
[ 'Äpfel', 'apple', 'Zebra' ]
"10" > "9" is false because "1" sorts before "9"; as soon as one side is a number ("10" > 9) both are compared as numbers. Uppercase letters come before all lowercase ones in Unicode, so "Zebra" < "apple". The default sort() compares as strings too — hence ["1", "10", "100", "9"]. Pass a comparator: a - b for numbers, localeCompare for human-sorted text.
&& and || Return Values, Not Booleans
&& returns the first falsy operand (or the last one); || returns the first truthy operand (or the last one). Evaluation stops as soon as the answer is known.
function expensive(label) {
console.log(` expensive(${label}) ran`);
return label;
}
console.log("hello" && 42);
console.log(null && expensive("A"));
console.log(0 || "fallback");
console.log("exists" || expensive("B"));
console.log(0 || "" || null);
console.log(!!"hello", !!0, !![], !!{}, !!"0", !!NaN);
$ node logical.js
42
null
fallback
exists
null
true false true true true false
expensive never ran: null && … is already falsy and "exists" || … already truthy, so the right side was skipped — short-circuiting. That’s what makes user && user.save() a null guard. When nothing is truthy, || returns the last operand (null), not false. !! converts to a real boolean; note that [], {} and "0" are all truthy.
?? vs ||
?? (nullish coalescing) falls back only on null or undefined. || falls back on every falsy value.
const inputs = [0, "", false, null, undefined, "Alice"];
for (const value of inputs) {
console.log(
String(JSON.stringify(value)).padEnd(10),
"|| ->", String(JSON.stringify(value || "default")).padEnd(10),
"?? ->", JSON.stringify(value ?? "default"),
);
}
const userScore = 0;
console.log(`Score: ${userScore || "No score"}`);
console.log(`Score: ${userScore ?? "No score"}`);
$ node nullish.js
0 || -> "default" ?? -> 0
"" || -> "default" ?? -> ""
false || -> "default" ?? -> false
null || -> "default" ?? -> "default"
undefined || -> "default" ?? -> "default"
"Alice" || -> "Alice" ?? -> "Alice"
Score: No score
Score: 0
For 0, "" and false, || discards a real value; ?? keeps it. Use ?? for defaults whenever a falsy value could be valid — a score of 0, an empty name, a disabled flag. (JSON.stringify(undefined) is itself undefined, which is why that row prints without quotes.)
Optional Chaining (?.)
?. stops and returns undefined if the value to its left is null or undefined, instead of throwing.
const user = { name: "Alice", address: { city: "Tokyo" }, greet() { return "hi"; } };
const guest = null;
console.log(user?.address?.city);
console.log(user.address?.zip);
console.log(user.contacts?.[0]?.email);
console.log(user.greet?.(), user.wave?.());
console.log(guest?.name, guest?.address.city);
console.log(user.contacts?.[0]?.email ?? "no email on file");
try { console.log(user.contacts[0].email); }
catch (err) { console.log(err.message); }
$ node optional.js
Tokyo
undefined
undefined
hi undefined
undefined undefined
no email on file
Cannot read properties of undefined (reading '0')
?.[…] and ?.() guard index access and calls. guest?.address.city is also safe: once ?. short-circuits, the entire rest of the chain is skipped. Paired with ??, it reads a deeply nested value with a default in one line. Without it, the first missing level throws — the most common TypeError in JavaScript.
Spread and Rest
... spreads an iterable into places that take several values, and — in a parameter list — collects extra arguments into an array.
const a = [1, 2, 3];
const b = [4, 5, 6];
console.log([...a, ...b], [..."hey"]);
const numbers = [5, 1, 8, 3];
console.log(Math.max(...numbers), Math.max(numbers));
const config = { ...{ theme: "dark", lang: "en" }, ...{ theme: "light" } };
console.log(config);
function sum(first, ...rest) {
console.log(`first=${first} rest=${JSON.stringify(rest)}`);
return rest.reduce((acc, n) => acc + n, first);
}
console.log(sum(1, 2, 3, 4));
$ node spread.js
[ 1, 2, 3, 4, 5, 6 ] [ 'h', 'e', 'y' ]
8 NaN
{ theme: 'light', lang: 'en' }
first=1 rest=[2,3,4]
10
A string is iterable, so spreading it gives characters. Math.max(numbers) without spread tries to convert the whole array to one number and gets NaN. In objects, later keys win. The same three dots in a parameter list do the reverse — rest is a real array.
Ternary Operator
condition ? a : b is an expression, so it works where statements can’t — inside template literals, arguments and JSX.
for (const count of [0, 1, 5]) {
console.log(`You have ${count} ${count === 1 ? "message" : "messages"}`);
}
const grade = (score) => (score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F");
console.log([95, 85, 72, 40].map(grade).join(" "));
$ node ternary.js
You have 0 messages
You have 1 message
You have 5 messages
A B C F
A chain of ternaries works (they associate to the right), and formatted one-per-line it reads like a lookup table. Beyond that, prefer if/else or an object lookup — nested ternaries inside branches are where readability goes.
Precedence
When operators mix, precedence decides grouping. Parentheses make it explicit.
console.log(2 + 3 * 4, (2 + 3) * 4);
console.log(true || false && false, (true || false) && false);
console.log(2 ** 3 ** 2, (2 ** 3) ** 2);
for (const src of ['null ?? "a" || "b"', "-2 ** 2"]) {
try { new Function(`return ${src}`); }
catch (err) { console.log(`${src.padEnd(20)} -> ${err.name}: ${err.message}`); }
}
console.log((null ?? "a") || "b", (-2) ** 2);
$ node precedence.js
14 20
true false
512 64
null ?? "a" || "b" -> SyntaxError: Unexpected token '||'
-2 ** 2 -> SyntaxError: Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence
a 4
&& binds tighter than ||, and ** groups right to left (2 ** 9 = 512). Two combinations are deliberately syntax errors because they’re ambiguous: mixing ?? with ||/&& without parentheses, and a unary minus directly before **. The code is compiled with new Function here only so the error can be caught and printed; in a normal file it would stop the script from running at all.
Practice
1. Write isBlank(value) that is true for null, undefined and "" but false for 0 and false.
const isBlank = (value) => value == null || value === "";
for (const v of [null, undefined, "", 0, false, "x"]) {
console.log(JSON.stringify(v) ?? "undefined", isBlank(v));
}
$ node p1.js
null true
undefined true
"" true
0 false
false false
"x" false
value == null is the one idiomatic use of ==: it covers both null and undefined.
2. Read order.customer.address.postcode from three orders, one with no customer and one with no address, defaulting to "n/a".
const orders = [
{ id: 1, customer: { address: { postcode: "SW1A 1AA" } } },
{ id: 2 },
{ id: 3, customer: {} },
];
for (const order of orders) {
console.log(order.id, order.customer?.address?.postcode ?? "n/a");
}
$ node p2.js
1 SW1A 1AA
2 n/a
3 n/a
3. Sort the prices ["19.99", "5", "100", "9.5"] from lowest to highest, and show what plain sort() does.
const prices = ["19.99", "5", "100", "9.5"];
console.log([...prices].sort());
console.log([...prices].sort((a, b) => a - b));
console.log([...prices].map(Number).sort((a, b) => a - b));
$ node p3.js
[ '100', '19.99', '5', '9.5' ]
[ '5', '9.5', '19.99', '100' ]
[ 5, 9.5, 19.99, 100 ]
a - b converts the strings to numbers for the comparison only; mapping to Number first gives you numbers in the result as well.
4. Write average(...nums) using rest parameters that returns 0 when called with nothing.
const average = (...nums) => (nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0);
console.log(average(2, 4, 9));
console.log(average());
console.log(average(...[10, 20]));
$ node p4.js
5
0
15
Without the nums.length check, average() would be 0 / 0, which is NaN.