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.
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;
};
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.
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.
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.
Opens in the editor — write it, run it, and check it against real tests.