The three building blocks you'll use in every JavaScript program you ever write.
A function is a reusable block of code. Understanding functions deeply β declarations vs expressions, closures, and how this works with arrows β unlocks everything else in JavaScript.
They look similar but behave differently in one critical way: hoisting. The JavaScript engine processes declarations before any code runs β expressions are not.
// ββ FUNCTION DECLARATION βββββββββββββββββββββββββββββ // Hoisted β can be called BEFORE it appears in the file console.log(greet("Alice")); // β Works! "Hello, Alice" function greet(name) { return "Hello, " + name; } // ββ FUNCTION EXPRESSION ββββββββββββββββββββββββββββββ // The variable "sayBye" exists, but holds undefined until this line // console.log(sayBye("Bob")); β β TypeError: sayBye is not a function const sayBye = function(name) { return "Goodbye, " + name; }; console.log(sayBye("Bob")); // β "Goodbye, Bob" // ββ NAMED FUNCTION EXPRESSION ββββββββββββββββββββββββ // Useful for self-reference and better stack traces const factorial = function fact(n) { return n <= 1 ? 1 : n * fact(n - 1); // can call itself by name "fact" }; console.log(factorial(5)); // 120
| Feature | Declaration | Expression |
|---|---|---|
| Syntax | function name() {} | const name = function() {} |
| Hoisted? | Yes β callable anywhere in scope | No β only usable after assignment |
| Has a name? | Always named | Can be anonymous or named |
| Best for | Top-level utility functions | Callbacks, conditionally assigned functions |
Arrow functions are a compact shorthand β but there is a crucial semantic difference: they do not have their own this. They inherit this from the surrounding (lexical) scope, which makes them ideal for callbacks but wrong for object methods.
// Single param β no parens needed const double = n => n * 2; // Multiple params β parens required const add = (a, b) => a + b; // No params β empty parens const greet = () => "Hello!"; // Multi-line β needs { } and explicit return const clamp = (n, min, max) => { if (n < min) return min; if (n > max) return max; return n; }; // Returning an object literal β wrap in ( ) const makePoint = (x, y) => ({ x, y });
const timer = { count: 0, // β Regular function loses "this" inside setTimeout startWrong() { setTimeout(function() { this.count++; // β "this" is undefined here }, 100); }, // β Arrow function inherits "this" from startCorrect startCorrect() { setTimeout(() => { this.count++; // β "this" = timer object console.log(this.count); // 1 }, 100); } }; timer.startCorrect();
| Feature | Regular function | Arrow function |
|---|---|---|
Own this | Yes β depends on call site | No β inherits from outer scope |
| Can be constructor? | Yes β new Fn() works | No β throws TypeError |
arguments object | Yes | No β use rest params ...args |
| Best for | Object methods, constructors | Callbacks, array methods, closures |
A closure is created when an inner function retains access to the variables of its outer (enclosing) function β even after the outer function has finished executing.
Think of it like a backpack: when the inner function is born, it packs all the variables it can see and carries them forever.
function makeCounter() { let count = 0; // β lives in the closure, private! return function() { count++; return count; }; } const counter = makeCounter(); // makeCounter() is DONE β but count lives on console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 // Each makeCounter() call creates a SEPARATE closure const counter2 = makeCounter(); console.log(counter2()); // 1 β its own independent count console.log(counter()); // 4 β counter continues unaffected
// A factory that produces specialized functions function makeMultiplier(factor) { return n => n * factor; // "factor" is closed over } const double = makeMultiplier(2); const triple = makeMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 console.log(double(100)); // 200 // Greeting factory const makeGreeter = greeting => name => `${greeting}, ${name}!`; const hello = makeGreeter("Hello"); const hej = makeGreeter("Hej"); console.log(hello("Anna")); // "Hello, Anna!" console.log(hej("Tomek")); // "Hej, Tomek!"
Why closures matter: They are the foundation of module patterns, React hooks, event handlers, memoization, and virtually every advanced JavaScript pattern. Master closures β master JavaScript.
Objects group related data and behaviour. Beyond basics, you need to understand this, safe copying, and how to iterate over keys and values.
const person = { name: "Alice", age: 25, city: "Warsaw" }; person.job = "developer"; // add person.age = 26; // update delete person.city; // remove // Bracket notation β useful when key is in a variable const key = "name"; console.log(person[key]); // "Alice"
Inside a method, this refers to the object the method belongs to. Its value is determined by how the function is called β not where it is defined.
const dog = { name: "Burek", breed: "Husky", age: 3, describe() { return `${this.name} is a ${this.breed}`; }, birthday() { this.age++; return `Happy birthday ${this.name}! Now ${this.age}.`; } }; console.log(dog.describe()); // "Burek is a Husky" console.log(dog.birthday()); // "Happy birthday Burek! Now 4." // β The "this" trap β loses context when detached const fn = dog.describe; // detached β no object prefix // fn() β TypeError in strict mode: this is undefined // Fix 1: .bind() creates a new permanently bound function const bound = dog.describe.bind(dog); console.log(bound()); // β "Burek is a Husky" // Fix 2: .call() / .apply() β invoke with explicit this console.log(dog.describe.call(dog)); // β same result
Arrow functions and this: Never use an arrow function as an object method if you need this. Arrow functions don't have their own this β they inherit it from the outer (module) scope, not from the object.
Objects aren't iterable by default. These three static methods turn an object's contents into arrays so you can use map, filter, and forEach on them.
const scores = { Anna: 88, Tomek: 72, Kasia: 95, Marek: 61 }; // Keys only console.log(Object.keys(scores)); // ["Anna", "Tomek", "Kasia", "Marek"] // Values only console.log(Object.values(scores)); // [88, 72, 95, 61] // Key-value pairs β great for iteration Object.entries(scores).forEach(([name, score]) => { console.log(`${name}: ${score}`); }); // Average score using Object.values() const vals = Object.values(scores); const avg = vals.reduce((s, v) => s + v, 0) / vals.length; console.log("Average:", avg.toFixed(1)); // 79.0 // Rebuild object with transformed values (Object.fromEntries) const boosted = Object.fromEntries( Object.entries(scores).map(([k, v]) => [k, v + 5]) ); console.log(boosted); // { Anna: 93, Tomek: 77, Kasia: 100, Marek: 66 }
Objects are stored by reference. Assigning const b = a does not copy β both names point to the same data. Modifying one modifies the other.
// β NOT a copy β same reference const original = { name: "Alice", age: 25 }; const oops = original; oops.age = 99; console.log(original.age); // 99 β original was mutated! // β SHALLOW COPY β spread operator const copy1 = { ...original }; copy1.age = 30; console.log(original.age); // 99 β original safe β // β SHALLOW COPY β Object.assign const copy2 = Object.assign({}, original); // β Shallow only goes ONE level deep! const user = { name: "Bob", address: { city: "Warsaw" } }; const shallow = { ...user }; shallow.address.city = "KrakΓ³w"; console.log(user.address.city); // "KrakΓ³w" β still shared! // β DEEP COPY β JSON round-trip (for plain data) const deep = JSON.parse(JSON.stringify(user)); deep.address.city = "GdaΕsk"; console.log(user.address.city); // "KrakΓ³w" β safe β // β DEEP COPY β structuredClone (modern, handles more types) const deep2 = structuredClone(user);
| Method | Nested objects? | Use when |
|---|---|---|
const b = a | Not a copy at all | Never for copying |
{...a} spread | Shallow β nested shared | Flat objects only |
Object.assign({}, a) | Shallow β nested shared | Flat objects, older code |
JSON.parse(JSON.stringify(a)) | Deep β fully independent | Nested plain data, no functions/dates |
structuredClone(a) | Deep β handles more types | Modern environments, best practice |
An ordered list. Each item sits at a numbered index starting from zero. Arrays come with powerful built-in methods β map, filter, reduce and more β that accept functions as arguments.
const nums = [1, 2, 3, 4, 5]; const doubled = nums.map(n => n * 2); // [2,4,6,8,10] const evens = nums.filter(n => n % 2 === 0); // [2,4] const total = nums.reduce((s, n) => s + n, 0); // 15
Real code combines closures, this, objects, and arrays in every file. Here is a task manager that uses all three at once.
10 questions covering functions, closures, this, object copying, iteration, and arrays.
add5 closes over x = 5. When called with 3, it returns 3 + 5 = 8. The closure keeps x alive.makeAdder(5) returns a function that remembers x = 5. Calling add5(3) gives 3 + 5 = 8.makeCounter() creates a brand new, independent closure with its own count. They never share state.makeCounter() call creates a separate closure with its own count starting from 0. c2() gives 1.this refer to inside a regular object method when called normally?dog.bark(), inside bark, this is the dog object β the thing before the dot.obj.method()), this is that object β the thing before the dot.this from the enclosing scope (usually the module level), not from the object β so this.property would be undefined.this. As an object method, this would refer to the outer scope (module/window), not the object.original.age be after this code?const copy = original doesn't copy the object β both variables point to the same memory location. Mutating one mutates both.= doesn't copy objects β it copies the reference. Both copy and original point to the same object, so age becomes 99 everywhere.Object.assign are shallow β nested objects are still shared. The JSON round-trip produces a completely independent deep copy.structuredClone) produces a true deep copy. Spread and Object.assign are shallow β nested objects remain shared.Object.entries(obj) return?Object.entries() returns [["key1", val1], ["key2", val2], ...], which you can destructure as ([key, value]) in callbacks.Object.entries() returns [key, value] pairs. Use Object.keys() for just keys, Object.values() for just values.map() returns a new array of the same length where each element has been transformed by your callback. The original array is never changed.map(). forEach loops without returning a value, filter keeps/removes items, and find returns the first match.