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,constand why notvar- 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
| Message | Cause | Fix |
|---|---|---|
Uncaught ReferenceError: x is not defined | Typo, or used before declaration | Check spelling and scope |
Uncaught TypeError: Cannot read properties of undefined | Read a property of something not there yet | Check the value first |
"5" + 3 gives "53" | + concatenates when either side is a string | Convert with Number() |
0.1 + 0.2 !== 0.3 | Floating point | Round, or work in whole paise |
x == "1" is true for 1 | Loose equality coerces | Always use === |
A const object still changed | const fixes the binding, not the contents | Expected |
Always use ===. == coerces types and produces results nobody predicts — 0 == "" is true.
Common mistakes
- Using
var ==instead of===if (marks)treating a genuine0as missing||for defaults where0or''are valid — use??- Float arithmetic on money
isNaNinstead ofNumber.isNaN- An arrow function as an object method needing
this - A regular function as a callback, losing
this - Missing
breakin aswitch - No
defaultin aswitch for...inover an array- Assuming
typeof nullis'null' - Omitting the file extension in a browser module import
- Expecting
constto make an object immutable
Practice
- Write
calculatePercentage,getGradeandcalculateTotalfor exam results. HandlemaxMarksof zero. - Run the
varversusletsetTimeoutloop and explain both outputs. - Write
if (student.marks)where marks is0. Confirm the bug, then fix it with!= null. - Compare
student.marks ?? 0andstudent.marks || 0for marks of0,nullandundefined. - Evaluate
0 == '',0 == '0','' == '0'and explain why the results are not transitive. - Compute
0.1 + 0.2and a fee total in floats. Then redo it in paise and compare. - Write an object with an arrow-function method using
this. Record whatthisis, then fix it with method shorthand. - Write a
setTimeoutcallback inside a method using a regular function. Fix it with an arrow. - Write
createCounterwith a closure and confirmcountis not reachable from outside. - Attach click handlers in a loop with
var, confirm they all show the last student, then fix it withlet. - Write a
switchwith a missingbreakand observe the fall-through. - Split your code into two ES modules and load them with
type="module". Omit the.jsextension 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,letand nevervar - Read a console error and jump to the line
- Explain why
0.1 + 0.2is not0.3
Review questions
- Why does
varin aforloop withsetTimeoutlog the same value every time? - Why is
??safer than||for default values? - When should an arrow function not be used?
- What is the temporal dead zone, and why is it an improvement?
Next: Arrays and objects