webdev.complete
🔧 Functions, Scope, Closures
JavaScript
Lesson 27 of 117
30 min

Functions Every Way

Declarations, expressions, arrows, defaults, rest, spread.

Functions are how you give a name to a chunk of behavior so you can reuse it. JS has, conservatively, three and a half ways to write one. They look similar, but each has a slightly different personality. Knowing which to pick is most of the battle.

Three ways to make a function

js
// 1. Function declaration
function add(a, b) {
  return a + b;
}

// 2. Function expression
const subtract = function (a, b) {
  return a - b;
};

// 3. Arrow function
const multiply = (a, b) => a * b;

// All three work the same when called:
add(2, 3);       // 5
subtract(5, 1);  // 4
multiply(3, 4);  // 12

The differences only show up at the edges: how they hoist, what they do with this, and how they read.

Hoisting: declarations move up

Function declarationsare hoisted, meaning you can call them before they appear in your file. Expressions and arrows aren't:

js
hello();   // Works - hoisted

function hello() {
  console.log("hi");
}

hi();      // ReferenceError - temporal dead zone

const hi = () => console.log("hi");
Just put functions before they're called
Hoisting feels clever until you debug it at 2am. Define a function before you use it and you never have to think about which kind it is.

Arrow functions: shorter and quieter

Arrows have a compact syntax for short functions:

js
// Full form
const add = (a, b) => {
  return a + b;
};

// Single expression - return is implicit, no braces
const add = (a, b) => a + b;

// Single param - parens optional (linters usually want them)
const double = n => n * 2;

// Returning an object literal? Wrap in parens
const wrap = n => ({ value: n });   // not { value: n }, that's a block

this: the one real difference

Regular functions have their own this. Arrow functions don't - they inherit this from where they were defined. This matters most inside object methods and callbacks:

js
const counter = {
  count: 0,

  // Regular function: 'this' is the counter
  increment() {
    this.count++;
  },

  // Inside a callback, regular functions LOSE 'this'
  startBroken() {
    setInterval(function () {
      this.count++;  // 'this' is undefined or window here
    }, 1000);
  },

  // Arrow callbacks keep the outer 'this'
  startFixed() {
    setInterval(() => {
      this.count++;  // 'this' is still the counter
    }, 1000);
  },
};
Don't use an arrow function as an object method if you need this to be the object. Use shorthand methods like increment() {} instead.

Default parameters

js
function greet(name = "stranger", greeting = "Hello") {
  return greeting + ", " + name + "!";
}

greet();                  // "Hello, stranger!"
greet("Ada");             // "Hello, Ada!"
greet("Ada", "Howdy");    // "Howdy, Ada!"

// Defaults only kick in for undefined (not null)
greet(undefined, "Hi");   // "Hi, stranger!"
greet(null, "Hi");        // "Hi, null!"

Rest parameters: catch all the extras

js
// ...args collects every remaining argument into an array
function sum(...nums) {
  return nums.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);          // 6
sum(1, 2, 3, 4, 5);    // 15

// Can mix with named params
function tagged(tag, ...items) {
  return tag + ": " + items.join(", ");
}

tagged("Fruits", "apple", "pear", "kiwi");
// "Fruits: apple, pear, kiwi"

Spread: the other ...

Same dots, opposite direction. ... in a function signature collects; in a call (or array/object literal) it spreads:

js
const nums = [1, 2, 3, 4];

Math.max(...nums);        // 4   (spreads to Math.max(1, 2, 3, 4))

const combined = [0, ...nums, 5];     // [0, 1, 2, 3, 4, 5]
const copy = [...nums];               // shallow clone

const a = { x: 1 };
const b = { ...a, y: 2 };             // { x: 1, y: 2 }

Try them all live

// Three flavors
function declared(name) { return "Hello " + name; }
const expressed = function (name) { return "Hello " + name; };
const arrowed = name => "Hello " + name;

console.log(declared("Ada"));
console.log(expressed("Grace"));
console.log(arrowed("Margaret"));

// Defaults
function greet(name = "stranger") {
  return "Hi, " + name;
}
console.log(greet());
console.log(greet("Linus"));

// Rest + spread
function sum(...n) { return n.reduce((a, b) => a + b, 0); }
console.log("sum:", sum(1, 2, 3, 4, 5));

const arr = [3, 1, 4, 1, 5, 9, 2, 6];
console.log("max:", Math.max(...arr));

// The 'this' gotcha
const obj = {
  name: "Counter",
  reportRegular: function () {
    setTimeout(function () {
      console.log("regular:", this?.name); // undefined in strict mode
    }, 100);
  },
  reportArrow: function () {
    setTimeout(() => {
      console.log("arrow:", this.name);    // "Counter"
    }, 100);
  },
};

obj.reportRegular();
obj.reportArrow();

Quiz

Quiz1 / 3

Which kind of function is safe to call before its definition?

Recap

  • Function declarations hoist. Arrows and function expressions don't.
  • Arrows inherit this from the enclosing scope. Regular functions get their own.
  • Default params catch undefined (not null).
  • ...rest collects into an array in a signature. ...spread unpacks in calls and literals.
  • When in doubt: short helper or callback → arrow. Object method or something that needs this → shorthand method.