Skip to main content
Published / updated

JavaScript Fundamentals

Before you start

You need: HTML and CSS (Articles 01–06). No prior JavaScript.

Time: about 50 minutes, plus the practice.

Learning objective

Write correct JavaScript and predict its behaviour around type coercion, scope and this.

Topics

  • let, const and why not var
  • Types and typeof
  • Truthiness and coercion
  • == versus ===
  • Control flow
  • Functions, arrow functions and this
  • Default, rest and spread
  • Scope, hoisting and closures
  • Modules
  • Strict mode

Variables

const schoolName = 'NexCoding Academy'; // cannot be reassigned
let studentCount = 400; // can be reassigned
var oldStyle = 'avoid'; // function-scoped, hoisted

Default to const. Use let only when the value genuinely changes. Never use var.

const prevents reassignment, not mutation:

const student = { name: 'Ravi Kumar' };
student.name = 'Ravi K'; // fine — the object is mutated
student = {}; // TypeError — reassignment

const marks = [87, 72];
marks.push(91); // fine

To freeze the contents:

const config = Object.freeze({ pageSize: 20 });
config.pageSize = 50; // silently ignored (throws in strict mode)

Why not var

// var is function-scoped and leaks out of blocks
if (true) {
var leaked = 'visible outside';
let contained = 'not visible outside';
}

console.log(leaked); // 'visible outside'
console.log(contained); // ReferenceError

The classic failure:

// var — all three log 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

// let — logs 0, 1, 2
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}

var creates one binding shared by every iteration; let creates a fresh binding each time. This exact bug appears whenever event handlers are attached in a loop.

Types

typeof 'Ravi Kumar' // 'string'
typeof 87 // 'number'
typeof 87n // 'bigint'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof null // 'object' <- a famous bug, kept for compatibility
typeof {} // 'object'
typeof [] // 'object'
typeof function () {} // 'function'

typeof null === 'object' is a bug from 1995 that cannot be fixed without breaking the web. Test for null explicitly:

if (value === null) { }
if (Array.isArray(value)) { }

null versus undefined

let notAssigned; // undefined — never given a value
const deliberatelyEmpty = null; // null — explicitly nothing

undefined means "no value yet"; null means "intentionally no value". An API returning null for a missing address is saying something different from a property that does not exist.

Numbers

0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false

Number.MAX_SAFE_INTEGER // 9007199254740991
parseInt('87.9') // 87
parseFloat('87.9') // 87.9
Number('87') // 87
Number('') // 0 <- surprising
Number('abc') // NaN
Number(null) // 0 <- surprising
Number(undefined) // NaN

JavaScript has one number type, a 64-bit float. Never do money arithmetic in floats:

// Wrong
const total = 0.1 + 0.2;

// Work in the smallest unit (paise), convert for display
const totalPaise = 10 + 20;
const totalRupees = (totalPaise / 100).toFixed(2);

NaN is not equal to itself:

NaN === NaN // false
Number.isNaN(value) // the correct test
isNaN('abc') // true — coerces first, avoid it
Number.isNaN('abc') // false — does not coerce, correct

Truthiness

Falsy values — everything else is truthy:

false, 0, -0, 0n, '', null, undefined, NaN
if ('0') { } // truthy — a non-empty string
if ([]) { } // truthy — an empty array
if ({}) { } // truthy — an empty object
if (0) { } // falsy
if ('') { } // falsy

The trap:

// Wrong: 0 marks is falsy, so a genuine zero is treated as missing
if (student.marks) {
display(student.marks);
}

// Correct
if (student.marks !== undefined && student.marks !== null) {
display(student.marks);
}

// Or, more concisely
if (student.marks != null) { // one of the two places == is correct
display(student.marks);
}

value != null is true for anything except null and undefined. It is the one idiomatic use of loose equality.

// ?? falls back only on null/undefined
const marks = student.marks ?? 0; // 0 marks stays 0

// || falls back on ANY falsy value
const wrong = student.marks || 0; // 0 marks becomes... 0, but so does '' and false

Use ??, not ||, for defaults whenever 0, '' or false are legitimate values. This is the modern version of the absent-versus-zero bug.

== versus ===

'87' == 87 // true — coerces
'87' === 87 // false — no coercion

0 == '' // true
0 == '0' // true
'' == '0' // false <- not transitive
null == undefined // true
null == 0 // false
[] == false // true
[] == ![] // true

Always use === and !==. The single exception is value != null to test for both null and undefined at once.

The '' == '0' being false while both equal 0 shows the rules are not transitive — which is why nobody memorises them and everybody uses ===.

Control flow

if (percentage >= 90) {
grade = 'A+';
} else if (percentage >= 80) {
grade = 'A';
} else {
grade = 'C';
}

switch (examType) {
case 'UnitTest':
case 'Assignment':
weight = 0.2;
break;
case 'Final':
weight = 0.5;
break;
default:
throw new Error(`Unknown exam type: ${examType}`);
}

switch uses strict comparison, so '87' does not match 87.

Missing break falls through to the next case. Deliberate fall-through — the stacked cases above — is fine; accidental fall-through is a silent bug. Always include default.

for (let index = 0; index < students.length; index++) { }

for (const student of students) { } // values — use this for arrays
for (const key in object) { } // keys — includes inherited ones

while (condition) { }
do { } while (condition);

for...in walks inherited enumerable properties too, so it is the wrong tool for arrays. Use for...of for values and Object.keys() for object keys.

for (const student of students) {
if (student.status !== 'Active') {
continue;
}

if (student.rollNumber === target) {
found = student;
break;
}
}

Functions

// Declaration — hoisted, callable before its definition
function calculatePercentage(marksObtained, maxMarks) {
if (maxMarks <= 0) {
throw new Error('maxMarks must be positive');
}

return (marksObtained / maxMarks) * 100;
}

// Expression — not hoisted
const calculateTotal = function (marks) {
return marks.reduce((sum, mark) => sum + mark, 0);
};

// Arrow function
const getGrade = (percentage) => {
if (percentage >= 90) { return 'A+'; }
if (percentage >= 80) { return 'A'; }
return 'C';
};

// Concise arrow — implicit return
const double = (n) => n * 2;
const makeStudent = (name) => ({ name }); // parentheses required for an object literal

Arrow functions and this

The real difference is not brevity.

const counter = {
count: 0,

incrementBroken: function () {
setTimeout(function () {
this.count++; // `this` is not `counter` here
}, 100);
},

incrementWorks: function () {
setTimeout(() => {
this.count++; // arrow inherits `this` from incrementWorks
}, 100);
}
};

A regular function gets its own this, decided by how it is called. An arrow function has no this of its own and uses the enclosing scope's — which is almost always what you want inside a callback.

The reverse trap:

const student = {
name: 'Ravi Kumar',

greetBroken: () => {
return `Hello, ${this.name}`; // `this` is not `student`
},

greetWorks() {
return `Hello, ${this.name}`; // shorthand method — works
}
};

Do not use an arrow function as an object method when it needs this. Use the method shorthand.

Arrow functions also have no arguments object and cannot be used with new.

Parameters

function search(term, pageSize = 20, page = 1) { }

function sum(...marks) { // rest — collects into an array
return marks.reduce((total, mark) => total + mark, 0);
}

sum(87, 72, 91);

const marks = [87, 72, 91];
Math.max(...marks); // spread — expands an array into arguments
// Destructured parameters with defaults — readable at the call site
function createStudent({ name, rollNumber, className, section = 'A' }) {
return { name, rollNumber, className, section };
}

createStudent({ name: 'Ravi Kumar', rollNumber: 'NCA-2024-0012', className: '10th' });

Named arguments make a call self-documenting, and adding a parameter does not break existing callers.

Default values are evaluated at call time, so function f(items = []) gets a fresh array per call — unlike some other languages.

Scope, hoisting and closures

console.log(hoistedFunction()); // works — declarations are fully hoisted
console.log(varVariable); // undefined — declaration hoisted, value not
console.log(letVariable); // ReferenceError — temporal dead zone

function hoistedFunction() { return 'ok'; }
var varVariable = 'value';
let letVariable = 'value';

let and const are hoisted but unreachable until the declaration runs — the "temporal dead zone". That produces a clear error instead of a silent undefined, which is why they are better.

Closures

A function keeps access to the scope it was created in, even after that scope has returned.

function createCounter() {
let count = 0;

return {
increment() { count++; return count; },
value() { return count; }
};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.value(); // 2 — `count` is private

Closures are how private state works in JavaScript, and how event handlers remember the data they were created with.

The classic bug, again:

// var — every handler sees the last student
for (var i = 0; i < students.length; i++) {
buttons[i].addEventListener('click', () => showStudent(students[i]));
}

// let — each handler closes over its own binding
for (let i = 0; i < students.length; i++) {
buttons[i].addEventListener('click', () => showStudent(students[i]));
}

Modules

// studentService.js
export const PAGE_SIZE = 20;

export function calculatePercentage(marksObtained, maxMarks) {
return (marksObtained / maxMarks) * 100;
}

export default class StudentService { }
// main.js
import StudentService, { calculatePercentage, PAGE_SIZE } from './studentService.js';
import * as students from './studentService.js';
<script type="module" src="/js/main.js"></script>

type="module" is required. Modules are deferred by default, are always in strict mode, and have their own top-level scope — no accidental globals.

The file extension is required in browser imports: './studentService.js', not './studentService'. Bundlers allow the shorter form, which is why code that works in a build fails when loaded directly.

Modules also need a server. Opening an HTML file with file:// fails with a CORS error — use a local dev server.

Strict mode

'use strict';

Modules and class bodies are strict automatically. In a plain script, opt in at the top of the file.

Strict mode turns silent failures into errors: assigning to an undeclared variable, writing to a frozen object, duplicate parameter names, and this being undefined rather than the global object in a plain function call.

There is no reason not to use it.

Errors you will hit

MessageCauseFix
Uncaught ReferenceError: x is not definedTypo, or used before declarationCheck spelling and scope
Uncaught TypeError: Cannot read properties of undefinedRead a property of something not there yetCheck the value first
"5" + 3 gives "53"+ concatenates when either side is a stringConvert with Number()
0.1 + 0.2 !== 0.3Floating pointRound, or work in whole paise
x == "1" is true for 1Loose equality coercesAlways use ===
A const object still changedconst fixes the binding, not the contentsExpected

Always use ===. == coerces types and produces results nobody predicts — 0 == "" is true.

Common mistakes

  • Using var
  • == instead of ===
  • if (marks) treating a genuine 0 as missing
  • || for defaults where 0 or '' are valid — use ??
  • Float arithmetic on money
  • isNaN instead of Number.isNaN
  • An arrow function as an object method needing this
  • A regular function as a callback, losing this
  • Missing break in a switch
  • No default in a switch
  • for...in over an array
  • Assuming typeof null is 'null'
  • Omitting the file extension in a browser module import
  • Expecting const to make an object immutable

Practice

  1. Write calculatePercentage, getGrade and calculateTotal for exam results. Handle maxMarks of zero.
  2. Run the var versus let setTimeout loop and explain both outputs.
  3. Write if (student.marks) where marks is 0. Confirm the bug, then fix it with != null.
  4. Compare student.marks ?? 0 and student.marks || 0 for marks of 0, null and undefined.
  5. Evaluate 0 == '', 0 == '0', '' == '0' and explain why the results are not transitive.
  6. Compute 0.1 + 0.2 and a fee total in floats. Then redo it in paise and compare.
  7. Write an object with an arrow-function method using this. Record what this is, then fix it with method shorthand.
  8. Write a setTimeout callback inside a method using a regular function. Fix it with an arrow.
  9. Write createCounter with a closure and confirm count is not reachable from outside.
  10. Attach click handlers in a loop with var, confirm they all show the last student, then fix it with let.
  11. Write a switch with a missing break and observe the fall-through.
  12. Split your code into two ES modules and load them with type="module". Omit the .js extension and record the error.

Exercises 3, 4 and 10 correspond to three bugs that reach production regularly.

You can now

  • Write correct JavaScript and predict its behaviour around coercion
  • Use === and say why == is dangerous
  • Choose const, let and never var
  • Read a console error and jump to the line
  • Explain why 0.1 + 0.2 is not 0.3

Review questions

  1. Why does var in a for loop with setTimeout log the same value every time?
  2. Why is ?? safer than || for default values?
  3. When should an arrow function not be used?
  4. What is the temporal dead zone, and why is it an improvement?

Next: Arrays and objects