JavaScript Language Fundamentals

The core of JavaScript: values vs references, equality and coercion, let/const scope, functions, objects and prototypes, classes, modules, array methods and error handling.

Beginner⏱ 7 min readLesson 2 of 13#languages#javascript#prototypes#modules#oop

The big idea

JavaScript has only a few building blocks: primitive values, objects, and functions (which are also objects). Almost every "weird" behaviour comes from one question: am I holding the thing itself, or a reference to it?

Think of a house and its address. A primitive (a number, a string) is like writing a number on a sticky note: copy the note and you have two independent notes. An object is a house: copying the variable only copies the address, and both variables point at the same house.

Primitives are copied, objects are shared by reference

📘 This lesson covers the language itself. The event loop, closures and this are explained visually in Frontend → JavaScript Essentials; async code on the server is in the next lesson.

Values and types

TypeExamplesKind
number42, 3.14, NaN, Infinityprimitive
bigint9007199254740993nprimitive
string"hi", `Hi ${name}`primitive
booleantrue, falseprimitive
undefineda declared variable with no valueprimitive
null"intentionally empty"primitive
symbolSymbol("id")primitive
object{}, [], functions, Date, Mapreference
let a = 5;
let b = a;      // copies the value
b++;
console.log(a); // 5

const user = { name: "Ana" };
const same = user;    // copies the reference
same.name = "Bo";
console.log(user.name); // "Bo" — same object

const copy = { ...user };           // shallow copy
const deep = structuredClone(user); // deep copy (nested objects too)

⚠️ const means the variable can't be reassigned, not that the object is frozen. const user = {} still allows user.name = "x". Use Object.freeze for a (shallow) read-only object.

Equality and coercion

0 == "0";            // true  — loose equality converts types
0 == "";             // true
"0" == "";           // false  😵 not transitive
null == undefined;   // true
NaN === NaN;         // false — use Number.isNaN(x)
[1] === [1];         // false — two different objects

Rule: always use === and !==. The only common exception is x == null, which checks for both null and undefined.

Truthy and falsy

The falsy values are: false, 0, -0, 0n, "", null, undefined, NaN. Everything else is truthy, including "0", [] and {}.

const port = config.port || 3000; // ❌ a port of 0 becomes 3000
const port2 = config.port ?? 3000; // ✅ only null/undefined fall back
const city = user?.address?.city;  // optional chaining: undefined instead of a crash

Variables and scope

varletconst
ScopeFunctionBlock { }Block { }
Reassign✅✅❌
Use before declarationundefined (hoisted)❌ ReferenceError (TDZ)❌ ReferenceError (TDZ)
Use it?AvoidWhen the value changesDefault
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0 1 2 — new j per loop

Functions

function add(a, b) { return a + b; }          // declaration (hoisted)
const sub = function (a, b) { return a - b; }; // expression
const mul = (a, b) => a * b;                   // arrow: short, no own `this`

function greet(name = "friend", ...rest) {     // default + rest parameters
  return `Hello ${name}, and ${rest.length} others`;
}

const { id, email: mail = "n/a" } = user;      // object destructuring with rename + default
const [first, , third] = [10, 20, 30];         // array destructuring

Functions are values: you can pass them, return them and store them. A function that takes or returns a function is a higher-order function.

const once = (fn) => {
  let done = false, result;
  return (...args) => (done ? result : ((done = true), (result = fn(...args))));
};
const init = once(() => console.log("connecting…"));
init(); init(); // logs once

Objects, prototypes and classes

Every object has a hidden link to a prototype. When a property isn't found on the object, JavaScript walks up the chain.

Drawing diagram…

class is clean syntax over that same prototype system:

class Animal {
  #energy = 10;                  // truly private field
  constructor(name) { this.name = name; }
  eat() { this.#energy++; return this; }
  get energy() { return this.#energy; }
  static create(name) { return new this(name); }
}

class Dog extends Animal {
  bark() { return `${this.name}: woof`; }
}

const rex = Dog.create("Rex");
rex.eat().bark();                  // "Rex: woof"
Object.getPrototypeOf(rex) === Dog.prototype; // true

Is JavaScript object-oriented?

Yes, and also functional. It supports the four OOP pillars:

PillarIn JavaScript
Encapsulation#private fields, closures, modules
AbstractionExpose a small public API, hide details
InheritancePrototypes / extends (prefer composition when in doubt)
PolymorphismAny object with the right method works ("duck typing")

Arrays and collections

const orders = [
  { id: 1, total: 40, paid: true },
  { id: 2, total: 15, paid: false },
  { id: 3, total: 90, paid: true },
];

const paidTotal = orders.filter((o) => o.paid).map((o) => o.total).reduce((a, b) => a + b, 0); // 130
const big = orders.find((o) => o.total > 50);      // { id: 3, … }
const anyUnpaid = orders.some((o) => !o.paid);     // true
const byId = new Map(orders.map((o) => [o.id, o])); // fast lookup by key
const sorted = orders.toSorted((a, b) => b.total - a.total); // non-mutating (ES2023)
NeedUse
Ordered listArray
Key → value with any key type, frequent add/removeMap
Unique valuesSet
Fixed record shapeplain object {}

⚠️ sort(), reverse() and splice() mutate the array. toSorted(), toReversed() and toSpliced() return a copy.

Modules

Modern JavaScript uses ES modules (import/export). Node.js also still supports the older CommonJS (require).

// math.js
export const PI = 3.14159;
export default function area(r) { return PI * r * r; }

// app.js
import area, { PI } from "./math.js";
const { readFile } = await import("node:fs/promises"); // dynamic import
ES modulesCommonJS
Syntaximport / exportrequire / module.exports
LoadingStatic, analysed before running (tree-shaking)Dynamic, at runtime
Top-level await✅❌
Enable in Node"type": "module" or .mjsdefault / .cjs

Errors

class NotFoundError extends Error {
  constructor(resource, id) {
    super(`${resource} ${id} not found`);
    this.name = "NotFoundError";
  }
}

try {
  const user = findUser(42) ?? (() => { throw new NotFoundError("User", 42); })();
} catch (err) {
  if (err instanceof NotFoundError) console.warn(err.message);
  else throw err; // don't swallow errors you don't understand
} finally {
  closeConnection();
}

Throw Error objects (not strings) so you keep a stack trace, and use cause to wrap lower-level errors: throw new Error("Save failed", { cause: err }).

Quick check

What does typeof null return, and why?

"object". It's a bug from the first version of JavaScript that can never be fixed without breaking the web. Check for null with value === null.

Why does [10, 9, 1].sort() return [1, 10, 9]?

Without a compare function, sort converts items to strings and sorts them alphabetically. Use sort((a, b) => a - b) for numbers.

Key takeaways

  • Primitives are copied by value; objects, arrays and functions are shared by reference.
  • Use ===, const by default, let when needed, never var. Use ?? and ?. for missing values.
  • Classes are syntax over prototypes; #private fields give real encapsulation.
  • Prefer non-mutating array methods, Map/Set for lookups and uniqueness, and ES modules for new code.
  • Throw real Error objects, rethrow what you can't handle, and wrap with cause.