Can you write this from memory?
Define a function `greet` taking `name` as an argument.
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?"
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.
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.
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/bindto changethis
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.
argumentsis 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.
| Pattern | Code |
|---|---|
| Declaration (hoisted) | function f() {} |
| Expression | const f = function() {} |
| Named expression | const f = function name() {} |
| Arrow | const f = () => {} |
| Arrow implicit return | const f = x => x * 2 |
| Return object literal | () => ({ key: value }) |
| Default param | (x = 10) => x |
| Rest params | (...args) => args |
| Destructuring | ({ a, b }) => a + b |
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
Explain closures and build a counter function.
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
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
console.log(add(2, 3)); // Works! Declarations are hoisted
function add(a, b) {
return a + b;
}const parseUser = function parseUser(raw) {
return JSON.parse(raw);
};const makeUser = (name) => ({ name, createdAt: Date.now() });const greet = (name = "stranger") => `Hello, ${name}!`;const sum = (...nums) => nums.reduce((a, b) => a + b, 0);function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const c = createCounter();
c(); // 1
c(); // 2const greet = ({ name, age }) => `${name} is ${age}`;JavaScript Functions Sample Exercises
Define a function `sum` that accepts arbitrary arguments into an array `args`.
function sum(...args) {
}
Define a function `greet` where `name` defaults to "World".
function greet(name = "World") {
}
What happens when you call a function with fewer args than params?
undefined+ 42 more exercises