04 · Functions & Scope¶
Function declarations¶
function add(a, b) {
return a + b;
}
function greet(name = "friend") { // default parameter
return `Hello, ${name}!`;
}
console.log(add(2, 3)); // 5
console.log(greet()); // Hello, friend!
console.log(greet("Ada")); // Hello, Ada!
Function expressions and arrow functions¶
// Function expression: an unnamed function assigned to a variable
const multiply = function (a, b) {
return a * b;
};
// Arrow function: shorter syntax, does NOT have its own `this`
const subtract = (a, b) => a - b;
// Single parameter: parens optional
const square = (x) => x * x;
// No parameters: empty parens required
const sayHi = () => "hi";
console.log(multiply(3, 4)); // 12
console.log(subtract(10, 4)); // 6
console.log(square(5)); // 25
Rest parameters and the arguments object¶
function total(...numbers) { // rest parameter — gathers args into a real array
return numbers.reduce((sum, n) => sum + n, 0);
}
function makeProfile(name, ...rest) {
return { name, extra: rest };
}
console.log(total(1, 2, 3, 4)); // 10
console.log(makeProfile("Ada", "London", 30));
// { name: 'Ada', extra: [ 'London', 30 ] }
Hoisting¶
console.log(hoisted()); // works — function declarations are fully hoisted
function hoisted() {
return "I run before my own line in the file";
}
console.log(typeof notYetDefined); // "undefined" — var is hoisted, but not its value
var notYetDefined = 5;
// console.log(letVar); // ReferenceError — let/const are hoisted but not initialized
let letVar = 10;
Function declarations are hoisted with their full body; var is hoisted but
only the declaration (not the assignment); let/const are hoisted into a
"temporal dead zone" where referencing them before their declaration throws.
Scope: block vs. function¶
function demo() {
if (true) {
let blockScoped = "only visible inside this block";
var functionScoped = "visible throughout demo()";
}
// console.log(blockScoped); // ReferenceError
console.log(functionScoped); // works — var ignores block boundaries
}
demo();
let x = "global";
function outer() {
let x = "enclosing";
function inner() {
let x = "local";
console.log(x); // local
}
inner();
console.log(x); // enclosing
}
outer();
console.log(x); // global
Each nested function can read variables from its enclosing scopes (this is the basis for closures, covered in depth in Level 2 · Module 1).
Functions are values¶
function squareFn(x) {
return x * x;
}
const operations = { square: squareFn };
console.log(operations.square(5)); // 25
// Higher-order function: takes a function as an argument
function applyTwice(fn, value) {
return fn(fn(value));
}
console.log(applyTwice(squareFn, 3)); // 81
How It Actually Works¶
Every time a function is called, V8 pushes a new stack frame holding its local
variables, arguments, and a reference to where execution should resume when it returns.
But scope resolution — how x inside a nested function finds the right x — doesn't
walk the call stack at all. It walks a separate structure built at parse time called
the scope chain: each function literal captures a pointer to the lexical
environment it was defined in, not the one it's called from. That's why a function
defined at the top level and called deep inside another function still only sees
top-level variables, never the caller's locals — scope is about where code is written,
not where it's invoked (this is "lexical scoping").
Function hoisting differs sharply by declaration form. A function foo() {}
declaration is fully hoisted — both the name and the function body are available
before the line runs, because V8's parser does a pre-pass that registers named function
declarations in the enclosing scope before executing any statements. A function
expression (const foo = function() {}) only hoists the const binding (in the TDZ,
unusable), not the assignment — so calling it before the line throws. arguments is
its own quirk: for non-arrow functions, V8 lazily materializes an arguments object
backed by the actual argument values on the stack; arrow functions have no arguments
binding of their own at all, so referencing arguments inside one walks the scope
chain up to the nearest enclosing non-arrow function.
🔀 See this in another language¶
Exercise¶
Write a function summarize(...values) that returns an object with the min,
max, and average of values, rounding the average to 2 decimal places with
.toFixed(2).