Skip to main content
PProfileMatch

JavaScript Variables, Data Types and Functions Explained

ProgrammingLesson 1 of 45 min read

What you'll learn

  • Choose between const, let and var and explain block scope and hoisting
  • Name JavaScript's primitive types and predict common coercion results
  • Write function declarations, arrow functions and functions with default parameters
  • Explain what a closure is and use one to keep state private

On this page

  1. let, const and var: three ways to name a value
  2. Primitive types and the coercion that surprises people
  3. Functions, closures and scope
  4. Common mistakes and the errors they produce
  5. How this shows up in real projects and interviews

Take a snack vending machine. A customer inserts 200 cents, the snack costs 175, and the machine must work out whether to vend and how much change to return. Two things go wrong in the first draft of almost every version of this program: the amounts are stored as strings because they came from a form, so "100" + "100" becomes "100100", and the price is stored as 1.75, so arithmetic produces numbers with tails like 0.30000000000000004. Both are questions about types, and that is where we start.

let, const and var: three ways to name a value

Use const by default. It stops you from rebinding the name, which makes code easier to follow because a const always means the same thing in its scope. Use let for values that genuinely change, such as a running total. Avoid var in new code: it ignores block boundaries and is initialised as undefined before its line runs, which hides mistakes.

vending.js
// vending.js - one snack machine
const MACHINE_ID = "VM-204";
let coinsInserted = 0;           // changes as coins arrive
const priceCents = 175;

console.log(typeof MACHINE_ID, typeof coinsInserted, typeof true, typeof undefined, typeof null);
console.log(typeof {}, typeof [], typeof (() => 1), typeof 10n, typeof Symbol("s"));

coinsInserted += 100;
coinsInserted += 100;
console.log(`Inserted ${coinsInserted} cents, price ${priceCents}`);
console.log(`Change: ${coinsInserted - priceCents} cents`);

try {
  MACHINE_ID = "VM-999";
} catch (err) {
  console.log(err.name + ": " + err.message);
}

Output

string number boolean undefined object
object object function bigint symbol
Inserted 200 cents, price 175
Change: 25 cents
TypeError: Assignment to constant variable.

Two of those typeof results are famous quirks. typeof null is "object", a historical mistake that cannot be fixed without breaking the web, so test for null with value === null. And arrays report "object", so use Array.isArray(value). Note that const stops reassignment only: a const array or object can still be mutated, which the next lesson covers.

Primitive types and the coercion that surprises people

TypeExampleNotes
number175, 1.75, NaNOne 64-bit floating-point type for integers and decimals
string"VM-204"Immutable text; template literals use backticks
booleantrue, falseResult of comparisons
undefinedundefinedA variable that has not been given a value
nullnullA deliberate "no value" that you assign
bigint10nIntegers beyond the safe range of number
symbolSymbol("id")Unique keys; rarely needed by beginners

JavaScript converts between types automatically in some situations, which is called coercion. The + operator prefers text if either side is a string, while - and * always do arithmetic. The loose equality operator == coerces before comparing, which is why most style guides say to use strict equality ===, which never converts. The one deliberate exception many teams allow is value == null, which is true for both null and undefined.

coercion.js
console.log(0.1 + 0.2);
console.log("5" + 3, "5" - 3, "5" * "2");
console.log(1 == "1", 1 === "1", null == undefined, null === undefined);
console.log(Number("12abc"), Number(""), Number(" 42 "), parseInt("12abc", 10));
console.log(NaN === NaN, Number.isNaN(NaN), typeof NaN);

for (const v of [0, "", "0", [], {}, null, undefined]) {
  console.log(String(JSON.stringify(v)).padEnd(10), Boolean(v));
}

console.log(0 || 5, 0 ?? 5);          // || replaces any falsy value, ?? only null/undefined
console.log("" || "n/a", ("" ?? "n/a").length);

Output

0.30000000000000004
53 2 10
true false true false
NaN 0 42 12
false true number
0          false
""         false
"0"        true
[]         true
{}         true
null       false
undefined  false
5 0
n/a 0

Read the truthiness list carefully. The empty array and empty object are truthy, while 0, "", null, undefined and NaN are falsy. That is why qty || 10 is a bug when 0 is a legitimate quantity: it replaces the zero. The nullish operator ?? only replaces null and undefined, so qty ?? 10 keeps the zero. As for the money problem, the vending machine stores integer cents, which sidesteps floating-point drift entirely and converts to dollars only for display.

Functions, closures and scope

A function can be declared with the function keyword, or written as an arrow function and stored in a const. Declarations are hoisted, so you can call them above where they appear; const and let are hoisted too but sit in a "temporal dead zone" until their line runs, and touching them early throws a ReferenceError. Parameters can have defaults, which apply only when the argument is undefined.

dispense.js
const centsToText = (cents) => "$" + (cents / 100).toFixed(2);

function dispense(balance, price, stock = 5) {
  if (stock === 0) return { ok: false, reason: "sold out" };
  if (balance < price) return { ok: false, reason: `need ${centsToText(price - balance)} more` };
  return { ok: true, change: balance - price };
}

console.log(dispense(200, 175));
console.log(dispense(100, 175));
console.log(dispense(500, 175, 0));

function makeSlot(startStock) {
  let stock = startStock;            // hidden: only the two methods can reach it
  return {
    vend() {
      if (stock === 0) return false;
      stock -= 1;
      return true;
    },
    left: () => stock,
  };
}
const slot = makeSlot(2);
console.log(slot.vend(), slot.vend(), slot.vend(), slot.left());

console.log(label("early"));         // works: declarations are hoisted
function label(x) { return "slot " + x; }

try {
  console.log(total);
  let total = 1;
} catch (e) {
  console.log(e.name + ": " + e.message);
}

var handlers = [];
for (var i = 0; i < 3; i++) handlers.push(() => i);
console.log(handlers.map((h) => h()));
const handlers2 = [];
for (let j = 0; j < 3; j++) handlers2.push(() => j);
console.log(handlers2.map((h) => h()));

Output

{ ok: true, change: 25 }
{ ok: false, reason: 'need $0.75 more' }
{ ok: false, reason: 'sold out' }
true true false 0
slot early
ReferenceError: Cannot access 'total' before initialization
[ 3, 3, 3 ]
[ 0, 1, 2 ]

makeSlot shows a closure: the returned vend and left functions keep access to stock even after makeSlot has finished, and nothing else can read or change it. That is how JavaScript does private state without classes. The last two loops show why var is discouraged. A var loop variable is one shared variable, so by the time the callbacks run it is already 3. A let loop variable is created fresh per iteration, so each callback remembers its own value.

Common mistakes and the errors they produce

  • Reassigning a `const`. Throws TypeError: Assignment to constant variable. Use let if the value truly changes.
  • Using a variable before its `let`/`const` line. Throws ReferenceError: Cannot access 'total' before initialization.
  • Misspelling a variable. Reading an undeclared name throws ReferenceError: name is not defined, but assigning to one in sloppy mode silently creates a global. Add "use strict" or use modules, which are strict by default.
  • Forgetting `return`. The function gives back undefined, and a later line fails with TypeError: Cannot read properties of undefined.
  • Comparing with `==`. "0" == false is true. Use === unless you mean == null.
  • Doing arithmetic on form values. input.value is always a string, so "100" + "100" is "100100". Convert with Number() first and check Number.isNaN.

How this shows up in real projects and interviews

In real code, const and strict equality are enforced by linters, and money is almost always kept in the smallest unit as an integer. Closures are everywhere: React hooks, event handlers and module patterns all rely on them. Interviewers love the var in a loop question, typeof null, the output of [] + {}-style coercions, and "what is a closure", so be ready to answer with a concrete example like the vending slot rather than a definition. Next, the structures that hold real data: objects, arrays and array methods.

Try it yourself

Write makeWallet(startCents) that returns an object with add(cents), spend(cents) and balance() methods. spend must refuse (return false) if the amount is larger than the balance or not a positive integer, and the balance variable must not be reachable from outside. Test it with a few calls and print the results.

Show a hint

Keep let balance = startCents inside makeWallet and return an object whose methods use it. Use Number.isInteger(cents) && cents > 0 for validation.

Key takeaways

  • Default to const, use let when a value changes, and avoid var.
  • Use === and Number.isNaN; remember typeof null is "object" and arrays need Array.isArray.
  • ?? only replaces null and undefined, while || replaces every falsy value including 0.
  • Keep money in integer cents and format only for display.
  • A closure is a function that keeps access to the variables around where it was created.

Test yourself

A trick from this lesson's ride: + is glue when text is nearby; - is always maths Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

What is the difference between let, const and var in JavaScript?

const creates a binding that cannot be reassigned, let creates one that can, and both are scoped to the nearest block. var is scoped to the whole function, is initialised as undefined before its line runs, and is best avoided in new code.

What is the difference between == and === in JavaScript?

== converts the operands to a common type before comparing, so "1" == 1 is true. === compares without converting, so it is false when the types differ. Most teams use === everywhere because the conversion rules are easy to get wrong.

What is a closure in JavaScript with an example?

A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished. For example, a makeCounter function can return an increment function that keeps updating a private count variable that nothing else can access.