JavaScript Functions, Scope and Closures

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

Node.js 22 LTS

Functions are first-class in JavaScript: assign them to variables, pass them around, return them. Understanding them means understanding the scope chain and closures.

Three ways to define one

// 1. Declaration: hoisted, callable before its definition
function add(a, b) {
  return a + b;
}

// 2. Expression: assigned to a variable, not hoisted
const multiply = function (a, b) {
  return a * b;
};

// 3. Arrow function: shorter, no own this
const divide = (a, b) => a / b;

The differences that matter:

  • Declarations are hoisted; expressions and arrows are not.
  • Arrow functions inherit this from the surrounding scope; regular functions get this from how they are called.
  • Use function when you need recursion by name, your own this, or new; use arrows for callbacks and short transforms.

Arrow functions also have no arguments object and cannot be used as constructors.

Parameters

function greet(name = "anonymous", punctuation = "!") {
  return `Hello, ${name}${punctuation}`;
}

greet();          // "Hello, anonymous!"
greet("ann");     // "Hello, ann!"

Defaults apply only when the argument is undefined:

greet(null);      // "Hello, null!"

Rest parameters collect the remaining arguments into a real array:

function sum(...nums) {
  return nums.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);    // 6

The rest parameter must be last. arguments still exists in regular functions, but it is array-like and has no array methods; use ... in new code.

When a function takes several related options, accept an object and destructure it:

function createUser({ name, role = "user", active = true }) {
  return { name, role, active };
}

createUser({ name: "ann", role: "admin" });
// { name: "ann", role: "admin", active: true }

The caller no longer has to remember parameter order.

Return values

A function without return returns undefined:

function log(msg) {
  console.log(msg);
}

const result = log("hi");   // prints hi, result is undefined

Arrow functions returning an object literal need parentheses, otherwise the braces are read as a body:

const makeUser = (name) => ({ name, createdAt: Date.now() });

A return on its own line followed by a value is a classic bug:

function bad() {
  return
    { ok: true };   // returns undefined; ASI inserted a semicolon
}

Scope and the scope chain

There are three levels: global, function, and block (let / const inside {}).

Lookups walk outward, which is what “scope chain” means:

const global = "global";

function outer() {
  const mid = "mid";

  function inner() {
    const local = "local";
    console.log(local, mid, global);   // local mid global
  }

  inner();
}

Inner scopes can read outer variables; outer scopes cannot read inner ones:

function outer() {
  const secret = "hidden";
}
console.log(secret);   // ReferenceError: secret is not defined

Closures

A function keeps a reference to the scope where it was defined, even after that scope has returned:

function makeCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}

const next = makeCounter();
next();   // 1
next();   // 2
next();   // 3

count survives because the returned function still references it. Each call to makeCounter() creates an independent count, which is the basis of private state in JavaScript:

function requireMin(n) {
  return (value) => value.length >= n;
}

const longEnough = requireMin(8);
["abc", "abcdefghij"].filter(longEnough);   // ["abcdefghij"]

The var-in-a-loop trap

const fns = [];
for (var i = 0; i < 3; i++) {
  fns.push(() => i);
}
fns.forEach((fn) => console.log(fn()));   // 3 3 3

var is function scoped, so all three closures share one i, which is 3 when the loop ends.

With let, each iteration gets its own binding:

const fns = [];
for (let i = 0; i < 3; i++) {
  fns.push(() => i);
}
fns.forEach((fn) => console.log(fn()));   // 0 1 2

This is the most concrete reason to stop using var.

this in one paragraph

For a regular function, this is decided at call time:

const obj = {
  value: 42,
  getValue() {
    return this.value;
  },
};

obj.getValue();          // 42 — this is obj

const detached = obj.getValue;
detached();              // TypeError: this is undefined in strict mode

The method was extracted from the object, so there is no receiver. Arrow functions avoid the problem by inheriting this:

const timer = {
  seconds: 0,
  start() {
    setInterval(() => {
      this.seconds += 1;    // still the timer object
    }, 1000);
  },
};

Use object method syntax when this should point at the object; use arrows for callbacks that should keep the enclosing this.

Common mistakes

1. Calling an arrow function before its definition

No hoisting; you get ReferenceError: Cannot access 'f' before initialization.

2. Expecting this to mean the caller in an arrow function

Arrows capture this lexically. A method written as an arrow will not see the object it is called on.

3. Returning an object literal without parentheses

() => { name } returns undefined; write () => ({ name }).

4. A return on its own line

Automatic semicolon insertion ends the statement; the value on the next line is unreachable.

5. Closures holding large objects

Anything referenced by a long-lived closure stays in memory. Do not capture a whole array when you need one field.

6. Async callbacks capturing a loop variable

With var they all see the final value; with let each iteration is independent.

7. Reassigning parameters

Mutating a parameter makes the function harder to reason about. Copy into a local const when you need a derived value.

Summary

  • function for hoisting, this, or new; arrows for callbacks.
  • Defaults apply for undefined; rest parameters collect the remainder into an array.
  • Closures keep their defining scope alive, which enables private state.
  • Declare loop counters with let to give each iteration its own binding.
  • this comes from the call site for regular functions, from the enclosing scope for arrows.

Next: the array methods you will use every day.