Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
B4

Functions (first half)

A function is a value first, a block of code second.

Three ways to write one

Same function, three spellings — and the differences between them aren't cosmetic. They change when the function exists and what it's allowed to do.

// 1. Declaration — a named statement
function add(a, b) {
  return a + b;
}

// 2. Expression — a value, happens to be a function, assigned like any other
const subtract = function (a, b) {
  return a - b;
};

// 3. Arrow — an expression too, but lighter and with no own "this"
const multiply = (a, b) => a * b;
Declaration Expression Arrow
Hoisted, fully usable early? yes no no
Has its own this yes yes no — inherits it
Has its own arguments yes yes no — inherits it
Works as a constructor (new) yes yes no
console.log(declared());     // what happens?
function declared() { return "I work before my own definition"; }

console.log(typeof viaVar);  // what happens?
viaVar();
var viaVar = function () { return "x"; };

declared() works — function declarations are hoisted completely: name and body, both ready before line 1 runs. viaVar is different: var hoists the name (pre-filled with undefined) but not the function it's later assigned. So typeof viaVar is "undefined", and calling it throws TypeError: viaVar is not a function — you're calling undefined(). Swap var for const and it's worse: a ReferenceError, because the name sits in the Temporal Dead Zone until its line runs.

Rule A function you need to call before its own line in the file must be a function declaration. An expression or arrow only exists from its own line onward — same as any other const.

Arrow functions — the concise cousin

Arrows drop the function keyword and, with exactly one parameter, the parentheses too. A one-expression body skips return entirely — the expression's value is the return value.

const square = n => n * n;                  // implicit return
const clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
const noisy = n => {                          // block body needs an explicit return
  console.log("squaring", n);
  return n * n;
};
⚠ Returning an object literal from a one-liner const make = () => { name: "a" }; does not return an object — the { is read as the start of a block body, and name: "a" is parsed as a label, not a key. Wrap it in parens: () => ({ name: "a" }).

The bigger difference isn't syntax, it's this and arguments. An arrow doesn't create either — it reads through to whatever function it's lexically written inside:

function outer(a, b) {
  const arrow = (x, y, z) => arguments.length;
  return arrow(1, 2, 3);
}
console.log(outer(10, 20));   // what happens?

2, not 3. The arrow's arguments isn't its own — it's outer's, which was called with two values. The same logic governs this inside an arrow, and it's the whole reason arrows became the default choice for callbacks: no more const self = this; workaround. The full mechanics of this get their own chapter later — for now, remember arrows borrow it rather than own it.

Parameters, arguments, defaults

A parameter is the name in the function's own definition. An argument is the actual value handed over at the call site. Extra arguments are silently dropped; missing ones become undefined — unless a default says otherwise.

function greet(name, greeting = "Hello") {
  return greeting + ", " + name + "!";
}
greet("Ana");              // "Hello, Ana!"
greet("Ana", "Hi");        // "Hi, Ana!"
greet("Ana", undefined);   // "Hello, Ana!" — undefined also triggers the default

Defaults aren't static values baked in once — they're expressions, evaluated fresh on every call that needs them, and they can reference earlier parameters:

function withDefault(a, b = a + 1) {
  return b;
}
console.log(withDefault(5));       // what happens?
console.log(withDefault(5, 100));  // what happens?

6, then 100 — the default only runs when the argument is missing (or explicitly undefined); supply anything else and the default expression never executes at all.

⚠ Defaults can only look left a can default from an earlier parameter, but not a later one — function f(a = b, b = 1) {} throws ReferenceError: Cannot access 'b' before initialization the moment a's default needs to run, because b is still in its own Temporal Dead Zone at that point.

Rest parameters — the modern arguments

...args in a parameter list collects every remaining argument into a real array — unlike the old arguments object, which looks array-ish but has no map/filter/reduce of its own. Arrows don't get arguments at all, so rest params are their only option for "however many args you send me."

function sum(...nums) {
  return nums.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4);   // 10

function logAll(label, ...rest) {   // rest must be LAST
  console.log(label, rest);
}

Return — and the newline that eats it

No return statement, or a bare return;, both give back undefined. That's not the interesting part — this is:

function makeUser() {
  return
  { name: "Ana" };
}
console.log(makeUser());   // what happens?

undefined — not the object. Automatic Semicolon Insertion sees a line break right after return and quietly inserts a semicolon there, turning it into return; followed by an unreachable, orphaned block. The object literal on the next line never has a chance to be returned.

Rule Never put a line break between return and the value. If the value is long, wrap it in parens and break inside them: return (
  { name: "Ana" }
);

Scope basics

Every function creates its own scope — variables declared inside are invisible outside. Nested functions can see everything in their parent's scope (that's a closure, coming properly in a later chapter); the reverse is never true.

function outer() {
  let secret = 42;
  function inner() {
    console.log(secret);   // fine — inner can see outer's variables
  }
  inner();
}
console.log(typeof secret);   // "undefined" — outer can't be seen from here

Inside a function, let/const are still block-scoped exactly like in the last chapter — an if or a for loop makes its own little scope even inside a function body. var ignores those inner blocks completely and belongs to the whole function.

Hoisting, one level up

The Temporal Dead Zone from the last two chapters applies exactly the same way inside a function body — the only new piece here is that parameters are hoisted too, as already-initialized bindings, so a default value can reference an earlier parameter without a TDZ error (as shown above), and the function body can shadow a parameter name with its own let:

function shadow(x) {
  console.log(x);     // the parameter's value
  let x2 = x;          // (renamed here only to keep the example simple —
  //  redeclaring "x" itself with let in the same scope is a SyntaxError)
}

That's a deliberate restriction: a parameter and a let/const of the same name can't coexist in one function scope — JS won't let you accidentally shadow an argument you probably still needed.

Practice this layer

Opens in the editor — write it, run it, and check it against real tests.

Find the line break that ate the return value3 tests · beginnerSum any number of arguments4 tests · beginner
←previousOperators & flow↑ CovernextObjects & arrays (first half)→