In a world with , chapter 3.1.

THE PATH · SESSION 11 OF 66 · UNIT 3 · ACT I

Mana Basics

Variables, Data Types, Operators, and Control Flow

45 min read5 diagrams4 side quests5 runnable3,397 words7 code blocks

At a glance

PACE
Session 11 of 66 · suggested week 4. Continue when you are ready.
UNIT
Unit 3 · My Cheat Code Turned Out to Be JavaScript
ACT
Act I · Foundation
BEFORE
2.5 The Character Sheet Forge
AFTER
3.2 Spell Crafting
ARTIFACT
npc-dialogue.js: eight player variables, a reputation ladder, a class switch, three ternaries and nothing but template literals
READ
45 min · 5 diagrams · 7 code blocks

The hook#

Everything you have built so far stands still.

That is not an insult. Your HTML gives a page its bones and your CSS gives it a face, and some of those faces were good. But nothing on those pages reacts. Click a button and the button does not know it was clicked. Type a name and the page does not know you are there. The page cannot count, cannot decide, cannot remember. It is a painting of a door.

JavaScript is the language that turns the painting into a door. It is the most widely used programming language on the planet, it runs in every browser ever shipped, and the same language runs servers, phones, games, robots, and the AI tools you have been hearing about. Learning it is as close to a cheat code as this world offers: one language, and the whole internet opens.

Before the cheat code, though, the mana bar. Every spell you cast from here on draws on four things: a place to keep a value, a sense of what kind of value it is, a way to combine values, and a way to choose between roads. Variables, types, operators, control flow. Fill those four and the next three sessions, functions, objects and the page itself, have something to run on.

You respawned in a world where the walls can think. Today you learn the first four words of the language they think in.

Open Chrome, press the key combination that opens DevTools (Cmd + Option + I on a Mac, F12 everywhere else), and click the Console tab. That blinking cursor is a JavaScript interpreter waiting for you. Type alert("I am a wizard now"), press Enter, and watch the browser obey. That is the whole session in miniature: you say something in the language, and the machine does it. Everything below with a Run button runs in this page, in the same engine, with the same results you would see in that console.

The idea#

There are four ideas today, and each one is small on its own. The skill is holding all four at once, so let us line them up.

A variable is a named container for a value. You write a name, you put something in it, and from then on you use the name. JavaScript gives you three words to declare one, and the choice between them is the first habit worth building.

Three vessels for a value: a green let jar and a yellow const flask drawn inside a dashed block, let refilled from 100 to 75 with a tick, const refused with a TypeError; a red var cauldron outside the block labelled function-scoped and hoisted, with an arrow reading leaks out
Fig 1 · Three ways to hold mana. let and const stay inside their block; var leaks out of the braces and is hoisted to undefined.

let is a jar with a lid. You can open it and change what is inside: let hp = 100 and later hp = 75. const is a sealed flask. Once filled, the binding cannot be pointed at anything else; try maxHp = 200 and JavaScript throws a TypeError at you, which is exactly what you want when a value was never meant to change. Both let and const are block-scoped: they live inside the nearest pair of curly braces and vanish when the block ends.

var is the cracked cauldron. It is the original 1995 keyword, it is function-scoped rather than block-scoped, so a var declared inside an if leaks out into the surrounding function, and it is hoisted in a way that reads as undefined instead of failing loudly. You will see it in older code and in older tutorials. Recognise it. Do not write it.

The rule, then: default to const, reach for let when the value must change, and never write var. Reading your own code back, every let is a flag that says "this one moves", which is useful information. One subtlety that trips people up: const seals the binding, not the contents. A const party = [] can still have members pushed into it; what you cannot do is point party at a different array.

Values come in seven primitive kinds, and everything else is an object. The seven are the atoms of the language.

Seven cards, string, number, boolean, undefined, null, symbol, bigint, each with an example value and its typeof answer in green; the null card is outlined in red because typeof null answers object, marked as a 1995 bug
Fig 2 · The seven primitives and what typeof says about each. Only null lies, and it has been lying since 1995.

A string is text in quotes or backticks. A number is any number, integer or decimal, in one type; there is no separate "int" and "float" to keep straight. A boolean is true or false, the two answers a question can have. undefined means a variable was declared and never given a value; it is what the language hands you for "nothing here yet". null means intentionally empty; it is what you hand the language when you mean "nothing here, on purpose". symbol makes a guaranteed-unique key, and bigint holds integers past the point where ordinary numbers lose precision. You will use the first five constantly and the last two rarely, but you should be able to name all seven.

The typeof operator tells you which kind a value is, and it is honest about six of them. For null it answers "object". That is a mistake in the original 1995 implementation; by the time anyone proposed fixing it, enough of the web depended on the wrong answer that fixing it would have broken more than it repaired. So it stays. Test for null with x === null, and when typeof says "object", remember it might mean "nothing".

Hoisting is two passes, not a move. Before JavaScript runs a single line of your file, it reads the whole thing once and registers every declaration it finds. Then it runs the lines top to bottom. People describe this as "declarations get moved to the top", and the picture is wrong in a way that will confuse you later. Nothing moves. There are two passes.

Two panels. Pass 1, registration: var ghost is set to undefined, let hero is locked in the temporal dead zone, function cast is ready. Pass 2, execution: console.log(ghost) prints undefined silently, console.log(hero) throws a ReferenceError, cast() works before its declaration line
Fig 3 · Hoisting is two passes. Declarations are registered first; then the lines run in order. var reads as undefined early, let and const throw.

In pass one, a var ghost is registered and filled with undefined, so reading ghost before its line silently gives you undefined. A let hero or const hero is registered but locked, in what the specification calls the temporal dead zone: touch it before its line and JavaScript throws a ReferenceError instead of quietly handing you nothing. A function cast() {} declaration is registered whole, body and all, which is why you can call a function above the place you wrote it. The practical rule is simply "declare before you use", and if you follow it, hoisting never bites. But when someone asks you what hoisting is, you now have a true answer: declarations are processed before execution; var reads as undefined early; let and const throw; function declarations are ready.

The loud error is the feature. The whole reason let and const exist is that silent undefined was a bug factory for twenty years.

Control flow is a road with gates. A program reads top to bottom unless you give it a reason to branch, and the reasons are if, else if, else, switch, and the one-line ? :. At every gate, JavaScript asks one question of the value in front of it: does this count as true? Six values do not: false, 0, "", null, undefined and NaN. These are the falsy six. Everything else counts as true, including some things that surprise people: the string "0", an empty array [], an empty object {}. Memorise the six and the surprises stop.

The demonstration#

Five files, in the order you would type them at the desk. Press Run on each and read the output against the code.

1. Containers, kinds, and the backtick#

Every primitive declared, checked with typeof, and used in a template literal.

JSmana.js
'use strict';// Variables: your mana containers.let playerName = 'Kirito';   // string, can change laterconst maxHP = 100;           // number, should never changelet isAlive = true;          // booleanlet inventory = null;        // null: intentionally emptylet questLog;                // undefined: declared, never given a value console.log(typeof playerName, typeof maxHP, typeof isAlive);console.log(typeof inventory, '<- the 1995 bug: null reports as "object"');console.log(typeof questLog);console.log(typeof 10n, typeof Symbol('id')); // let allows reassignment. const refuses, loudly.playerName = 'Asuna';try {  maxHP = 200;} catch (e) {  console.log('const refused:', e.constructor.name, '-', e.message);} // const seals the binding, not the contents of an object.const party = ['Asuna'];party.push('Klein');console.log(party); // Template literals: backticks, ${} for any expression, newlines allowed.const damage = 25;console.log('Hello, ' + playerName + '! You have ' + maxHP + ' HP.'); // the old wayconsole.log(`Hello, ${playerName}! You have ${maxHP} HP.`);          // the new wayconsole.log(`${playerName} took ${damage} damage! HP: ${maxHP - damage}`);console.log(`  Name:   ${playerName}  HP:     ${maxHP}  Status: ${isAlive ? 'Alive' : 'Dead'}`);console.log('ok: mana');

Three things to notice. The two greetings print the same text, and the second is the one you will write from now on: a template literal uses backticks, drops any expression into ${}, and can span lines without escape characters. The const refused line is the sealed flask doing its job; read the message once so you recognise it later. And party.push('Klein') succeeded on a const, because the flask holds a reference to the array, and the array itself is open.

2. Two passes, watched live#

The same file as fig. 3, so you can see the registration pass leave its fingerprints.

JShoisting.js
'use strict';// Pass 1 registered every declaration below before Pass 2 ran a single line.console.log('ghost before its line:', ghost);   // undefined, silentlyvar ghost = 'boo'; try {  console.log(hero);                            // the temporal dead zone} catch (e) {  console.log('hero before its line:', e.constructor.name, '-', e.message);}let hero = 'Aria'; console.log(cast(), '<- declared below, works anyway');function cast() { return 'fireball'; } // A block makes a new scope for let and const, not for var.if (true) {  let potion = 'minor healing';  var gold = 50;}console.log(typeof potion, '<- potion stayed inside the block');console.log(gold, '<- gold leaked out: var is function-scoped');console.log('ok: hoisting');

ghost reads as undefined with no complaint. hero throws, and the message says precisely what happened: "cannot access before initialization". cast() runs a line before the function is written. Then the block: potion is gone the moment the braces close, and gold is still there, because var never respected the braces in the first place. That last pair of lines is the whole case against var in four lines of output.

3. Operators, and what they hand back#

Arithmetic, the assignment shortcuts, comparison, and the logical operators. Predict each line before you run it.

JSoperators.js
'use strict';let hp = 100;const damage = 30;console.log('remaining', hp - damage);     // 70console.log('critical', damage * 2);       // 60console.log('even?', hp % 2 === 0);        // trueconsole.log('squared', damage ** 2);       // 900console.log('7 / 2 =', 7 / 2, '| 7 % 2 =', 7 % 2, '| 0.1 + 0.2 =', 0.1 + 0.2); let gold = 50;gold += 10;   // 60gold -= 5;    // 55gold *= 2;    // 110console.log('gold', gold); // Comparison. Strict means "same type AND same value".console.log(5 === 5, 5 === '5', 5 !== '5');console.log(3 > 2, 2 >= 2, 'b' > 'a'); // Logical operators, and what they really hand back.const hasKey = true;const doorOpen = false;console.log(hasKey && doorOpen, hasKey || doorOpen, !doorOpen);console.log('potion' || 'nothing', 0 || 'fallback', '' && 'never reached');console.log(null ?? 'default', 0 ?? 'default');console.log('ok: operators');

Two lines deserve a pause. 0.1 + 0.2 is not 0.3; it is 0.30000000000000004, because numbers are stored in binary and some decimals do not fit. Every language with floating-point numbers does this; it is not a JavaScript quirk, and the Numbers side quest shows you how to round and compare safely. And look at what || and && return: not true or false, but one of their operands. 0 || 'fallback' gives 'fallback'. That is why || is used for defaults, and why ??, which only steps in for null and undefined, is the safer default when 0 or "" are legitimate values.

4. The road, and the three ways to build a gate#

if / else if / else, truthy and falsy at the gate, switch with and without break, the ternary, and then all three in one function.

A flowchart of an if, else if, else chain for playerHP equal to 75: the first test, greater than 50, is taken along a solid green arrow to You are healthy; the other three tests are dimmed with dashed arrows; a side panel lists the six falsy values, false, 0, empty string, null, undefined and NaN
Fig 4 · The branching road. Only the first true test is taken, so order matters; the six falsy values decide what counts as true at each gate.

JSquest.js
'use strict';// 1. if / else if / else: tests run in order, the first true one wins.function hpReport(playerHP) {  if (playerHP > 50) return 'You are healthy!';  else if (playerHP > 20) return 'You are wounded. Find a potion!';  else if (playerHP > 0) return 'CRITICAL HP! Run away!';  else return 'Game Over.';}for (const hp of [75, 40, 5, 0]) console.log(hp, '->', hpReport(hp)); // 2. Truthy and falsy: if() asks "does this count as true?"const username = '';if (username) console.log(`Welcome, ${username}!`);else console.log('Please enter a username.  ("" is falsy)');const samples = [['0', 0], ['"0"', '0'], ['""', ''], ['[]', []], ['null', null], ['NaN', NaN]];for (const [label, v] of samples) console.log(label.padEnd(5), 'counts as', Boolean(v)); // 3. switch: one value, many cases. break or you fall through.function weaponBonus(weapon) {  switch (weapon) {    case 'sword': return 'Attack +10';    case 'bow': return 'Attack +7, Range +5';    case 'staff': return 'Attack +3, Magic +15';    default: return 'No weapon equipped.';  }}console.log(weaponBonus('sword'), '|', weaponBonus('staff'), '|', weaponBonus('spoon')); // The fall-through trap, on purpose this time: no break between cases.function rarityLabel(stars) {  let label = '';  switch (stars) {    case 3: label += 'LEGENDARY ';    case 2: label += 'RARE ';    case 1: label += 'COMMON';      break;    default: label = 'junk';  }  return label;}console.log('3 stars ->', rarityLabel(3), '| 1 star ->', rarityLabel(1)); // 4. Ternary: a one-line if/else that produces a value.const level = 25;const rank = level >= 50 ? 'Elite' : 'Novice';const currentHP = 0;console.log(rank, `| Player is ${currentHP > 0 ? 'alive' : 'dead'}`); // 5. The class selector, all three tools together.function describeClass(playerClass, level) {  const tier = level >= 30 ? 'veteran' : level >= 10 ? 'journeyman' : 'rookie';  let stats;  switch (playerClass) {    case 'warrior': stats = { hp: 150, atk: 20, mp: 0 }; break;    case 'mage': stats = { hp: 80, atk: 35, mp: 100 }; break;    case 'healer': stats = { hp: 90, atk: 15, mp: 120 }; break;    default: return `Unknown class "${playerClass}". Pick warrior, mage or healer.`;  }  return `${tier} ${playerClass}: HP ${stats.hp} · ATK ${stats.atk}${stats.mp ? ` · MP ${stats.mp}` : ''}`;}console.log(describeClass('mage', 35));console.log(describeClass('warrior', 3));console.log(describeClass('bard', 12));console.log('ok: quest');

Read the hpReport output against fig. 4. Seventy-five enters at the top, the first gate is true, and the other three gates are never consulted. That is why order matters: if you wrote the > 0 test first, a player with 75 HP would be told to run away. The for (const hp of [75, 40, 5, 0]) line that feeds it is a loop: it runs the statement after it once for each value in the list, with hp holding that value. Read for ... of as "for each of these" for now; loops get their proper introduction in 3.4. The samples loop is the falsy six and their look-alikes printed by the engine; "0" is a string with a character in it, so it counts as true, and that one bites people in form handling for years.

The rarityLabel function is the switch trap shown on purpose. With no break after a matching case, execution falls through into the next case and the next, which is why three stars collects all three words. Occasionally that is what you want. Usually it is a bug, and the fix is a break on every case. Finally, describeClass is the three tools together: a ternary for the tier because it produces a value, a switch for the class because it matches one value against several, and an early return in default so the bad input never reaches the template.

5. A character sheet, written the clean way#

Not a run block, because you are about to write this one yourself. This is the shape the first exercise asks for: declare everything, calculate, then print once with a template literal.

JS
const name = 'Kirito';const level = 12;const baseAttack = 40;const baseDefense = 25;const weapon = 'Elucidator';const weaponBonus = 15; const totalAttack = baseAttack + weaponBonus;const powerLevel = (totalAttack + baseDefense) * level;const isEvenLevel = level % 2 === 0; console.log(`=== CHARACTER SHEET ===Name:         ${name}Level:        ${level} (${isEvenLevel ? 'even' : 'odd'})Weapon:       ${weapon} (+${weaponBonus})Total Attack: ${totalAttack}Defense:      ${baseDefense}Power Level:  ${powerLevel}=======================`);

Every variable is a const, because nothing on a character sheet changes while you print it. The maths happens once, into named variables, so the template reads like a form instead of a formula. That habit, name the value then use the name, is worth more than any individual operator.

Where it breaks#

Your turn#

The quest#

The guild receptionist eyes you warily as you approach the counter. "State your name, class, and business," she says, pulling out a thick ledger. Depending on who you are and what you have done, this conversation could go very differently.

The NPC Dialogue System. Create npc-dialogue.js and give the receptionist a mind.

  1. Declare at least eight variables for the player: name, level, playerClass, gold, hp, maxHP, reputation, hasMagic. Pick const or let deliberately for each one.
  2. Build the reputation ladder. At 80 or above, she is friendly and offers a discount. From 40 to 79, she is neutral and businesslike. Below 40, she is hostile and charges double. If the player has no gold at all, she says something funny, whatever the reputation.
  3. Add a switch on playerClass that changes how she reacts: a mage gets offered scrolls, a warrior gets pointed at the bounty board, and so on, with a default for classes she has never heard of.
  4. Use at least three ternaries somewhere in the dialogue, for example the health remark (hp < maxHP / 2 ? 'You look terrible.' : 'You look well.').
  5. Every string the receptionist says is a template literal. No + between strings anywhere in the file.
  6. Leave at least three comments that explain a decision, not a line: why the ladder is ordered the way it is, not what an if does.

Hints that save an hour: declare every variable at the top before you write a single line of dialogue; build one branch, run it, change a value, run it again; and if a branch never fires no matter what you type, check whether an earlier test in the chain is catching it first, the way fig. 4 showed.

Stretch goals, in order of difficulty: a bartering rule using modulo (gold divisible by 7 earns a lucky discount); a second NPC with entirely different logic; a timeOfDay variable that changes her mood (cheerful in the morning, suspicious at night). Commit npc-dialogue.js and push it: git add -A, git commit -m "npc dialogue", git push -u origin main. The js-fundamentals repo collects the whole unit's work.

Side quests#

Four pages in the library go deeper than one session can. Read them in this order; each one is a short reference sheet with code that runs in the page.

Side quests from this session

  • Equality & coercion gotchasThe full table behind fig. 5: ===, == and Object.is, the falsy six, typeof, and every coercion rule as runnable asserts.
    Languages
  • Numbers & MathWhy 0.1 + 0.2 is not 0.3, how to round and format, parseInt versus Number, and the limits of the number type.
    Languages
  • StringsEverything the template literal did not cover: indexing, slicing, splitting, the immutability trap.
    Languages
  • Closures, scope & hoistingvar versus let versus const, the classic loop bug, and hoisting explained from scope up; read this one after Session 3.2 if it feels early.
    Languages

Before next session#

Three things, in this order.

First, finish the NPC Dialogue System if it did not get done in the session. It is the artifact for this session, and Session 3.2 assumes you can write a branching chain without looking one up.

Second, read the "What is JavaScript?" and "Variables" sections of MDN's JavaScript first steps. You know enough now that it will read like review, which is the point: the second pass is where it sticks.

Third, open the DevTools console and try five type-coercion experiments of your own. Write the prediction down before you press Enter. "3" * "4", [] + [], [] + {}, "b" + "a" + +"a" + "a", and whatever else occurs to you. When a prediction is wrong, find out why; that is the whole exercise.

Session 3.2 is called Spell Crafting. You will take the branches and calculations you wrote today and wrap them in functions, so that you write them once and cast them anywhere, and you will meet the array methods that turn a list of orcs into a list of the ones over level ten in a single line. The consts you chose today are about to become the arguments of the next session.

Say it once more without reading it. If it comes out in your own words, your mana bar is full.

See also