A Primer on JavaScript
Published on June 21, 2026
If you already know Python and Java, JavaScript is easy to pick up. It sits somewhere between the two:
- Like Python, it is dynamically typed — you don’t declare types, and a variable can hold a string now and a number later.
- Like Java, the syntax is C-style — curly braces for blocks, semicolons
at the end of statements,
forloops with the classic three-part header.
This post collects the handful of things worth knowing up front.
Primitive types
Unlike some languages, JavaScript doesn’t split numbers into int, float,
double, etc. — there is just one number type that covers integers and
floating-point alike. In total JavaScript has 7 primitive types:
| Type | Example |
|---|---|
number |
42, 3.14, NaN, Infinity |
string |
"hello", 'world' |
boolean |
true, false |
undefined |
let x; |
null |
let x = null; |
bigint |
123n |
symbol |
Symbol("id") |
A couple of things worth noting:
numberincludes special valuesNaN(not-a-number) andInfinity.bigintis for integers larger thannumbercan safely hold — note thensuffix.
You check a value’s type with the typeof operator. A few worth remembering:
typeof 42 // "number"
typeof "hello" // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" <-- historical bug
typeof {} // "object"
typeof [] // "object"
typeof function(){} // "function"
Two gotchas to note: typeof null returns "object" (a long-standing bug in
the language), and arrays also report "object".
For arrays specifically, use Array.isArray(obj). Your instinct might be to
reach for typeof, but there’s a gotcha — both arrays and plain objects report
"object", so typeof can’t tell them apart:
typeof [] // "object"
typeof {} // "object"
Array.isArray([]) // true
Array.isArray({}) // false
Math utilities
The built-in Math object has the usual helpers. Math.min and Math.max
return the smallest/largest of their arguments, and Infinity is handy as a
starting “very large” value (for example when finding a minimum):
Math.min(3, 1, 2); // 1
Math.max(3, 1, 2); // 3
let smallest = Infinity;
for (const x of [5, 2, 8]) {
smallest = Math.min(smallest, x);
}
// smallest === 2
Always use ===, never ==
This is the single most important habit to build. The honest answer is that
most modern JavaScript developers rarely use ==.
Historically, == was added to make JavaScript more forgiving in the early days
of the web. It does type coercion before comparing, so it converts the
operands to a common type first:
"5" == 5 // true
1 == true // true
0 == false // true
The idea was that developers wouldn’t have to manually convert types all the time. For example, a value coming from an HTML form is always a string:
let age = "25"; // from an HTML form
if (age == 25) {
console.log("same"); // this prints
}
The problem is that these coercion rules are surprising and error-prone. ===
(strict equality) compares value and type with no coercion:
"5" === 5 // false
1 === true // false
0 === false // false
Rule of thumb: always use === and !==. Forget == exists.
Declaring variables: const and let
Prefer const for anything that won’t be reassigned (think final in Java),
and let when you genuinely need to reassign. Avoid the old var.
const name = "Seroze"; // cannot be reassigned
let count = 0; // can be reassigned
count = count + 1;
Watch out: if you assign to a variable without
declaring it with let/const/var, JavaScript silently creates a global
variable instead of erroring. This is a historical quirk and a real source of
bugs.
I got bitten by this in a binary search — I used lo and hi without declaring
them, so they leaked into the global scope and the code did all sorts of garbage:
function search(arr, target) {
lo = 0; // ❌ no let/const — becomes a global!
hi = arr.length - 1;
while (lo <= hi) {
// ...
}
}
The fix is just to declare them: let lo = 0, hi = arr.length - 1;. (Running in
strict mode — "use strict"; — turns this silent footgun into an actual
error, which is why modules and classes enable it by default.)
For loops
The classic C-style loop is exactly what you’d expect:
for (let i = 0; i < 10; i++) {
console.log(i);
}
Arrays
Arrays are JavaScript’s lists. Use .length to get the number of elements:
const arr = [10, 20, 30];
console.log(arr.length); // 3
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
A few array basics worth learning early: push() / pop() (add/remove at the
end), shift() / unshift() (front), slice(), indexOf(), and the
higher-order ones like map(), filter(), and forEach().
Creating an array of size n
Use the Array(n) constructor together with .fill() to get an array of a
fixed size with a default value:
const zeros = new Array(5).fill(0); // [0, 0, 0, 0, 0]
const ones = new Array(3).fill(1); // [1, 1, 1]
If you need each element computed from its index, use Array.from():
// [0, 1, 2, 3, 4]
const seq = Array.from({ length: 5 }, (_, i) => i);
// 2D array (3x3 grid of zeros) — note: build each row separately
const grid = Array.from({ length: 3 }, () => new Array(3).fill(0));
Heads up: don’t write
new Array(3).fill(new Array(3).fill(0))for a 2D grid — every row would be the same array reference, so changing one row changes all of them. UseArray.fromas shown above.
Checking if an element is in an array
Use includes() — it returns a boolean and is the cleanest way to check
membership:
const arr = [10, 20, 30];
arr.includes(20); // true
arr.includes(99); // false
If you also need the position of the element, use indexOf(). It returns the
index, or -1 if the element isn’t present:
arr.indexOf(20); // 1
arr.indexOf(99); // -1
if (arr.indexOf(20) !== -1) {
console.log("found it");
}
Note: indexOf() returns only the first matching
index, even if the value appears multiple times in the array.
[5, 7, 5, 9].indexOf(5); // 0, not 2 — only the first match
(If you need the last one, use lastIndexOf().)
Sorting: always pass a comparator
By default sort() converts elements to strings and sorts
lexicographically. This produces nonsense for numbers:
const nums = [1, 2, 10, 5];
nums.sort();
console.log(nums); // [1, 10, 2, 5] ❌
10 comes before 2 because the string "10" is less than "2".
Always pass a custom comparator (a lambda) when sorting
numbers. The comparator returns a negative number if a should come first, zero
if equal, positive if b should come first.
const nums = [1, 2, 10, 5];
nums.sort((a, b) => a - b); // ascending
console.log(nums); // [1, 2, 5, 10] ✅
nums.sort((a, b) => b - a); // descending
console.log(nums); // [10, 5, 2, 1]
Yes, the string-by-default behaviour is garbage — but this is how JavaScript works, so just build the habit of always passing a comparator.
Comparator vs key functions (vs Python)
There’s a subtle difference here if you’re coming from Python. JavaScript
follows the Java convention — sort() takes a comparator (a, b) that
answers “which of these two comes first?”. Python takes a key function
that answers “what value should I sort this element by?”.
# Python — key style
arr.sort(key=lambda obj: obj.x)
// JavaScript — comparator style
arr.sort((a, b) => a.x - b.x);
A common mistake is to pass a Python-style key to JS: arr.sort(o => o.x). That
silently breaks, because JS calls it as fn(a, b), not fn(a). To sort by a
key fn, wrap it in a comparator: arr.sort((a, b) => fn(a) - fn(b)).
String utility methods
Strings come with a bunch of built-in helpers. Get familiar with them — you’ll reach for these constantly:
const s = "hello world";
s.charAt(0); // "h"
s.slice(0, 5); // "hello"
s.toUpperCase(); // "HELLO WORLD"
s.toLowerCase(); // "hello world"
s.indexOf("world") // 6
s.includes("lo") // true
s.split(" "); // ["hello", "world"]
s.trim(); // removes leading/trailing whitespace
Note that .length works on strings too (s.length is 11).
Strings in JavaScript are immutable — you can’t change a character in place. Assigning to an index simply does nothing:
let s = "hello";
s[0] = "H";
console.log(s); // "hello" — unchanged
If you need to edit a string, convert it to a character array first, mutate the array, then join it back:
let s = "hello";
const chars = s.split(""); // ["h", "e", "l", "l", "o"]
chars[0] = "H";
s = chars.join(""); // "Hello"
console.log(s); // "Hello"
The key trick is const chars = s.split("") — since strings are immutable, this
is the standard way to get a mutable char array you can index into and edit. It
comes up constantly in string problems, so keep it handy.
And to go back the other way, join the array into a string:
return chars.join(""); // convert char array back to a String
Sets
A Set stores unique values. Use add() to insert, has() to check
membership, delete() to remove, and .size for the count:
const seen = new Set();
seen.add(1); seen.add(1); seen.add(2);
console.log(seen.has(1), seen.size); // true 2
The ... spread / rest operator
... is one of the most useful pieces of modern JS syntax. It does two
opposite-looking things depending on where it appears. (It’s roughly analogous
to Python’s *args / *list unpacking.)
Spread — “unpack” into individual items
It expands an array (or any iterable) into its individual elements. This is
exactly what fn(...args) does in the cancellable example above:
fn(...args); // if args = [2, 3], this calls fn(2, 3)
Other common uses:
const a = [1, 2, 3];
const b = [...a, 4, 5]; // [1, 2, 3, 4, 5] — copy + extend
Math.max(...a); // 3 — array -> arguments
const o1 = { x: 1 };
const o2 = { ...o1, y: 2 }; // { x: 1, y: 2 } — works on objects too
It’s also the idiomatic way to make a shallow copy: const copy = [...arr].
Note that spread is only a shallow copy — nested objects/arrays are still
shared by reference. For a true deep copy, use the built-in
structuredClone():
const original = { a: 1, nested: { b: 2 } };
const shallow = { ...original };
shallow.nested.b = 99;
console.log(original.nested.b); // 99 — nested object was shared ❌
const deep = structuredClone(original);
deep.nested.b = 42;
console.log(original.nested.b); // 99 — original untouched ✅
This is the JS equivalent of Python’s copy.deepcopy() (versus the shallow
copy.copy()).
Rest — “collect” the leftovers into one array
In a function’s parameter list (or in destructuring), ... does the reverse —
it gathers multiple values into a single array:
function sum(...nums) { // nums is a real array
return nums.reduce((acc, x) => acc + x, 0);
}
sum(1, 2, 3); // 6
const [first, ...others] = [10, 20, 30];
// first = 10, others = [20, 30]
Rule of thumb: ... in a call or literal spreads (unpacks); ... in a
parameter list or destructuring target collects (rest).
Closures
A closure is a function that “remembers” the variables from the scope where it was created, even after that outer function has finished running. The inner variable stays alive as long as the returned function holds a reference to it.
function create() {
let value = 5;
return () => {
value *= 2;
console.log(value);
};
}
const fn = create();
fn(); // 10
fn(); // 20
fn(); // 40
Even though create() has already returned, its value isn’t garbage-collected
— the returned arrow function keeps it alive and mutates it across calls. This is
the basis for things like counters, memoization, and private state.
Analogy in Python: the exact same thing works, but reassigning the captured
variable needs the nonlocal keyword:
def create():
value = 5
def fn():
nonlocal value
value *= 2
print(value)
return fn
Analogy in Java: Java has closures via lambdas/anonymous classes, but the
captured local variable must be effectively final — you cannot reassign it.
To get mutable state like above you’d capture a field or a one-element array
(int[] value = {5};).
Arrow functions and this
Arrow functions aren’t just shorter syntax for function — they differ in one
crucial way: an arrow function does not have its own this. It captures
this lexically, from the scope where it was defined. A regular function,
by contrast, gets its this rebound depending on how it’s called.
This wrinkle is the source of a classic pre-ES6 workaround. Look at this code:
const person = {
name: "Alice",
greet() {
const self = this;
function fn() {
console.log(self.name);
}
fn();
}
};
person.greet(); // "Alice"
Why the self = this maneuver? Inside greet(), this correctly refers to
person (because it was called as person.greet()). But fn is a plain
function, not a method — so when it’s invoked as a bare fn(), JavaScript
rebinds its this to undefined (in strict mode) or the global object (in
sloppy mode). It is not person. So this.name inside fn would blow up or
print undefined.
The old fix: capture the outer this into an ordinary variable (self, often
called that or _this) while you still have the right value, then close over
that variable from the inner function. Closures behave predictably; this
doesn’t. So you sidestep this entirely.
Arrow functions make this trick obsolete. Because an arrow function inherits
this from its surrounding scope, you can drop self completely:
const person = {
name: "Alice",
greet() {
const fn = () => {
console.log(this.name); // `this` is still `person`
};
fn();
}
};
person.greet(); // "Alice"
The arrow function doesn’t get its own this, so this inside it is the same
this as in greet — which is person. No self, no bind, no surprises.
This is exactly why you’ll see arrows used for callbacks (setTimeout,
.map, event handlers, promises) — they keep this pointing at whatever the
enclosing method’s this was.
One consequence of the same rule: never use an arrow function as a method when you need
thisto be the object.greet: () => this.namewould capturethisfrom the module/global scope, notperson. Use thegreet() { ... }shorthand (or a regularfunction) for methods, and arrows for the callbacks inside them.
Analogy in Python: Python never had this footgun because methods take an
explicit self parameter — the binding is in the signature, not in how you call
the function. A nested function in Python simply closes over self like any
other variable:
class Person:
def __init__(self):
self.name = "Alice"
def greet(self):
def fn():
print(self.name) # plain closure over `self`
fn()
The JavaScript const self = this line is essentially a manual re-creation of
what Python gives you for free. This connects to [[blog-python-analogies]] —
JS’s this is the implicit, call-site-dependent version of Python’s explicit
self.
Generators
A generator is a function that can pause and resume, producing a sequence of
values lazily — much like Python’s generators. You declare one with function*
and emit values with yield. Calling the generator returns an iterator, and
each .next() runs until the next yield.
Here’s an infinite Fibonacci generator:
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const gen = fibonacci();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
Because it’s lazy, an infinite generator is fine — you just take as many values as you need. For example, the first 10 Fibonacci numbers:
function firstN(gen, n) {
const out = [];
for (const x of gen) {
out.push(x);
if (out.length === n) break;
}
return out;
}
console.log(firstN(fibonacci(), 10));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Objects
In JavaScript you don’t need an explicit class declaration to make an object. An object is just a bag of key-value pairs — essentially a hashmap with string (or symbol) keys. You write one with braces, and you can read, add, or delete keys at will:
const user = {
name: "Alice",
age: 30,
};
console.log(user.name); // "Alice" — dot access
console.log(user["age"]); // 30 — bracket access, like a hashmap
user.email = "a@x.com"; // add a key on the fly
delete user.age; // remove one
This is the same mental model as a Python dict — except the .key dot syntax
is first-class, and the keys are unordered string properties rather than
arbitrary hashable objects.
Functions are special objects. A function in JS is an object that happens to be callable — which means you can hang arbitrary properties off it, exactly like any other object:
function hello() {
console.log("hello");
}
console.log(typeof hello); // "function"
hello.myProperty = 42;
console.log(hello.myProperty); // 42
That last bit is perfectly valid JavaScript. The function still runs as a
function, but it also carries the myProperty field. This is why things like
memoization caches or static-style counters are sometimes stashed directly on
the function object.
Where do prototype and class fit? Because objects are just hashmaps,
JavaScript originally had no real classes — shared behaviour was wired up through
the prototype chain. You can think of a prototype as roughly the equivalent
of an interface (or shared base) in Java: it’s the object that supplies the
methods every instance inherits. Modern JavaScript adds the class keyword,
which gives you familiar class syntax — but under the hood it’s still just
prototypes and objects:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
const a = new Animal("Rex");
a.speak(); // "Rex makes a sound"
Analogy in Python: a plain JS object maps cleanly onto a Python dict, and
the class keyword onto a Python class. The difference is that Python keeps
“dict” and “class instance” as distinct concepts, whereas in JavaScript a class
instance is still an object/hashmap underneath — the line between data and
type is much blurrier.
Extending built-ins with .prototype
In JavaScript you can add new methods to a built-in class (like Array) by
attaching them to its prototype. Every array then has access to your method:
Array.prototype.second = function () {
return this[1];
};
const arr = [10, 20, 30];
console.log(arr.second()); // 20
This is real monkey-patching — you’re modifying the built-in type itself.
Note: you cannot do this directly in Python. Python forbids patching built-in types like
list(list.second = ...raises aTypeError). To get similar behaviour there you’d have to subclasslist:class MyList(list): def second(self): return self[1]
A word of caution: monkey-patching built-ins is powerful but considered risky — it affects every array in your program and can clash with future language features or other libraries. Use it sparingly.
setTimeout and clearTimeout
setTimeout(fn, t) schedules fn to run once after t milliseconds. It
returns a timer id that you can pass to clearTimeout(id) to cancel the
call before it fires.
A classic LeetCode problem (Cancellable Function) makes this concrete: schedule
a function, but hand back a cancel() that stops it if called in time.
var cancellable = function (fn, args, t) {
const timerId = setTimeout(() => { fn(...args); }, t);
// calling this before `t` ms have passed cancels the scheduled fn
return function () {
clearTimeout(timerId);
};
};
How it plays out: if the returned cancel() runs before t, clearTimeout
removes the pending call and fn never executes. If t elapses first, fn
runs and the later clearTimeout is a harmless no-op.
const fn = (x) => x * 5;
const args = [2], t = 20, cancelTimeMs = 50;
const cancel = cancellable(fn, args, t); // fn fires at t = 20ms
setTimeout(cancel, cancelTimeMs); // cancel at 50ms — too late, fn already ran
// fn(2) returns 10
Here cancelTimeMs (50) is greater than t (20), so fn fires first and the
cancel does nothing. If cancelTimeMs were less than t, the call would be
cancelled and fn would never run.
If the calling and execution flow is still confusing, here’s another way to frame it:
- You’re supposed to execute
fnafter a delay oft, and also return acancelfunction. - If
cancelis called, it should stop the pending timeout forfn. - So if
cancelis called afterfnhas already executed, nothing happens. But if it’s called before, we clear the timeout sofnnever runs at all.
The implementation recipe is simple: put a setTimeout (which will resolve into
executing fn) just before returning the cancel function, and put a
clearTimeout inside cancel.
setInterval and clearInterval
Where setTimeout fires once, setInterval(fn, t) calls fn repeatedly
every t milliseconds. It also returns a timer id, which you cancel with
clearInterval(id) to stop the repeats:
let count = 0;
const id = setInterval(() => {
count++;
console.log(count);
}, 1000); // logs 1, 2, 3, ... every second
// later, to stop it:
clearInterval(id);
The event loop: microtasks vs macrotasks
JavaScript is single-threaded, and async callbacks are scheduled onto two different queues:
- Microtask queue — Promise callbacks (
.then,await),queueMicrotask. - Macrotask queue —
setTimeout,setInterval, I/O events.
The event loop’s rule is simple but easy to get wrong:
- Run all microtasks (draining the queue completely).
- Run one macrotask.
- Repeat.
Crucially, the entire microtask queue is emptied before the next macrotask
runs — so a setTimeout(fn, 0) still waits behind every pending Promise
callback. Consider:
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
Promise.resolve()
.then(() => { console.log(3); })
.then(() => { console.log(4); });
console.log(5);
This prints 1, 5, 3, 4, 2:
1and5are synchronous — they run first, top to bottom.3and4are microtasks (Promise callbacks) — they drain next, in order.2is a macrotask (setTimeout) — even with a0ms delay, it runs last, after the microtask queue is empty.
(Python’s asyncio has a comparable distinction between ready callbacks and
scheduled-later ones, though the exact ordering rules differ.)
valueOf() and toString() — custom conversion
JavaScript objects can control how they behave when converted to a number or a string. This is something I ran into while solving LeetCode.
valueOf()is called when JavaScript needs a numeric value from an object (for example, with the+operator).toString()is called when it needs a string representation (for example, insideString(...)).
class Box {
constructor(x) {
this.x = x;
}
valueOf() { return this.x; }
toString() { return `Box(${this.x})`; }
}
const a = new Box(3);
const b = new Box(7);
console.log(a + b); // 10 -> a.valueOf() + b.valueOf()
console.log(String(a)); // "Box(3)" -> a.toString()
The LeetCode Array Wrapper problem uses both at once:
var ArrayWrapper = function (nums) {
this.nums = nums;
};
ArrayWrapper.prototype.valueOf = function () {
return this.nums.reduce((sum, x) => sum + x, 0);
};
ArrayWrapper.prototype.toString = function () {
return `[${this.nums.join(',')}]`;
};
const obj1 = new ArrayWrapper([1, 2]);
const obj2 = new ArrayWrapper([3, 4]);
obj1 + obj2; // 10 -> obj1.valueOf() + obj2.valueOf()
String(obj1); // "[1,2]" -> obj1.toString()
This lets operators like + and functions like String() work naturally with
your own classes — a feature without a direct equivalent in Python, Java, or Go.
Takeaways
- Dynamic typing like Python, C-style syntax like Java.
- Always use
===(and!==); ignore==. - Use
constby default,letwhen you must reassign. - Learn the array basics and the common string methods — they cover most day-to-day work.
- Always pass a comparator to
sort()for numbers — the default sorts as strings. - Generators (
function*/yield) give you lazy sequences. - You can monkey-patch built-ins via
.prototype, but do it sparingly. - Custom classes can hook into
+andString()viavalueOf()andtoString(). ...spreads in calls/literals and collects (rest) in parameter lists.
Tags: javascript, web, basics