Cheat SheetsJavaScript & TypeScriptLanguage Core

Language Core — Cheat Sheet

JavaScript & TypeScript · 8 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Language Core
JavaScript & TypeScript8 topicsQuick revision reference
1

Values & Types — What JavaScript Actually Has

JavaScript has seven primitive types and one object type. Everything that is not a primitive — arrays, functions, dates, regexes — is an object, and that single fact explains most surprising behaviour in the language.

  • Seven primitives — string, number, boolean, null, undefined, symbol, bigint — and everything else is an object
  • Every number is a 64-bit float; there is no int, so 0.1 + 0.2 !== 0.3 and money belongs in integer paise
  • typeof null returns "object" — a 1995 bug kept for compatibility; use value === null instead
  • Primitives copy by value, objects copy by reference — the single most common source of "why did that change?"
  • Spread copies one level deep; structuredClone() copies the whole tree
typeof, and where it lies to you
typeof 'hello'        // 'string'
typeof 42             // 'number'   — always a 64-bit float
typeof true           // 'boolean'
typeof undefined      // 'undefined'
typeof Symbol('id')   // 'symbol'
typeof 9007199254740993n  // 'bigint'

typeof null           // 'object'  <-- the famous bug, from 1995
typeof []             // 'object'  — arrays are objects
typeof function () {} // 'function' — objects you can call

// Reliable checks:
Array.isArray([])            // true
value === null               // the only safe null check
Number.isInteger(42)         // true
2

Equality & Coercion — == vs === and Why It Matters

JavaScript will convert types to make a comparison succeed. == does this conversion, === does not, and knowing the handful of rules turns "weird" behaviour into predictable behaviour.

  • === compares type and value with no conversion; == converts first — default to === always
  • value == null is the one useful loose comparison: true for null and undefined, nothing else
  • Exactly eight falsy values: false, 0, -0, 0n, "", null, undefined, NaN — empty arrays and objects are truthy
  • ?? falls back only on null/undefined, || falls back on any falsy value — using || for defaults breaks on 0 and ""
  • ?. short-circuits to undefined instead of throwing, which is what makes reading nested API responses safe
Loose equality, in the cases that matter
// Strict: no conversion, compares type then value
1 === '1'          // false
null === undefined // false

// Loose: converts first
1 == '1'           // true  — string becomes number
0 == false         // true  — boolean becomes number
'' == false        // true  — both become 0
null == undefined  // true  — special-cased in the spec
null == 0          // false — null only equals undefined

// The one genuinely useful ==
if (value == null) {
  // true for BOTH null and undefined, nothing else
}
// same as: value === null || value === undefined
3

Scope & Hoisting — let, const, var and the TDZ

var is function-scoped and hoisted as undefined; let and const are block-scoped and hoisted into a temporal dead zone that throws if you touch them early. Use const by default, let when you must reassign, var never.

  • var is function-scoped and leaks out of blocks; let and const are block-scoped
  • All declarations hoist, but var initialises to undefined while let/const sit in a temporal dead zone that throws
  • let creates a fresh binding per loop iteration — this is why closures in loops work with let and not var
  • const prevents reassignment, not mutation: const objects and arrays can still be changed
  • Default to const, use let when reassigning, and treat var as legacy
The loop bug that made let necessary
// var — one binding, shared by every callback
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 3, 3, 3  — the loop finished before any callback ran

// let — a new binding each iteration
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)
}
// 0, 1, 2

// var also leaks out of blocks:
if (true) { var leaked = 'yes'; let contained = 'no' }
console.log(leaked)     // 'yes'
console.log(contained)  // ReferenceError
4

Closures — Functions That Remember

A closure is a function together with the scope it was created in. It keeps that scope alive after the outer function has returned, which is how JavaScript does private state, factories, and almost every callback pattern.

  • A closure is a function plus the scope it was defined in, kept alive after the outer function returns
  • Each call to the outer function creates an independent scope — two counters do not share state
  • Closures capture the variable, not its value at creation time; this is what makes stale closures possible
  • Private state, memoisation, debounce, throttle and React hooks are all closures underneath
  • The classic var-in-a-loop bug is a closure capturing one shared binding instead of three
Private state, built from scope
function counter() {
  let count = 0                    // lives on after counter() returns
  return {
    increment: () => ++count,
    value: () => count,
  }
}

const a = counter()
const b = counter()              // independent scope
a.increment(); a.increment()
a.value()      // 2
b.value()      // 0

// count is unreachable from outside — genuine private state,
// which JavaScript had no other way to express until #private fields.
5

`this` — Four Rules and an Arrow Function

In JavaScript `this` is decided by how a function is called, not where it is defined — except in arrow functions, which have no `this` of their own and inherit it from the surrounding scope.

  • `this` is bound by how a function is called, not where it is defined
  • Priority: new > explicit (call/apply/bind) > implicit (object before the dot) > default
  • Extracting a method from an object loses its binding — the commonest `this` bug in real code
  • Arrow functions have no `this`; they inherit it lexically, which is why they are right for callbacks
  • Never use an arrow function as an object method that needs `this` — it will point at the enclosing scope
How `this` is chosen, in order
// 1. new binding — this is the newly created object
function User(name) { this.name = name }
const u = new User('Asha')       // this === u

// 2. explicit binding — call, apply, bind
function greet() { return this.name }
greet.call({ name: 'Ravi' })     // 'Ravi'
const bound = greet.bind(user)   // permanently bound

// 3. implicit binding — the object left of the dot
const obj = { name: 'Meera', greet }
obj.greet()                      // 'Meera'

// 4. default — undefined in strict mode / modules,
//    globalThis in sloppy mode
const loose = obj.greet
loose()                          // undefined (strict) — the classic bug
6

Prototypes — How Inheritance Actually Works

JavaScript objects delegate to other objects through a prototype chain. Classes are syntax over this mechanism, not a separate system — which is why understanding the chain explains behaviour classes cannot.

  • Property lookup walks the prototype chain; assignment always writes to the object itself
  • class is syntax over prototypes — extends sets the chain, super walks up it
  • Methods live once on the prototype and are shared by every instance, not copied
  • Object.hasOwn() distinguishes own properties from inherited ones — important when iterating
  • #private fields are enforced by the engine, which is what closures were being used for before
Lookup walks the chain; assignment does not
const animal = { breathes: true, describe() { return 'a living thing' } }
const dog = Object.create(animal)
dog.barks = true

dog.barks        // true  — own property
dog.breathes     // true  — found on animal via the chain
dog.describe()   // 'a living thing'
dog.toString()   // inherited from Object.prototype

Object.getPrototypeOf(dog) === animal        // true
Object.hasOwn(dog, 'breathes')               // false — inherited

// The full chain for a plain array:
// [] -> Array.prototype -> Object.prototype -> null
7

Functions — Declarations, Expressions, Arrows, Defaults

Functions are values: assignable, passable, returnable. The forms differ in hoisting, in whether they have `this` and `arguments`, and in whether they can be used as constructors.

  • Function declarations hoist and are callable before their definition; expressions and arrows are not
  • Arrow functions have no own this, no arguments object, and cannot be called with new
  • Rest parameters give a real array; the old arguments object is array-like but not an array
  • A destructured parameter with `= {}` lets the caller omit the options object entirely
  • Functions are values — passing and returning them is ordinary JavaScript, not an advanced pattern
Three forms, three behaviours
// Declaration — hoisted, callable before its definition
function add(a, b) { return a + b }

// Expression — not hoisted; the variable follows normal TDZ rules
const sub = function (a, b) { return a - b }

// Arrow — no own this/arguments, cannot be newed
const mul = (a, b) => a * b
const square = n => n * n              // one param, parens optional
const make = () => ({ ok: true })      // object literal needs parens

// Arrows cannot be constructors:
const Bad = () => {}
new Bad()          // TypeError: Bad is not a constructor
8

Modules — import, export and the CommonJS Split

ES Modules are the standard: static import/export, resolved before execution, with live bindings. CommonJS require() is the older Node system, and the two do not mix cleanly — which is the source of most "cannot use import outside a module" errors.

  • ES Modules are static — imports are resolved before execution, which enables tree-shaking
  • Prefer named exports: they are explicit, checked, and refactor better than defaults
  • Imported bindings are live views of the export, not copies taken at import time
  • Node picks ESM or CommonJS by file extension and package.json "type" — this is what causes ERR_REQUIRE_ESM
  • ESM can import CommonJS but not the reverse; use dynamic import() to load a module at runtime
Named exports vs default
// utils.js — named exports
export const formatPrice = paise => `₹${(paise / 100).toFixed(2)}`
export function slugify(s) { ... }
export { internalName as publicName }

// importing
import { formatPrice, slugify } from './utils.js'
import { formatPrice as money } from './utils.js'
import * as utils from './utils.js'

// default — one per module, name chosen by the importer
export default function Button(props) { ... }
import Button from './Button.jsx'
import AnythingAtAll from './Button.jsx'   // legal, and a downside
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/javascript