Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. JavaScript
  3. JavaScript Functions Practice
JavaScript45 exercises

JavaScript Functions Practice

Practice function declarations vs expressions vs arrows. Hoisting behavior, this binding, closures, default/rest parameters, and the function syntax you should know cold.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Define a function `greet` taking `name` as an argument.

On this page
  1. 1Four ways to write functions
  2. 2Hoisting: why you can call declarations before they appear
  3. 3Arrow functions: when to use them, when NOT to
  4. Arrow function limitations
  5. 4Closures: functions remember their scope
  6. Common closure patterns
  7. The closure trap (capturing by reference)
  8. 5Default and rest parameters
  9. Default parameters
  10. Rest parameters
  11. 6Returning objects from arrows
  12. 7Quick reference
  13. 8References
Four ways to write functionsHoisting: why you can call declarations before they appearArrow functions: when to use them, when NOT toClosures: functions remember their scopeDefault and rest parametersReturning objects from arrowsQuick referenceReferences

JavaScript has multiple ways to define functions, and choosing the wrong one creates subtle bugs:

  • Function declarations are hoisted: you can call them before they appear in your code.
  • Arrow functions capture the surrounding this. Great for callbacks, bad for object methods.
  • Closures happen automatically: every function remembers variables from its outer scope.

You'll work through callbacks, factories, methods, and debugging "why is this undefined?"

Related JavaScript Topics
JavaScript ClassesJavaScript Async/AwaitJavaScript LoopsJavaScript Collections

Declarations are hoisted and have their own this. Arrows capture surrounding this (great for callbacks, bad for methods). Know which to reach for.

JavaScript has four main function syntaxes, each with different behavior:

// 1. Function declaration (hoisted, has own 'this')
function add(a, b) {
  return a + b;
}

// 2. Function expression (not hoisted, has own 'this')
const add = function(a, b) {
  return a + b;
};

// 3. Arrow function (not hoisted, captures outer 'this')
const add = (a, b) => a + b;

// 4. Async function (returns a Promise, enables await)
async function fetchData() {
  const res = await fetch('/api/data');
  return res.json();
}

The differences matter when you use them as methods or callbacks. Async functions can use any of the first three syntaxes (async function, async () => {}, etc.). If you're coming from Python, the Python vs JavaScript functions comparison highlights the key differences in scoping and this binding.


Ready to practice?

Start practicing JavaScript Functions with spaced repetition

Function declarations are fully hoisted (callable before their line). const f = () => {} is NOT hoisted—accessing it before the line throws a ReferenceError (TDZ).

Hoisting comparison: function declarations are callable everywhere, var hoists as undefined, const/let are in the TDZ until their declaration line

Function declarations are fully hoisted, moved to the top of their scope:

greet("Alice"); // Works! Declaration is hoisted

function greet(name) {
  console.log(`Hello, ${name}`);
}

Function expressions are NOT hoisted the same way:

greet("Alice"); // TypeError: greet is not a function

var greet = function(name) {
  console.log(`Hello, ${name}`);
};

With var, the variable greet is hoisted as undefined, so calling it throws an error.

With let or const, it's even stricter. You get a ReferenceError (temporal dead zone):

greet("Alice"); // ReferenceError: Cannot access 'greet' before initialization

const greet = (name) => console.log(`Hello, ${name}`);

Rule of thumb: Use declarations for functions you need to call before they appear. Use expressions/arrows for callbacks and inline functions.


Arrows inherit this from their surrounding scope. Use them for callbacks and closures. Never use them as object methods or constructors—this won't be what you expect.

Regular functions get this from how they are called — obj.method() gives this = obj, but bare fn() gives this = undefined. Arrow functions always capture this from the surrounding lexical scope

Arrow functions are great for callbacks because they're concise and capture this lexically:

// Good: arrow in callback
const doubled = numbers.map(n => n * 2);

// Good: arrow preserves 'this' in event handler
class Timer {
  count = 0;
  start() {
    setInterval(() => {
      this.count++; // 'this' refers to Timer instance
    }, 1000);
  }
}

Do NOT use arrows as object methods:

// BAD: arrow as method
const user = {
  name: "Alice",
  greet: () => {
    console.log(`Hi, I'm ${this.name}`); // 'this' is NOT user!
  }
};
user.greet(); // "Hi, I'm undefined"

// GOOD: regular function as method
const user = {
  name: "Alice",
  greet() {
    console.log(`Hi, I'm ${this.name}`); // 'this' is user
  }
};
user.greet(); // "Hi, I'm Alice"

Arrow function limitations

Arrow functions cannot:

  • Be used as constructors (new Arrow() throws TypeError)
  • Access arguments (use rest parameters instead)
  • Be used with call/apply/bind to change this

Closure scope chain: createCounter returns a function that retains access to the count variable even after createCounter returns

Every function in JavaScript is a closure. It retains access to variables from its outer scope:

function createCounter() {
  let count = 0; // This variable is "closed over"

  return function increment() {
    count += 1;
    return count;
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

The inner function increment keeps access to count even after createCounter has returned.

Common closure patterns

Private state:

function createWallet(initial) {
  let balance = initial;
  return {
    deposit: (amount) => { balance += amount; },
    withdraw: (amount) => { balance -= amount; },
    getBalance: () => balance
  };
}

Memoization: The closure-based factory pattern here is also at the heart of the Strategy design pattern, where you pass different functions to change behavior at runtime.

function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (!(key in cache)) {
      cache[key] = fn(...args);
    }
    return cache[key];
  };
}

The closure trap (capturing by reference)

Closures capture variables by reference, not by value:

// BUG: All functions log 3
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() { console.log(i); });
}
funcs[0](); // 3 (not 0!)

// FIX 1: Use let (block scoped)
for (let i = 0; i < 3; i++) {
  funcs.push(function() { console.log(i); });
}

// FIX 2: Create a new scope with IIFE
for (var i = 0; i < 3; i++) {
  funcs.push((function(j) {
    return function() { console.log(j); };
  })(i));
}

Default parameters

Defaults apply when an argument is undefined or missing, NOT when it's null:

function greet(name = "stranger") {
  console.log(`Hello, ${name}`);
}

greet();          // "Hello, stranger"
greet(undefined); // "Hello, stranger"
greet(null);      // "Hello, null" (null does NOT trigger default!)
greet("");        // "Hello, " (empty string does NOT trigger default)

Rest parameters

Rest parameters collect remaining arguments into a real array:

function sum(...numbers) {
  return numbers.reduce((a, b) => a + b, 0);
}

sum(1, 2, 3, 4); // 10

Why rest params beat arguments: For a detailed comparison of how TypeScript adds type safety to these patterns, see the JavaScript vs TypeScript functions comparison.

  • arguments is array-like (no .map, .filter, etc.)
  • Arrow functions don't have arguments
  • Rest params are explicit and readable
// Old way (avoid)
function sum() {
  return Array.from(arguments).reduce((a, b) => a + b, 0);
}

// Modern way (prefer)
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);

This is a common gotcha. Curly braces are ambiguous because they could be a block or an object literal:

// BUG: Returns undefined (braces parsed as block)
const getUser = () => { name: "Alice" };

// FIX: Wrap object in parentheses
const getUser = () => ({ name: "Alice" });

Without parentheses, { name: "Alice" } is parsed as a block statement with a labeled expression, which returns undefined.


PatternCode
Declaration (hoisted)function f() {}
Expressionconst f = function() {}
Named expressionconst f = function name() {}
Arrowconst f = () => {}
Arrow implicit returnconst f = x => x * 2
Return object literal() => ({ key: value })
Default param(x = 10) => x
Rest params(...args) => args
Destructuring({ a, b }) => a + b

  • MDN: Functions
  • MDN: Arrow function expressions
  • MDN: Closures
  • MDN: Default parameters
  • MDN: Rest parameters

When to Use JavaScript Functions

  • Create reusable behavior and keep code DRY.
  • Use closures to capture state (factories, callbacks, memoization).
  • Use arrow functions for concise callbacks and lexical this.
  • Prefer function declarations for named functions you call in multiple places (clearer stack traces + hoisting).

Check Your Understanding: JavaScript Functions

Prompt

Explain closures and build a counter function.

What a strong answer looks like

Return an inner function that increments a private variable; explain how the inner function retains access to its outer scope even after the outer function has returned.

What You'll Practice: JavaScript Functions

Function declarations vs expressions (and when hoisting matters)Named function expressions (better stack traces)Arrow function syntax + implicit returnsthis binding (regular function vs arrow function)Default parametersRest parameters (...args) vs argumentsDestructuring in parametersClosures and scopeIIFE pattern (Immediately Invoked Function Expression)

Common JavaScript Functions Pitfalls

  • Using arrow functions as object methods (lexical this causes bugs)
  • Assuming function expressions are callable before initialization
  • Implicit return gotchas (object literals need parentheses)
  • Using arguments in arrow functions (it's not available)
  • Default params don't apply to null (only undefined/missing)
  • Trying to use arrow functions with `new` (not constructible)
  • Forgetting that closures capture by reference, not value
  • Passing fn() instead of fn as a callback (invokes immediately instead of passing reference)

JavaScript Functions FAQ

Arrow function or function declaration?

Use arrows for callbacks or when you want lexical `this`. Use function declarations for named functions, hoisting, and clearer stack traces.

What is function hoisting?

Function declarations are hoisted to the top of their scope, so you can call them before their definition. Function expressions depend on the variable they're assigned to: `var` hoists undefined, `let`/`const` creates a temporal dead zone.

Why is `this` "wrong" inside my arrow function method?

Arrow functions don't have their own `this`. They capture `this` from the enclosing scope. If you use an arrow as an object method, `this` won't refer to the object. Use a regular function for methods.

How do I return an object literal from an arrow function?

Wrap the object in parentheses: `() => ({ ok: true })`. Without parentheses, `{}` is parsed as a block statement, not an object literal.

When do default parameters apply?

Default parameters are used when the argument is missing or explicitly `undefined`. Passing `null` does NOT trigger the default because null is a valid value.

Rest parameters vs arguments: which should I use?

Prefer rest parameters (`...args`) because they're a real array with all array methods. `arguments` is array-like (no .map, .filter), and arrow functions don't have `arguments` at all.

Can I use arrow functions with `new`?

No. Arrow functions are not constructible. Calling `new` on an arrow function throws a TypeError. Use a regular function or class for constructors.

What is a named function expression?

A function expression with a name: `const parse = function parseUser(raw) { ... }`. The name appears in stack traces (helpful for debugging) but is only accessible inside the function body.

Why does my callback run immediately instead of on click?

You wrote `onClick={handleClick()}` instead of `onClick={handleClick}`. Parentheses invoke the function immediately. Pass the function reference without parentheses, or wrap it: `onClick={() => handleClick(arg)}`.

JavaScript Functions Syntax Quick Reference

Function declaration (hoisted)
console.log(add(2, 3)); // Works! Declarations are hoisted
function add(a, b) {
  return a + b;
}
Named function expression (better stacks)
const parseUser = function parseUser(raw) {
  return JSON.parse(raw);
};
Arrow + object literal return
const makeUser = (name) => ({ name, createdAt: Date.now() });
Default parameters
const greet = (name = "stranger") => `Hello, ${name}!`;
Rest parameters (prefer over arguments)
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);
Closure factory (counter)
function createCounter() {
  let count = 0;
  return function () {
    count += 1;
    return count;
  };
}
const c = createCounter();
c(); // 1
c(); // 2
Destructuring in parameters
const greet = ({ name, age }) => `${name} is ${age}`;

JavaScript Functions Sample Exercises

Example 1Difficulty: 2/5

Define a function `sum` that accepts arbitrary arguments into an array `args`.

function sum(...args) {
}
Example 2Difficulty: 2/5

Define a function `greet` where `name` defaults to "World".

function greet(name = "World") {
}
Example 3Difficulty: 2/5

What happens when you call a function with fewer args than params?

undefined

+ 42 more exercises

Related Design Patterns

Strategy Pattern

Also in Other Languages

Python Function Arguments Practice: defaults, *args, keyword-only, **kwargs

Start practicing JavaScript Functions

Free daily exercises with spaced repetition. No credit card required.

← Back to JavaScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.