You use all fourteen of these every day. You have just never had to say out loud how they work. That is the only thing an interview actually tests.
Read one topic, close the page, and say it in your own words. If it comes out fuzzy, read it again before moving on.
var, let and const
These are three ways to create a variable. They differ in three things: where you can use it, whether you can change it, and what happens if you use it too early.
| var | let | const | |
|---|---|---|---|
| Where it works | whole function | only inside braces | only inside braces |
| Can you create it twice? | yes | no | no |
| Can you change it? | yes | yes | no |
| Using it too early gives | undefined | an error | an error |
"Only inside braces" means the variable is born at the opening brace and dies at the closing brace. var does not care about braces at all. It only cares about functions.
function demo() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1 -- var escaped the if block
console.log(b); // Error: b is not defined
}The temporal dead zone
A let or const variable is created as soon as the block starts. But you cannot touch it until the line that declares it runs. That gap is called the temporal dead zone.
Why does this exist? So that const can be trusted. If const behaved like var, a constant would be undefined for part of its life. That makes no sense. An error is better than a wrong value.
const user = { name: "Subham" };
user.name = "Kumar"; // works -- we changed what is inside
user.age = 26; // works
user = {}; // Error -- we tried to point it somewhere new
const list = [1, 2];
list.push(3); // works -- list is now [1, 2, 3]Hoisting
JavaScript reads your code twice. On the first pass it writes down every name you declared. On the second pass it actually runs the lines. Hoisting is just the name for that first pass.
Nothing physically moves in your file. The names were simply known before line one ran.
| How you declared it | Value before its line runs | Reading it early gives |
|---|---|---|
function greet() {} | the whole function | it works, you can call it |
var a | undefined | undefined |
let / const | nothing, it is locked | ReferenceError |
class Person {} | nothing, it is locked | ReferenceError |
var f = function () {} | undefined | TypeError when you call it |
Look at that last row carefully. The variable f was hoisted like any var, so it holds undefined. Calling undefined() is a TypeError, not a ReferenceError. Interviewers love this difference.
Primitives and objects
Some values are stored directly inside the variable. Others are stored somewhere else, and the variable only holds the address. That one difference explains a lot of confusing bugs.
There are exactly seven values stored directly. These are called primitives: string, number, boolean, null, undefined, symbol and bigint.
Everything else is an object — objects, arrays, functions, dates, Maps, regex. For those, the variable holds an address.
let a = 10;
let b = a; // b gets its own 10
b++;
console.log(a, b); // 10 11 -- separate
let x = { n: 10 };
let y = x; // y gets a copy of the arrow, not the object
y.n++;
console.log(x.n, y.n); // 11 11 -- same object
y = { n: 99 }; // now y points somewhere else
console.log(x.n, y.n); // 11 99Pass by value or pass by reference?
The answer is always pass by value. The catch is that for an object, the value being copied is the address.
function mutate(obj) { obj.name = "changed"; } // caller sees this
function reassign(obj) { obj = { name: "new" }; } // caller sees nothing
const person = { name: "original" };
mutate(person); console.log(person.name); // "changed"
reassign(person); console.log(person.name); // "changed" -- stillEquality, truthy and falsy
=== is strict. Different types means false, immediately. == is lenient. It first converts the two sides to the same type, then compares. That converting step is called coercion, and almost every strange JavaScript result comes from it.
The eight falsy values
false, 0, -0, 0n, "", null, undefined, NaN.
Everything else is truthy. That includes the ones that surprise people: [], {}, "0", "false" and -1.
The rules that actually matter
null == undefinedis true. Neither one equals anything else, not0and not"".NaNis not equal to anything, including itself. UseNumber.isNaN(x).- A boolean becomes a number first.
truebecomes1,falsebecomes0. - String against number: the string becomes a number.
- Object against primitive: the object becomes a primitive first.
// Walking through the famous one, step by step:
[] == false
// 1. false is a boolean, so turn it into a number -> 0
// 2. [] is an object, so turn it into a primitive -> ""
// 3. "" against 0, so turn the string into a number -> 0
// 4. 0 == 0 -> true
null == 0 // false -- null only ever matches undefined
null >= 0 // true -- >= uses a different rule and turns null into 0
NaN == NaN // false
"" == 0 // true
"0" == 0 // true
"" == "0" // false -- both are strings, so no conversion happens
"5" + 3 // "53" -- + joins text if either side is a string
"5" - 3 // 2 -- every other operator only does mathstypeof and instanceof
Four ways to ask "what is this value?". Each has a blind spot, so you need to know which to use when.
typeof 42 // "number"
typeof "hi" // "string"
typeof undefined // "undefined"
typeof null // "object" <-- famous bug
typeof [] // "object" -- no help for arrays
typeof {} // "object"
typeof function(){} // "function"
typeof notDeclared // "undefined" -- does not throw. Only typeof is safe here.Why is typeof null equal to "object"? It is a bug from 1995. Back then a value's type was read from a few bits, and the null pointer was all zeroes, which was the same pattern used for objects. Fixing it would break millions of websites, so it stayed.
instanceof asks whether a constructor appears anywhere in the object's prototype chain. Good for your own classes. Two blind spots: it does not work on primitives, and it fails across iframes or worker threads.
[] instanceof Array // true
[] instanceof Object // true -- it checks the whole chain
"hi" instanceof String // false -- primitives are never instances
new Date() instanceof Date // trueThe two you should actually use
Array.isArray([]); // true -- always use this for arrays
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(new Date()); // "[object Date]"
function typeOf(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
typeOf([]); // "array"
typeOf(null); // "null"Destructuring
Instead of pulling values out one line at a time, you write the shape you expect on the left side. JavaScript matches it and hands you the pieces. Objects match by key name. Arrays match by position.
const user = { id: 1, name: "Subham", address: { city: "Bengaluru" } };
const { name } = user; // "Subham"
const { name: fullName } = user; // rename it to fullName
const { role = "dev" } = user; // default when the key is missing
const { address: { city } } = user; // reach inside
const { id, ...rest } = user; // rest = everything else
const nums = [10, 20, 30, 40];
const [first, second] = nums; // 10, 20
const [, , third] = nums; // skip positions -> 30
const [head, ...tail] = nums; // 10, [20, 30, 40]
let p = 1, q = 2;
[p, q] = [q, p]; // swap, no temp variableWhere you already use it
// Express -- you write this every day
const { email, password } = req.body;
// React
function Card({ title, onClick, size = "md" }) {}
// looping an object
for (const [key, value] of Object.entries(user)) {}
// options object with a safe default
function request(url, { method = "GET", retries = 3 } = {}) {}
// the trailing = {} is what makes request(url) work with one argumentSpread, rest and copying
Same three dots, two opposite jobs. Look at where it sits to know which one it is.
- Rest collects many things into one. It sits on the left, or in a function's parameters.
- Spread opens one thing out into many. It sits on the right, or in a function call.
// REST -- collecting
function sum(...numbers) { return numbers.reduce((a, b) => a + b, 0); }
const [head, ...tail] = [1, 2, 3];
const { id, ...others } = user;
// SPREAD -- opening out
const merged = [...arr1, ...arr2];
const copy = { ...original, status: "active" }; // later keys win
Math.max(...[3, 7, 2]); // 7Spread copies only one level
This is the most important thing in this whole note. Spread makes a shallow copy. The top level is new. Anything nested inside is still the same object as the original.
const original = { name: "Subham", tags: ["node"], meta: { active: true } };
const copy = { ...original };
copy.name = "Kumar";
console.log(original.name); // "Subham" -- safe, it was a primitive
copy.meta.active = false;
console.log(original.meta.active); // false -- leaked!
copy.tags.push("react");
console.log(original.tags); // ["node", "react"] -- leaked too| Way to deep copy | Handles | Breaks on |
|---|---|---|
structuredClone(obj) | nested objects, arrays, Date, Map, Set, RegExp, circular links | functions, DOM nodes |
JSON.parse(JSON.stringify(obj)) | plain data only | Date turns into text, undefined and functions disappear, Map and Set become empty objects |
lodash cloneDeep | almost everything | needs the package |
Optional chaining and nullish coalescing
Three small operators. Together they remove most of the defensive if checks you used to write.
Stop instead of crashing
If the thing on the left is null or undefined, the whole expression stops and gives you undefined. No crash.
const city = user?.address?.city; // undefined instead of a crash
const first = list?.[0]; // safe index
callback?.(); // only runs if callback exists
// what people used to write
const city2 = user && user.address && user.address.city;Only replace missing values
This is the important one. || replaces every falsy value. ?? replaces only null and undefined.
const count = 0;
count || 10; // 10 -- wrong, 0 was a real answer
count ?? 10; // 0 -- correct
const name = "";
name || "Guest"; // "Guest"
name ?? "Guest"; // "" -- the empty string was a real answera ??= 5; // set a only if it is null or undefined
b ||= 5; // set b only if it is falsy
c &&= 5; // set c only if it is truthy
cache.users ??= await fetchUsers(); // the fetch runs only if cache is emptyTemplate literals
Strings written with backticks. You can drop values inside them, and they can span multiple lines. Any expression works inside the placeholder, not just a variable.
Tagged templates
Put a function name right before the backticks. That function gets called with two things: the text pieces, and the values you injected — kept separate. Keeping them separate is the whole point. The function can trust the text and treat the injected values as unsafe.
function highlight(strings, ...values) {
// strings = ["Hello ", ", you are ", " years old"]
// values = ["Subham", 26]
return strings.reduce(
(out, str, i) => out + str + (values[i] !== undefined ? "[" + values[i] + "]" : ""),
""
);
}Strict mode
A switch that turns JavaScript's silent mistakes into loud errors. You turn it on by putting "use strict"; as the first line of a file or a function. You rarely write it now, because ES modules and class bodies are always strict. But interviewers still ask what it changes.
| Without strict mode | With strict mode |
|---|---|
| Assigning to an undeclared name quietly creates a global | throws an error |
this in a plain function is window | this is undefined |
| Writing to a frozen property fails silently | throws an error |
| Two parameters with the same name is allowed | syntax error |
Numbers written like 0755 are allowed | syntax error |
The this change is the one that matters most. Without strict mode, a broken method call quietly writes onto window. With it, you get an error and find the bug.
Floating point precision
Every number in JavaScript is stored as a decimal in base 2. Some numbers cannot be written exactly in base 2, so the computer stores the closest value it can. Adding two of those makes the small error visible.
Why 0.1 + 0.2 is not 0.3
0.1 + 0.2; // 0.30000000000000004
0.1 + 0.2 === 0.3; // falseconst nearlyEqual = (a, b) => Math.abs(a - b) < Number.EPSILON;
nearlyEqual(0.1 + 0.2, 0.3); // true
// Number.EPSILON is the smallest gap between 1 and the next number
// JavaScript can store. Roughly 0.0000000000000002.Number.MAX_SAFE_INTEGER; // 9007199254740991
9007199254740992 === 9007199254740993; // true (!) -- both round to the same value
const big = 9007199254740993n; // the n makes it a BigInt
big + 1n; // exact
big + 1; // TypeError -- you cannot mix BigInt and NumberWhere this hits you for real: if your backend sends a 64-bit database ID as a JSON number, JSON.parse in the browser will silently change the last digits. The fix is to send IDs as strings.
JSON stringify and parse
JSON is a small text format that only understands six kinds of things: objects, arrays, strings, numbers, booleans and null. Anything else in your object gets dropped, changed, or throws an error.
| What you had | What stringify does with it |
|---|---|
undefined | the key disappears, or becomes null inside an array |
| a function | same, it disappears |
Date | becomes a text string, and never turns back into a Date |
Map, Set | become empty objects, contents gone |
NaN, Infinity | become null |
| an object that links back to itself | throws an error |
JSON.stringify({ a: undefined, b: () => {}, c: new Date(), d: new Set([1]) });
// '{"c":"2026-08-28T10:00:00.000Z","d":{}}'
// a and b vanished. c became text. d lost its contents.JSON.stringify(obj, null, 2); // pretty print with 2 spaces
JSON.stringify(obj, (key, value) => // hide fields before logging
key === "password" ? undefined : value);
JSON.parse(text, (key, value) => // fix values on the way back in
key === "createdAt" ? new Date(value) : value);An object can also define its own toJSON() method. Stringify calls that instead of reading the object directly. That is exactly how Date turns itself into text, and it is a clean way to control what your model looks like in an API response.
Symbols
A value whose only job is to be unique. Two symbols are never equal, even with the same description. That makes them safe keys that can never clash with anyone else's key.
const a = Symbol("id");
const b = Symbol("id");
a === b; // false -- the text is only a label for debugging
const user = { name: "Subham", [a]: 123 };
user[a]; // 123
Object.keys(user); // ["name"] -- symbol keys are skipped
JSON.stringify(user); // '{"name":"Subham"}' -- skipped here tooWhy you would use one: to attach your own data to an object you do not own, with zero risk that someone later adds a normal key with the same name.
Built-in symbols
JavaScript has special symbols that act as hooks. Add one to your object and it starts working with syntax it could not use before.
// Symbol.iterator makes an object work with for...of and with spread
const range = {
from: 1,
to: 4,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next: () =>
current <= last
? { value: current++, done: false }
: { value: undefined, done: true },
};
},
};
[...range]; // [1, 2, 3, 4]
for (const n of range) {} // 1 2 3 4Others worth recognising: Symbol.asyncIterator makes for await...of work, and Symbol.toPrimitive controls how your object converts in == and +.
Labels and switch
Three small corners of the syntax. You will rarely write them, but two cause real bugs and one shows up in puzzle questions.
Labels
Normally break only exits the loop it is directly inside. A label lets you name an outer loop and break out of that one instead.
outer:
for (const row of grid) {
for (const cell of row) {
if (cell === target) break outer; // exits BOTH loops
}
}switch fallthrough
Two things to know. First, switch compares with ===, so "1" never matches case 1. Second, without a break, the code keeps running into the next case.
switch (role) {
case "admin":
case "owner": // on purpose -- both run the same code
grantFullAccess();
break;
case "editor":
grantWriteAccess();
// no break -- so an editor also gets read access below. Usually a bug.
case "viewer":
grantReadAccess();
break;
default:
deny();
}The comma operator
It runs every expression from left to right and gives you the last one. You will basically never write it, but you will see it in minified code and in interview puzzles.
const x = (1, 2, 3); // 3
for (; i < 5; i++, j++) {} // the one normal useTen questions
Say your answer out loud before you reveal it. If your spoken answer was fuzzy, that topic is not finished. Read it again instead of moving on.
- 01What does this print?
console.log(a)thenconsole.log(b), withvar a = 1andlet b = 2declared below them.First line prints undefined. Second line throws a ReferenceError.
var ais registered with a starting value of undefined, so reading it is allowed.let bis registered but locked until its own line runs. Reading it inside that locked period throws. - 02Is
config.retries = 5allowed when config was declared withconst? What does const actually stop you from doing?Allowed.
constonly stops you from pointing the variable at a different value. It does not lock the contents.Reassigning the whole object would throw. If you want the contents locked, use
Object.freeze— and that is only one level deep. - 03Name all eight falsy values. Is an empty array one of them?
false,0,-0,0n,"",null,undefined,NaN.No. Both
[]and an empty object are truthy. That is whyif (arr)never tells you whether an array has items — you needarr.length. - 04Why is
[] == falsetrue, but[] === falsefalse?===checks the type first. An object is not a boolean, so it is false straight away.==converts.falsebecomes the number 0. The empty array becomes an empty string. Then the empty string becomes 0. Finally 0 equals 0, which is true. - 05A function sets
obj.x = 99, reassigns itsarrparameter to a new array, and reassigns itsnumparameter. Which changes are visible to the caller?Only the first one. The caller sees
o.xas 99, but the array and the number are unchanged.Setting a property changes the same object the caller is holding. Reassigning a parameter only points the local copy somewhere new. This single question covers the whole pass-by-value story.
- 06A user sets their limit to
0. Why doessettings.limit || 20ignore them, and what is the fix?0is falsy, so||throws it away and uses 20. The user's real choice is lost.Fix:
settings.limit ?? 20. The??operator only steps in for null and undefined, so a real 0 survives. The same bug happens with empty strings and with false. - 07What is wrong with taking
const copy = {...state}and then settingcopy.user.name?Spread only copies one level.
copy.useris the exact same object asstate.user, so this changes the original state too.Either spread every level you touch, or take a real deep copy with
structuredClone. - 08Why is
typeof nullequal to"object"? What should you use instead?An old bug. Values used to carry a small type tag in their bits, and the null pointer was all zeroes, which was the same tag used for objects. Fixing it would break too much existing code.
Use
value === nullfor null,Array.isArrayfor arrays, andObject.prototype.toString.callwhen you need the exact type of anything. - 09An object holds a Date, a Set, a function and an undefined key. What survives a
JSON.parse(JSON.stringify(...))round trip?Only the Date and the Set key survive, and both are damaged.
The Date became text and will not turn back. The Set became an empty object with its contents gone. The function and the undefined key disappeared completely. This is exactly why the JSON trick is a bad deep clone.
- 10Calling a hoisted
varfunction expression before its line throws one kind of error. Using a class before its line throws another. Which is which, and why?The function expression throws a TypeError. The variable exists, it just holds undefined right now, and you cannot call undefined.
The class throws a ReferenceError. Classes are locked until their line runs, so the name cannot be touched at all yet.
Short version: exists but wrong type, versus cannot be touched yet.
Six exercises
Do these in a plain file with autocomplete off and no searching.
- 01
Write deepClone(value)
No
structuredCloneand no JSON. Handle nested objects, arrays, Date, Map, Set and primitives. Then make it survive an object that points back at itself. Hint: keep aWeakMapof things you have already cloned. - 02
Write typeOf(value)
Return a lowercase word that is correct for every input: array, null, date, map, regexp, number, nan, function.
NaNneeds its own check, becausetypeof NaNis"number". - 03
Write get(object, path, fallback)
Rebuild lodash's
get, so that a dotted path walks safely and returns the fallback if any step is missing. Then write the one-line version using optional chaining and??, and decide which one you would ship. - 04
Predict, then run
Write your answer down before running:
"5" - 3,"5" + 3,1 < 2 < 3,3 > 2 > 1,null >= 0,null > 0,null == 0. Then explain each result using the coercion rules above. The explanation matters more than the answer. - 05
A safe money helper
Write a small module that stores amounts as whole paise and exposes add, subtract, multiply and format. Write one test that proves your version handles
0.1 + 0.2correctly where plain numbers do not. - 06
Make something iterable
Build an object with a
Symbol.iteratorthat yields fixed-size chunks of an array, so that spreading it gives you groups of two. Then write the same thing as a generator function and decide which reads better.