JavaScript · 14 topics

JavaScript language fundamentals, in plain English

The 14 fundamentals every JavaScript interview starts with, explained in plain English with diagrams, gotchas and the answer to say out loud.

Published
Reading time
24 min read
Topics covered
14 topics

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.

varletconst
Where it workswhole functiononly inside bracesonly inside braces
Can you create it twice?yesnono
Can you change it?yesyesno
Using it too early givesundefinedan erroran 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.

BLOCK STARTSBLOCK ENDSTEMPORAL DEAD ZONEexists, but lockedReferenceErrorUSABLEreading it works normallylet name = "Subham";THE DECLARATION LINE
The variable is not missing during the dead zone. It exists. JavaScript just refuses to let you read it until execution reaches the declaration line.

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.

PASS 1 — WRITE DOWN THE NAMESNAMESTARTING VALUEgreetthe functionaundefinedblocked (TDZ)PASS 2 — RUN THE LINESgreet()worksconsole.log(a)undefinedconsole.log(b)throwssayHi()throwsSOURCE ORDER, WHICH NEVER CHANGESfunction greet() • var a = 1 • let b = 2 • var sayHi = function()
Both passes happen before your first line of output. What each name is worth on the second pass depends entirely on how it was declared.
How you declared itValue before its line runsReading it early gives
function greet() {}the whole functionit works, you can call it
var aundefinedundefined
let / constnothing, it is lockedReferenceError
class Person {}nothing, it is lockedReferenceError
var f = function () {}undefinedTypeError 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.

PRIMITIVE — EACH GETS ITS OWN COPYa10b10b++ changes only bOBJECT — BOTH HOLD THE SAME ADDRESSxyn: 10y.n++ is visible through x too
Copying a primitive duplicates the value. Copying an object duplicates only the arrow, so both names lead to the same object.
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 99

Pass 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"  -- still

Equality, 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 == undefined is true. Neither one equals anything else, not 0 and not "".
  • NaN is not equal to anything, including itself. Use Number.isNaN(x).
  • A boolean becomes a number first. true becomes 1, false becomes 0.
  • 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 maths

typeof 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 // true

The 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 variable

Where 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 argument

Spread, 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]);                           // 7

Spread 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.

SHALLOW COPY — spreadoriginalname, metacopyname, metaactivesharedone objectDEEP CLONE — structuredCloneoriginalactivecloneactive
Spread rebuilds only the outer object, so the nested object stays shared and writing through the copy also changes the original. A deep clone rebuilds every level.
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 copyHandlesBreaks on
structuredClone(obj)nested objects, arrays, Date, Map, Set, RegExp, circular linksfunctions, DOM nodes
JSON.parse(JSON.stringify(obj))plain data onlyDate turns into text, undefined and functions disappear, Map and Set become empty objects
lodash cloneDeepalmost everythingneeds 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 answer
a ??= 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 empty

Template 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 modeWith strict mode
Assigning to an undeclared name quietly creates a globalthrows an error
this in a plain function is windowthis is undefined
Writing to a frozen property fails silentlythrows an error
Two parameters with the same name is allowedsyntax error
Numbers written like 0755 are allowedsyntax 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;      // false
const 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 Number

Where 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 hadWhat stringify does with it
undefinedthe key disappears, or becomes null inside an array
a functionsame, it disappears
Datebecomes a text string, and never turns back into a Date
Map, Setbecome empty objects, contents gone
NaN, Infinitybecome null
an object that links back to itselfthrows 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 too

Why 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 4

Others 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 use

Ten 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.

  1. 01
    What does this print? console.log(a) then console.log(b), with var a = 1 and let b = 2 declared below them.
  2. 02
    Is config.retries = 5 allowed when config was declared with const? What does const actually stop you from doing?
  3. 03
    Name all eight falsy values. Is an empty array one of them?
  4. 04
    Why is [] == false true, but [] === false false?
  5. 05
    A function sets obj.x = 99, reassigns its arr parameter to a new array, and reassigns its num parameter. Which changes are visible to the caller?
  6. 06
    A user sets their limit to 0. Why does settings.limit || 20 ignore them, and what is the fix?
  7. 07
    What is wrong with taking const copy = {...state} and then setting copy.user.name?
  8. 08
    Why is typeof null equal to "object"? What should you use instead?
  9. 09
    An object holds a Date, a Set, a function and an undefined key. What survives a JSON.parse(JSON.stringify(...)) round trip?
  10. 10
    Calling a hoisted var function expression before its line throws one kind of error. Using a class before its line throws another. Which is which, and why?

Six exercises

Do these in a plain file with autocomplete off and no searching.

  1. 01

    Write deepClone(value)

    No structuredClone and no JSON. Handle nested objects, arrays, Date, Map, Set and primitives. Then make it survive an object that points back at itself. Hint: keep a WeakMap of things you have already cloned.

  2. 02

    Write typeOf(value)

    Return a lowercase word that is correct for every input: array, null, date, map, regexp, number, nan, function. NaN needs its own check, because typeof NaN is "number".

  3. 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.

  4. 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.

  5. 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.2 correctly where plain numbers do not.

  6. 06

    Make something iterable

    Build an object with a Symbol.iterator that 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.

Common questions

What is the temporal dead zone in JavaScript?
The temporal dead zone is the period between the start of a block and the line that declares a let or const variable. The variable already exists during this period but cannot be read, so accessing it throws a ReferenceError. It exists so that const can never be observed holding undefined.
What is the difference between var, let and const?
var is function scoped and starts as undefined, so reading it early returns undefined. let and const are block scoped and stay locked until their declaration line runs, so reading them early throws a ReferenceError. const also prevents reassigning the variable, though you can still change the contents of an object it points to.
Why does 0.1 + 0.2 not equal 0.3 in JavaScript?
Every JavaScript number is stored as a 64-bit binary floating point value. Numbers like 0.1 cannot be represented exactly in base 2, so the closest storable value is used. Adding two of those approximations makes the error visible, giving 0.30000000000000004. Compare floats with a tolerance such as Number.EPSILON, and store money as whole paise or cents instead.
What is the difference between ?? and || in JavaScript?
The || operator falls back whenever the left side is any falsy value, including 0, an empty string and false. The ?? operator falls back only when the left side is null or undefined. For user settings and API data, ?? is usually correct because it preserves a deliberate 0 or empty string.
Is JavaScript pass by value or pass by reference?
JavaScript is always pass by value. For objects, the value being copied is the reference itself. That is why changing a property inside a function is visible to the caller, while reassigning the whole parameter is not.