Fundamentals
JavaScript Fundamentals
The concepts here are not beginner material — they're the parts of JavaScript that trip up experienced developers, cause subtle bugs, and come up in senior engineering discussions.
var vs let vs const
Scope
function scopeDemo() {
var x = 1; // function-scoped — visible anywhere in this function
let y = 2; // block-scoped — only within the nearest { }
const z = 3; // block-scoped + cannot be reassigned
}
// var leaks out of blocks
if (true) {
var leaked = 'I am visible outside';
let contained = 'I am not';
}
console.log(leaked); // 'I am visible outside'
console.log(contained); // ReferenceError
Hoisting and the Temporal Dead Zone
var is hoisted and initialized to undefined. let and const are hoisted but not initialized — accessing them before the declaration throws a ReferenceError. This inaccessible window is the Temporal Dead Zone (TDZ).
console.log(a); // undefined — var is hoisted and initialized
console.log(b); // ReferenceError — b is in the TDZ
console.log(c); // ReferenceError — c is in the TDZ
var a = 1;
let b = 2;
const c = 3;
const is not immutable
const prevents reassignment, not mutation.
const user = { name: 'Prajwal' };
user.name = 'Updated'; // ✅ mutation is allowed
user = {}; // ❌ TypeError: reassignment is not allowed
const nums = [1, 2, 3];
nums.push(4); // ✅ mutation allowed
nums = []; // ❌ TypeError
To make an object truly immutable:
const user = Object.freeze({ name: 'Prajwal', role: 'admin' });
user.name = 'Other'; // silently fails in non-strict mode, throws in strict
console.log(user.name); // 'Prajwal'
Note: Object.freeze is shallow — nested objects are still mutable.
Type Coercion
JavaScript silently converts types in many operations — a frequent source of bugs.
== vs ===
=== (strict equality) — compares value AND type, no coercion.
== (loose equality) — coerces types before comparing.
0 == false // true — false coerces to 0
0 === false // false — different types
'' == false // true — both coerce to 0
'' === false // false
null == undefined // true — special case in spec
null === undefined // false
NaN == NaN // false — NaN is not equal to itself
Rule: Always use ===. The only common exception is x == null which checks for both null and undefined.
// Checks both null and undefined
if (user == null) { ... } // same as: user === null || user === undefined
Truthy and Falsy
Every value is either truthy or falsy when evaluated in a boolean context.
Falsy values (only 8):
false, 0, -0, 0n, '', "", ``, null, undefined, NaN
Everything else is truthy — including [], {}, '0', 'false'.
if ([]) console.log('truthy'); // prints — empty array is truthy
if ({}) console.log('truthy'); // prints — empty object is truthy
if ('0') console.log('truthy'); // prints — non-empty string is truthy
Type coercion in operations
// + with a string → string concatenation
1 + '2' // '12' — 1 coerced to string
'3' - 1 // 2 — '3' coerced to number (- only works on numbers)
'5' * '2' // 10 — both coerced to number
// Unary + coerces to number
+'42' // 42
+'' // 0
+null // 0
+undefined // NaN
+true // 1
+false // 0
+[] // 0
+{} // NaN
Type checking
// typeof — reliable for primitives, unreliable for objects
typeof 42 // 'number'
typeof 'hello' // 'string'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof null // 'object' ← infamous bug in JS spec, null is NOT an object
typeof [] // 'object'
typeof {} // 'object'
typeof function(){} // 'function'
// instanceof — checks prototype chain
[] instanceof Array // true
[] instanceof Object // true (Array inherits from Object)
null instanceof Object // false
// Most reliable: Object.prototype.toString
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
Object.prototype.toString.call(/regex/) // '[object RegExp]'
Object.prototype.toString.call(new Map()) // '[object Map]'
// Practical utility
function typeOf(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
typeOf([]) // 'array'
typeOf(null) // 'null'
typeOf(new Map()) // 'map'
Primitive Types In Depth
JavaScript has 7 primitive types: string, number, bigint, boolean, undefined, null, symbol.
Numbers and floating point
// JavaScript uses IEEE 754 double-precision floating point
0.1 + 0.2 === 0.3 // false — 0.30000000000000004
// Fix: use toFixed or a tolerance
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true
// Special number values
Infinity // 1 / 0
-Infinity // -1 / 0
NaN // Not a Number — result of invalid math
// NaN is the only value not equal to itself
NaN === NaN // false
Number.isNaN(NaN) // true — safe check
Number.isNaN('abc') // false — unlike global isNaN() which coerces
isNaN('abc') // true ← coerces 'abc' to NaN first, misleading
// Safe integer range
Number.MAX_SAFE_INTEGER // 9007199254740991 (2^53 - 1)
9007199254740991 + 1 === 9007199254740992 // true
9007199254740992 + 1 === 9007199254740992 // true ← precision lost!
BigInt — for large integers
const bigNum = 9007199254740991n; // trailing n
bigNum + 1n // 9007199254740992n — exact
// Cannot mix BigInt and Number without explicit conversion
1n + 1 // TypeError
1n + BigInt(1) // 2n
Symbol — unique identifiers
const id1 = Symbol('id');
const id2 = Symbol('id');
id1 === id2 // false — every Symbol is unique
// Use case: non-colliding object property keys
const INTERNAL_ID = Symbol('internalId');
const user = {
name: 'Prajwal',
[INTERNAL_ID]: 'usr_abc123',
};
user[INTERNAL_ID] // 'usr_abc123'
Object.keys(user) // ['name'] — Symbol keys are hidden from enumeration
Primitive boxing
Primitives are not objects but they behave like objects because JS auto-boxes them when you access a property.
'hello'.toUpperCase() // JS temporarily wraps 'hello' in a String object
(42).toString(2) // '101010' — converts 42 to binary string
// This is why you can call methods on primitives
// The wrapper object is created, the method is called, the wrapper is discarded
Scope Chain and Lexical Scope
JavaScript uses lexical (static) scope — a function's scope is determined by where it is written, not where it is called.
const x = 'global';
function outer() {
const x = 'outer';
function inner() {
const x = 'inner';
console.log(x); // 'inner' — finds x in its own scope first
}
function noLocal() {
console.log(x); // 'outer' — looks up scope chain to outer
}
inner();
noLocal();
}
outer();
The scope chain is the linked list of variable environments from the current scope up to the global scope.
// Variable lookup order:
// 1. Current function scope
// 2. Outer function scope(s)
// 3. Global scope
// 4. ReferenceError if not found
Function Types and Declarations
Function Declaration vs Expression
// Declaration — hoisted completely (name AND body)
greet(); // works — hoisted
function greet() { return 'hello'; }
// Expression — variable hoisted, but the function is NOT
sayHi(); // TypeError: sayHi is not a function
var sayHi = function() { return 'hi'; };
// Arrow function expression — also not hoisted
sayBye(); // ReferenceError
const sayBye = () => 'bye';
Arrow functions vs regular functions
Key differences — not just syntax:
// 1. Arrow functions have no own `this`
const obj = {
name: 'Prajwal',
greet: function() {
console.log(this.name); // 'Prajwal' — `this` is obj
},
greetArrow: () => {
console.log(this.name); // undefined — arrow captures `this` from definition context (global)
},
};
// 2. Arrow functions have no `arguments` object
function regular() {
console.log(arguments); // Arguments [1, 2, 3]
}
const arrow = () => {
console.log(arguments); // ReferenceError
};
// 3. Arrow functions cannot be used as constructors
const Fn = () => {};
new Fn(); // TypeError: Fn is not a constructor
// 4. Arrow functions have no `prototype` property
const arrow2 = () => {};
console.log(arrow2.prototype); // undefined
Error Handling
// Creating custom error types
class AppError extends Error {
constructor(message, code, statusCode = 500) {
super(message);
this.name = 'AppError';
this.code = code;
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor); // cleaner stack trace
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} with id ${id} not found`, 'NOT_FOUND', 404);
this.name = 'NotFoundError';
}
}
class ValidationError extends AppError {
constructor(message, fields) {
super(message, 'VALIDATION_ERROR', 400);
this.name = 'ValidationError';
this.fields = fields;
}
}
// Usage
try {
throw new NotFoundError('Order', 'ORD-123');
} catch (err) {
if (err instanceof NotFoundError) {
console.log(err.statusCode); // 404
}
}
Error handling in async code
// Always handle promise rejections
async function fetchUser(id) {
try {
const user = await userService.getById(id);
return user;
} catch (err) {
if (err instanceof NotFoundError) throw err; // rethrow known errors
throw new AppError('Failed to fetch user', 'FETCH_ERROR'); // wrap unknown
}
}
// Global unhandled rejection handler
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
process.exit(1); // fail loudly in production
});
Short-circuit Evaluation and Nullish Coalescing
// || returns first truthy value
const name = user.name || 'Anonymous'; // problem: '' or 0 are treated as missing
// && returns first falsy value (or last truthy)
const result = isValid && processData(); // only calls processData if isValid is truthy
// ?? (nullish coalescing) — only checks for null/undefined, not all falsy
const name = user.name ?? 'Anonymous'; // '' or 0 are treated as valid values
// ?. (optional chaining) — short-circuits on null/undefined
const city = user?.address?.city; // undefined instead of TypeError
const first = arr?.[0]; // undefined if arr is null/undefined
const result2 = obj?.method?.(); // calls method only if it exists
// Logical assignment
user.name ||= 'Anonymous'; // assign only if user.name is falsy
user.count ??= 0; // assign only if user.count is null/undefined
config.debug &&= process.env.NODE_ENV !== 'production'; // assign only if truthy
Destructuring
// Object destructuring
const { name, age, role = 'user' } = user; // with default
const { name: userName } = user; // rename
const { address: { city } } = user; // nested
const { id, ...rest } = user; // rest
// Array destructuring
const [first, second, , fourth] = arr; // skip elements
const [head, ...tail] = arr; // rest
// Function parameter destructuring
function createOrder({ userId, items, status = 'PENDING' }) {
// ...
}
// Swap without temp variable
let a = 1, b = 2;
[a, b] = [b, a];
// Return multiple values
function getMinMax(arr) {
return { min: Math.min(...arr), max: Math.max(...arr) };
}
const { min, max } = getMinMax([3, 1, 4, 1, 5]);
Iterators and Generators
Iterators
Any object with a [Symbol.iterator] method that returns an iterator (an object with a next() method).
// Custom iterator
function range(start, end) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
}
for (const n of range(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5
}
const nums = [...range(1, 5)]; // [1, 2, 3, 4, 5]
Generators
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i; // pauses here, resumes on next()
}
}
const gen = range(1, 3);
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { value: undefined, done: true }
for (const n of range(1, 5)) console.log(n);
// Infinite sequence
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
const ids = idGenerator();
ids.next().value; // 1
ids.next().value; // 2
Map, Set, WeakMap, WeakSet
// Map — key-value pairs where keys can be any type
const map = new Map();
map.set('key', 'value');
map.set(42, 'number key');
map.set({ id: 1 }, 'object key');
map.get('key'); // 'value'
map.has('key'); // true
map.size; // 3
map.delete('key');
for (const [key, value] of map) { ... }
// Convert to/from object
const obj = Object.fromEntries(map);
const map2 = new Map(Object.entries(obj));
// Set — unique values
const set = new Set([1, 2, 2, 3, 3]);
console.log([...set]); // [1, 2, 3]
set.add(4);
set.has(2); // true
set.delete(2);
// Deduplicate array
const unique = [...new Set(arr)];
// WeakMap — keys must be objects, not enumerable, GC-friendly
const wm = new WeakMap();
const key = {};
wm.set(key, 'private data');
// When `key` is garbage collected, the entry is automatically removed
// Use case: attaching private data to DOM nodes or objects without memory leaks
const cache = new WeakMap();
function process(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = expensiveOperation(obj);
cache.set(obj, result);
return result;
}
Interview definition (short answer)
"JavaScript fundamentals that matter in production:
let/consthave block scope and TDZ;==coerces types (===does not);constprevents reassignment not mutation;typeof nullis'object'(spec bug); arrow functions have no ownthisorarguments;??differs from||by only checking null/undefined; WeakMap/WeakSet allow GC of keys."